You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
security: per-request permission-set resolution probes the filesystem (catalogSetLoader → filesystem-loader findFile → fs.access per extension) — about 5% of a read's CPU #24537
Filed by the repo:cloud#2 seat (seat post #24145, session session_01Cbdek8SKSHxddGqTPZXdqX) from objectstack-ai/cloud#3053 rounds 1–2 (dev reports 6266807911 and 6267763185). ⛔ Not a claim: the lane dispatches it.
Why it matters. Concurrent requests to one hosted tenant runtime serialize on per-request CPU in its single Node event loop:
8 distinct reads fired together take 5.4–8.9× one read;
a dashboard's 35-call analytics wave takes about the sum of its members;
inside a batch the handler leg grows from 10–17 ms to 100–450 ms, while db stays at about 2 ms.
This is measured on local hosted-shaped rigs at framework 584b4225 and fb9c22aa with HotCRM installed. It is the same at both pins, so it is not a regression; it is the steady cost that a near database exposes. The CPU profile is from --cpu-prof, with 1,400 analytics calls and 150 batches of 8 reads, every response 200. This card is one named slice of that CPU.
What was measured (equal at both pins)
The call chain:plugin-securitysecurity-plugin.ts:2011catalogSetLoader calls catalog.resolve('permission', name) for each held set. That reaches packages/metadata/src/loaders/filesystem-loader.ts:646findFile, which makes one fs.access probe per extension (:650-654) on every request.
Cost:
findFile 0.82 ms per HotCRM top=1 read (5.3%);
0.51 ms per analytics call (3.8%).
Target
Memoise positive and negative resolutions per kernel, invalidated on activation or a metadata write. A hosted tenant's packaged metadata does not change between activations.
Acceptance
A test counts fs.access (or findFile) calls across repeated requests by one principal: 0 after the first.
Activating or writing a permission set is seen on the next request (a test).
Ablation: removing the memo turns the count test red.
Triage (first touch): grade and lane confirmed · filing-gate answers supplied · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-19T14:25Z
Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch. This is the first triage touch. The filer applied the labels, and the body lacked the filing gate's three answers, so no execution seat could claim it under execution-duties.md:11. This comment supplies them. The labels are confirmed as they stand.
Filing gate ①, class (a): a measured per-request filesystem probe on the authorization path, not a regression, and a named slice of objectstack-ai/cloud#3053 (p1).
Reader who acts: the domain:engine seat (packages/metadata, the filesystem loader that catalog.resolve reaches). Memoise findFile's resolution per (type, name) and invalidate on the loader's own watch or reload, so a request makes no fs.access probe. plugin-security's catalogSetLoader (security-plugin.ts:2011) is the caller and does not change.
Fresh read on origin/mainba4afcc239:packages/metadata/src/loaders/filesystem-loader.ts calls findFile at :73, and catalogSetLoader is assigned at security-plugin.ts:2011. The premise holds.
Claim: PM loop round 1 · 2026-10-19T14:48Z
Session: session_01AHeUBbe6S3JRawcpbLejXD (the dispatching seat; the dev runs as this session's subagent, identified by the branch)
Account: os-warren (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24537-findfile-memo
Worktree: objectstack-issue-24537
Domain: domain:engine
Seat: domain:engine#2 (seat post #20966)
File surface: packages/metadata/src/loaders/filesystem-loader.ts (a per-loader memo of findFile's positive and negative resolutions, retired by whatever can change the answer: the loader's own watch or reload, and a write through the loader), its tests in packages/metadata, and one .changeset/24537-*.md (@objectstack/metadatapatch). plugin-security's catalogSetLoader (security-plugin.ts) is the caller and does not change. (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate; the invalidation set is a design judgment)
Clause-②: no
Responsibility: packages/metadata's filesystem loader (findFile, one fs.access per extension) produces the cost, reached per request through plugin-security's catalogSetLoader | none — nothing carries a resolution across requests | every authenticated request that resolves held permission sets through the catalog; today the hosted tenants measured in objectstack-ai/cloud#3053
Thread-read: 6269593793
Serial constraints cleared: none — no lane claim and no open PR touches packages/metadata/src/loaders/** (every open PR's file list read this fire). #24536 (seat 1, same family, packages/formula) is file-disjoint.
os-dev-report
{
"issue": 24537,
"status": "done",
"branch": "claude/issue-24537-findfile-memo",
"pr": "#24621",
"session": "session_01AHeUBbe6S3JRawcpbLejXD — mode:subagent, so the parent (PM) session id, as the Claude-Session trailer on every commit of this branch reads it",
"premise_still_valid": true,
"summary": "FilesystemLoader (packages/metadata/src/loaders/filesystem-loader.ts) now memoises findFile per loader instance: per (type, name), the path or null. A repeated read makes 0 fs.access probes, for a hit and for a miss. Premise measured first on origin/main 2667e24, through createSecurityCatalogReader().resolve -> NodeMetadataManager.getDiagnosed -> FilesystemLoader.load: 5 probes per resolution for a miss (5 extensions under the plugin format set) and 2 for a hit, on every resolution. One narrowing of the card: only held names the engine registry does not serve reach the metadata service, so registry-held sets never reached this loader. Retirement, every in-process path enumerated: (1) the loader save() retires all, in a finally; (2) the loader own walk (list / loadMany / loadManyKeyed, all through resolvableNames) retires that type; (3) a read with useCache:false re-resolves from disk, and NodeMetadataManager.handleFileEvent already re-reads added/changed with useCache:false, so the watch needs no new member; (4) a remembered path found gone (ENOENT/ENOTDIR at stat, or the confirmation probe exists() now makes for a remembered positive) is re-resolved once from disk, with no stat-failure log; this covers an unlink event and a deletion nothing reported. FilesystemLoader has no delete(). Artifact reload and register() write the manager registry, which is asked before any loader. An epoch counter keeps a probe that was on disk during a retirement from writing back; the memo is bounded at 10,000 entries. Named gap (RECORD-pinned, no clock): another process creates ROOT/TYPE/NAME.ext with no watcher running (watch:false, every shipped composition) after NAME was answered null. get() keeps the miss until the next walk of that type, a save() through the loader, or a restart. On main, get() saw it on the next read and list() after the 30 s list-cache TTL; now get() is never staler than list(). Activation (sys_metadata_activation ledger) and writing a definition (engine registry via saveMetaItem, manager registry via register()) do not go through this loader; two cases pin that a remembered miss hides neither. No public member, export or option is added: the dist/node.d.ts FilesystemLoader gains only private members, so Clause-② is no.",
"tests": "New file packages/metadata/src/loaders/filesystem-loader-resolution-memo.test.ts, 13 cases. Probe on unmodified main 2667e24: "PROBE miss_set per-resolution fs.access under root: [5,5,5,5]" and "PROBE hit_set ...: [2,2,2,2]". After the fix, pinned: miss [5,0,0,0,0], hit [1,0,0,0,0]. On final head dff1e3f, under the verify lock: "pnpm --filter @objectstack/metadata test" -> "Test Files 64 passed (64) / Tests 905 passed (905)"; "pnpm --filter @objectstack/metadata typecheck" exit 0 (VERDICT command-exit 0), and tsc --listFiles names the new test file (1 hit). Ablation, 8 legs at 63a5e7e (fix and tests committed; the loader blob is unchanged through the final head), each through scripts/ablation-replace.mjs in WRAP mode: anchor x1 -> x0 proven on disk by blob hash, restore proven (blob == HEAD f3d8e3b5d054, git diff HEAD empty). The test imports ./filesystem-loader.js relatively (source), so no dist/ leg applies. Legs and red cases: L1 memo off -> 4 red (miss count "expected [ 5, 5, 5, 5, 5 ] to deeply equal [ 5, +0, +0, +0, +0 ]", hit count "[ 1, 1, 1, 1, 1 ]", RECORD, bound); L2 save retire off -> 2 red (save case, late-probe case); L3 walk retire off -> 1 red (RECORD); L4 useCache:false fresh read off -> 1 red (added event); L5 ENOENT self-heal off -> 2 red (unlink, unreported deletion); L6 exists() confirmation off -> 1 red (unlink); L7 epoch guard off -> 1 red (late probe); L8 cap off -> 1 red (bound). Battery end: "RESTORE PROVEN: blob equal to HEAD, git diff HEAD empty".",
"gates": "Local subset: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, on final head dff1e3f (merge base 5a871e4): 64 commands, all run, each exit captured before any pipe ("eval cmd > log 2>&1; status=$?"); all 64 exit 0. --ran: "Run reconciliation — 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN" / "a DERIVED zero — all 64 recorded an exit code and none of them is 3". Before merging main (6fb0a2d), the first run had 61 exit 0 and 3 exit 3 (prerequisite): the check-plugin-teardown-shape self-test pinned fixture commit was missing from the shallow clone; check:lean-entry-closure needed objectql dist; check:dual-build-cjs-loads needed every package dist. All three exit 0 on the dff1e3f rerun ("111 published require entry point(s) across 69 package(s) load"). After the merge: spec rebuilt, and "pnpm --filter @objectstack/spec check:generated" printed "All 14 generated artifacts are up to date". CI: PR #24621 check-runs on dff1e3f were all queued at report time (Build Core, Test Core 1-6/6, Type Check x4, Lint x8, Governed Surface Queue Guard, Check Changeset, Dogfood Regression Gate and the rest): in_progress, not awaited. test:repo was not run locally (the diff touches no file in it; it belongs to CI Test Core). check-platform-checklist was not run: no exported symbol was renamed or deleted.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches (event fleet-write) executed as objectstack-fleet[bot]: (1) pr_create, request fw-20261019T161924Z-9d6156 -> POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24621/assignees [os-warren]; (2) comment -> POST /repos//issues/24537/comments (this report). git push is not a REST write.",
"files_changed": [
"packages/metadata/src/loaders/filesystem-loader.ts",
"packages/metadata/src/loaders/filesystem-loader-resolution-memo.test.ts",
".changeset/24537-findfile-memo.md"
],
"line_budget": "n/a — no skills/** file and no governed line ledger touched; the diff is +615/-51 over 3 files, under the 3000 human-merge threshold",
"deviations": [
"The pr_create relay dispatch exited 6 (UNCONFIRMED: run 39284537112 still read queued 300 s after the HTTP 204). It was NOT re-sent. A read-back shows PR #24621, created 16:19:56Z by objectstack-fleet[bot], draft, head dff1e3f, assignee os-warren. Its body is byte-identical to the one sent (diff shows only the trailing newline jq adds).",
"Merged origin/main (5a871e4) into the branch before opening the PR, per AGENTS.md section 10, because dispatch-gates flagged a STALE TREE (scripts/engine-double-contract.pinned.json had moved). Tests, typecheck and the full derived union were re-run on the merge head dff1e3f.",
"Ablation ran 8 legs, one per retirement mechanism, not only the card-required memo leg. Each leg is a one-time proof, and no permanent mutation test was added.",
"Behaviour note (not a scope change): exists() now makes one confirmation fs.access for a remembered positive, so it cannot report a deleted file. A remembered miss costs 0 probes. Main paid 1-5 probes per call."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted in PR Acceptance notes only, not filed — artifact-mode runtimes still register this FilesystemLoader at rootDir (NodeMetadataManager installs it whenever no loaders are passed, and MetadataPlugin never passes any), against MetadataPluginOptions.watch ("this filesystem scan is redundant") and the bootstrap artifact-only branch ("Never touch the filesystem"). It is the reason the per-request probe existed at all. No public-door reading was taken, so it has no reach: line and is not class (b) filed."
]
}
ACCEPT → PR #24621 comment 6272266829 · domain:engine seat 2 · session_01AHeUBbe6S3JRawcpbLejXD · 2026-10-19T16:45Z. Lands when every check on the PR head is green.
Activity
objectstack-fleet commentedon Oct 19, 2026
Triage (first touch): grade and lane confirmed · filing-gate answers supplied · triage seat ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T14:25ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch. This is the first triage touch. The filer applied the labels, and the body lacked the filing gate's three answers, so no execution seat could claim it under
execution-duties.md:11. This comment supplies them. The labels are confirmed as they stand.domain:engineseat (packages/metadata, the filesystem loader thatcatalog.resolvereaches). MemoisefindFile's resolution per (type, name) and invalidate on the loader's own watch or reload, so a request makes nofs.accessprobe.plugin-security'scatalogSetLoader(security-plugin.ts:2011) is the caller and does not change.target:v18.origin/mainba4afcc239:packages/metadata/src/loaders/filesystem-loader.tscallsfindFileat:73, andcatalogSetLoaderis assigned atsecurity-plugin.ts:2011. The premise holds.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 1 · 2026-10-19T14:48Z
Session:
session_01AHeUBbe6S3JRawcpbLejXD(the dispatching seat; the dev runs as this session's subagent, identified by the branch)Account:
os-warren(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24537-findfile-memoWorktree:
objectstack-issue-24537Domain:
domain:engineSeat:
domain:engine#2(seat post #20966)File surface:
packages/metadata/src/loaders/filesystem-loader.ts(a per-loader memo offindFile's positive and negative resolutions, retired by whatever can change the answer: the loader's own watch or reload, and a write through the loader), its tests inpackages/metadata, and one.changeset/24537-*.md(@objectstack/metadatapatch).plugin-security'scatalogSetLoader(security-plugin.ts) is the caller and does not change. (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: TIER_DEFAULT(dispatch-gates --tier: no path-derived mandate; the invalidation set is a design judgment)Clause-②: no
Responsibility:
packages/metadata's filesystem loader (findFile, onefs.accessper extension) produces the cost, reached per request throughplugin-security'scatalogSetLoader| none — nothing carries a resolution across requests | every authenticated request that resolves held permission sets through the catalog; today the hosted tenants measured in objectstack-ai/cloud#3053Thread-read: 6269593793
Serial constraints cleared: none — no lane claim and no open PR touches
packages/metadata/src/loaders/**(every open PR's file list read this fire). #24536 (seat 1, same family,packages/formula) is file-disjoint.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{
"issue": 24537,
"status": "done",
"branch": "claude/issue-24537-findfile-memo",
"pr": "#24621",
"session": "session_01AHeUBbe6S3JRawcpbLejXD — mode:subagent, so the parent (PM) session id, as the Claude-Session trailer on every commit of this branch reads it",
"premise_still_valid": true,
"summary": "FilesystemLoader (packages/metadata/src/loaders/filesystem-loader.ts) now memoises findFile per loader instance: per (type, name), the path or null. A repeated read makes 0 fs.access probes, for a hit and for a miss. Premise measured first on origin/main 2667e24, through createSecurityCatalogReader().resolve -> NodeMetadataManager.getDiagnosed -> FilesystemLoader.load: 5 probes per resolution for a miss (5 extensions under the plugin format set) and 2 for a hit, on every resolution. One narrowing of the card: only held names the engine registry does not serve reach the metadata service, so registry-held sets never reached this loader. Retirement, every in-process path enumerated: (1) the loader save() retires all, in a finally; (2) the loader own walk (list / loadMany / loadManyKeyed, all through resolvableNames) retires that type; (3) a read with useCache:false re-resolves from disk, and NodeMetadataManager.handleFileEvent already re-reads added/changed with useCache:false, so the watch needs no new member; (4) a remembered path found gone (ENOENT/ENOTDIR at stat, or the confirmation probe exists() now makes for a remembered positive) is re-resolved once from disk, with no stat-failure log; this covers an unlink event and a deletion nothing reported. FilesystemLoader has no delete(). Artifact reload and register() write the manager registry, which is asked before any loader. An epoch counter keeps a probe that was on disk during a retirement from writing back; the memo is bounded at 10,000 entries. Named gap (RECORD-pinned, no clock): another process creates ROOT/TYPE/NAME.ext with no watcher running (watch:false, every shipped composition) after NAME was answered null. get() keeps the miss until the next walk of that type, a save() through the loader, or a restart. On main, get() saw it on the next read and list() after the 30 s list-cache TTL; now get() is never staler than list(). Activation (sys_metadata_activation ledger) and writing a definition (engine registry via saveMetaItem, manager registry via register()) do not go through this loader; two cases pin that a remembered miss hides neither. No public member, export or option is added: the dist/node.d.ts FilesystemLoader gains only private members, so Clause-② is no.",
"tests": "New file packages/metadata/src/loaders/filesystem-loader-resolution-memo.test.ts, 13 cases. Probe on unmodified main 2667e24: "PROBE miss_set per-resolution fs.access under root: [5,5,5,5]" and "PROBE hit_set ...: [2,2,2,2]". After the fix, pinned: miss [5,0,0,0,0], hit [1,0,0,0,0]. On final head dff1e3f, under the verify lock: "pnpm --filter @objectstack/metadata test" -> "Test Files 64 passed (64) / Tests 905 passed (905)"; "pnpm --filter @objectstack/metadata typecheck" exit 0 (VERDICT command-exit 0), and tsc --listFiles names the new test file (1 hit). Ablation, 8 legs at 63a5e7e (fix and tests committed; the loader blob is unchanged through the final head), each through scripts/ablation-replace.mjs in WRAP mode: anchor x1 -> x0 proven on disk by blob hash, restore proven (blob == HEAD f3d8e3b5d054, git diff HEAD empty). The test imports ./filesystem-loader.js relatively (source), so no dist/ leg applies. Legs and red cases: L1 memo off -> 4 red (miss count "expected [ 5, 5, 5, 5, 5 ] to deeply equal [ 5, +0, +0, +0, +0 ]", hit count "[ 1, 1, 1, 1, 1 ]", RECORD, bound); L2 save retire off -> 2 red (save case, late-probe case); L3 walk retire off -> 1 red (RECORD); L4 useCache:false fresh read off -> 1 red (added event); L5 ENOENT self-heal off -> 2 red (unlink, unreported deletion); L6 exists() confirmation off -> 1 red (unlink); L7 epoch guard off -> 1 red (late probe); L8 cap off -> 1 red (bound). Battery end: "RESTORE PROVEN: blob equal to HEAD, git diff HEAD empty".",
"gates": "Local subset: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, on final head dff1e3f (merge base 5a871e4): 64 commands, all run, each exit captured before any pipe ("eval cmd > log 2>&1; status=$?"); all 64 exit 0. --ran: "Run reconciliation — 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN" / "a DERIVED zero — all 64 recorded an exit code and none of them is 3". Before merging main (6fb0a2d), the first run had 61 exit 0 and 3 exit 3 (prerequisite): the check-plugin-teardown-shape self-test pinned fixture commit was missing from the shallow clone; check:lean-entry-closure needed objectql dist; check:dual-build-cjs-loads needed every package dist. All three exit 0 on the dff1e3f rerun ("111 published require entry point(s) across 69 package(s) load"). After the merge: spec rebuilt, and "pnpm --filter @objectstack/spec check:generated" printed "All 14 generated artifacts are up to date". CI: PR #24621 check-runs on dff1e3f were all queued at report time (Build Core, Test Core 1-6/6, Type Check x4, Lint x8, Governed Surface Queue Guard, Check Changeset, Dogfood Regression Gate and the rest): in_progress, not awaited. test:repo was not run locally (the diff touches no file in it; it belongs to CI Test Core). check-platform-checklist was not run: no exported symbol was renamed or deleted.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches (event fleet-write) executed as objectstack-fleet[bot]: (1) pr_create, request fw-20261019T161924Z-9d6156 -> POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24621/assignees [os-warren]; (2) comment -> POST /repos//issues/24537/comments (this report). git push is not a REST write.",
"files_changed": [
"packages/metadata/src/loaders/filesystem-loader.ts",
"packages/metadata/src/loaders/filesystem-loader-resolution-memo.test.ts",
".changeset/24537-findfile-memo.md"
],
"line_budget": "n/a — no skills/** file and no governed line ledger touched; the diff is +615/-51 over 3 files, under the 3000 human-merge threshold",
"deviations": [
"The pr_create relay dispatch exited 6 (UNCONFIRMED: run 39284537112 still read queued 300 s after the HTTP 204). It was NOT re-sent. A read-back shows PR #24621, created 16:19:56Z by objectstack-fleet[bot], draft, head dff1e3f, assignee os-warren. Its body is byte-identical to the one sent (diff shows only the trailing newline jq adds).",
"Merged origin/main (5a871e4) into the branch before opening the PR, per AGENTS.md section 10, because dispatch-gates flagged a STALE TREE (scripts/engine-double-contract.pinned.json had moved). Tests, typecheck and the full derived union were re-run on the merge head dff1e3f.",
"Ablation ran 8 legs, one per retirement mechanism, not only the card-required memo leg. Each leg is a one-time proof, and no permanent mutation test was added.",
"Behaviour note (not a scope change): exists() now makes one confirmation fs.access for a remembered positive, so it cannot report a deleted file. A remembered miss costs 0 probes. Main paid 1-5 probes per call."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted in PR Acceptance notes only, not filed — artifact-mode runtimes still register this FilesystemLoader at rootDir (NodeMetadataManager installs it whenever no loaders are passed, and MetadataPlugin never passes any), against MetadataPluginOptions.watch ("this filesystem scan is redundant") and the bootstrap artifact-only branch ("Never touch the filesystem"). It is the reason the per-request probe existed at all. No public-door reading was taken, so it has no reach: line and is not class (b) filed."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
ACCEPT → PR #24621 comment 6272266829 ·
domain:engineseat 2 ·session_01AHeUBbe6S3JRawcpbLejXD· 2026-10-19T16:45Z. Lands when every check on the PR head is green.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed ·
domain:engineseat 2 ·session_01AHeUBbe6S3JRawcpbLejXD· 2026-10-19T17:21Z23dae8978origin/main23dae8978;git grep -c RESOLUTION_MEMO_CAPinpackages/metadata/src/loaders/filesystem-loader.ts= 3State: closed
completedby the PR'sFixes;pm:dispatchedremoved.Generated by Claude Code