@monoes/monomindcli 2.0.3 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/control-start.cjs +13 -20
- package/.claude/helpers/event-logger.cjs +109 -6
- package/.claude/helpers/graphify-freshen.cjs +19 -17
- package/.claude/helpers/handlers/agent-start-handler.cjs +60 -28
- package/.claude/helpers/handlers/capture-handler.cjs +99 -28
- package/.claude/helpers/handlers/edit-handler.cjs +27 -9
- package/.claude/helpers/handlers/gates-handler.cjs +18 -9
- package/.claude/helpers/handlers/loops-status-handler.cjs +1 -1
- package/.claude/helpers/handlers/route-handler.cjs +33 -4
- package/.claude/helpers/handlers/session-handler.cjs +75 -45
- package/.claude/helpers/handlers/session-restore-handler.cjs +0 -12
- package/.claude/helpers/handlers/task-handler.cjs +53 -22
- package/.claude/helpers/hook-handler.cjs +75 -4
- package/.claude/helpers/intelligence.cjs +94 -15
- package/.claude/helpers/session.cjs +6 -1
- package/.claude/helpers/statusline.cjs +10 -10
- package/.claude/helpers/token-tracker.cjs +3 -3
- package/.claude/helpers/utils/agent-registrations.cjs +41 -0
- package/.claude/helpers/utils/fs-helpers.cjs +183 -0
- package/.claude/helpers/utils/micro-agents.cjs +10 -6
- package/.claude/helpers/utils/monograph.cjs +73 -2
- package/.claude/helpers/utils/telemetry.cjs +35 -19
- package/README.md +74 -92
- package/bin/cli.js +10 -1
- package/dist/src/agents/registry-builder.js.map +1 -1
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/browser/workflow/engine.d.ts.map +1 -1
- package/dist/src/browser/workflow/engine.js.map +1 -1
- package/dist/src/browser/workflow/store.d.ts +2 -2
- package/dist/src/browser/workflow/store.d.ts.map +1 -1
- package/dist/src/browser/workflow/store.js.map +1 -1
- package/dist/src/capabilities/manager.d.ts.map +1 -1
- package/dist/src/capabilities/watcher.d.ts.map +1 -1
- package/dist/src/commands/agent-lifecycle.js.map +1 -1
- package/dist/src/commands/agent-ops.d.ts.map +1 -1
- package/dist/src/commands/agent-ops.js +9 -15
- package/dist/src/commands/agent-ops.js.map +1 -1
- package/dist/src/commands/agent.d.ts.map +1 -1
- package/dist/src/commands/analyze-ast.js.map +1 -1
- package/dist/src/commands/analyze.d.ts.map +1 -1
- package/dist/src/commands/analyze.js.map +1 -1
- package/dist/src/commands/autopilot.d.ts.map +1 -1
- package/dist/src/commands/browse-action.d.ts.map +1 -1
- package/dist/src/commands/browse-action.js.map +1 -1
- package/dist/src/commands/browse-platform.d.ts.map +1 -1
- package/dist/src/commands/browse-workflow.d.ts.map +1 -1
- package/dist/src/commands/browse.d.ts +1 -1
- package/dist/src/commands/browse.d.ts.map +1 -1
- package/dist/src/commands/cleanup.d.ts.map +1 -1
- package/dist/src/commands/cleanup.js +20 -4
- package/dist/src/commands/cleanup.js.map +1 -1
- package/dist/src/commands/completions.d.ts.map +1 -1
- package/dist/src/commands/completions.js +66 -261
- package/dist/src/commands/completions.js.map +1 -1
- package/dist/src/commands/config.d.ts.map +1 -1
- package/dist/src/commands/crash-reporting.d.ts.map +1 -1
- package/dist/src/commands/design-detect.d.ts.map +1 -1
- package/dist/src/commands/design-palette.d.ts.map +1 -1
- package/dist/src/commands/doc.d.ts.map +1 -1
- package/dist/src/commands/doctor-env-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-env-checks.js +5 -0
- package/dist/src/commands/doctor-env-checks.js.map +1 -1
- package/dist/src/commands/doctor-project-checks.js.map +1 -1
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +7 -2
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/guidance.d.ts.map +1 -1
- package/dist/src/commands/hooks-workers.d.ts.map +1 -1
- package/dist/src/commands/hooks-workers.js +7 -0
- package/dist/src/commands/hooks-workers.js.map +1 -1
- package/dist/src/commands/hooks.d.ts.map +1 -1
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +3 -0
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/mcp.d.ts.map +1 -1
- package/dist/src/commands/mcp.js +11 -6
- package/dist/src/commands/mcp.js.map +1 -1
- package/dist/src/commands/memory-admin.d.ts.map +1 -1
- package/dist/src/commands/memory-admin.js +12 -6
- package/dist/src/commands/memory-admin.js.map +1 -1
- package/dist/src/commands/memory-transfer.d.ts.map +1 -1
- package/dist/src/commands/memory-transfer.js +6 -13
- package/dist/src/commands/memory-transfer.js.map +1 -1
- package/dist/src/commands/memory.d.ts.map +1 -1
- package/dist/src/commands/monograph.d.ts.map +1 -1
- package/dist/src/commands/monograph.js +3 -3
- package/dist/src/commands/monograph.js.map +1 -1
- package/dist/src/commands/org.d.ts +3 -1
- package/dist/src/commands/org.d.ts.map +1 -1
- package/dist/src/commands/org.js +321 -127
- package/dist/src/commands/org.js.map +1 -1
- package/dist/src/commands/performance.d.ts.map +1 -1
- package/dist/src/commands/platforms.d.ts +1 -1
- package/dist/src/commands/platforms.d.ts.map +1 -1
- package/dist/src/commands/providers.d.ts.map +1 -1
- package/dist/src/commands/report-crash.d.ts.map +1 -1
- package/dist/src/commands/route.d.ts.map +1 -1
- package/dist/src/commands/search-universal.d.ts.map +1 -1
- package/dist/src/commands/security.d.ts.map +1 -1
- package/dist/src/commands/session.d.ts.map +1 -1
- package/dist/src/commands/start.d.ts.map +1 -1
- package/dist/src/commands/start.js +142 -37
- package/dist/src/commands/start.js.map +1 -1
- package/dist/src/commands/status.d.ts.map +1 -1
- package/dist/src/commands/swarm.d.ts.map +1 -1
- package/dist/src/commands/task.d.ts.map +1 -1
- package/dist/src/commands/tokens.d.ts.map +1 -1
- package/dist/src/commands/tokens.js +21 -3
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/transfer-store.d.ts.map +1 -1
- package/dist/src/commands/update.d.ts.map +1 -1
- package/dist/src/config-adapter.d.ts.map +1 -1
- package/dist/src/config-adapter.js +37 -17
- package/dist/src/config-adapter.js.map +1 -1
- package/dist/src/consensus/audit-writer.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +64 -28
- package/dist/src/index.js.map +1 -1
- package/dist/src/init/claudemd-generator.d.ts.map +1 -1
- package/dist/src/init/claudemd-generator.js +44 -11
- package/dist/src/init/claudemd-generator.js.map +1 -1
- package/dist/src/init/executor.d.ts.map +1 -1
- package/dist/src/init/executor.js +93 -55
- package/dist/src/init/executor.js.map +1 -1
- package/dist/src/init/mcp-generator.js.map +1 -1
- package/dist/src/mcp-client.d.ts +8 -3
- package/dist/src/mcp-client.d.ts.map +1 -1
- package/dist/src/mcp-client.js +37 -3
- package/dist/src/mcp-client.js.map +1 -1
- package/dist/src/mcp-server.d.ts.map +1 -1
- package/dist/src/mcp-server.js +8 -2
- package/dist/src/mcp-server.js.map +1 -1
- package/dist/src/mcp-tools/agent-tools.d.ts +20 -0
- package/dist/src/mcp-tools/agent-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/agent-tools.js +10 -5
- package/dist/src/mcp-tools/agent-tools.js.map +1 -1
- package/dist/src/mcp-tools/analyze-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/auto-install.d.ts +8 -8
- package/dist/src/mcp-tools/auto-install.d.ts.map +1 -1
- package/dist/src/mcp-tools/auto-install.js.map +1 -1
- package/dist/src/mcp-tools/browser-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/browser-tools.js.map +1 -1
- package/dist/src/mcp-tools/claims-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/coherence/causal-infer.d.ts.map +1 -1
- package/dist/src/mcp-tools/coherence/causal-infer.js.map +1 -1
- package/dist/src/mcp-tools/coherence/coherence-check.d.ts.map +1 -1
- package/dist/src/mcp-tools/coherence/consensus-verify.d.ts.map +1 -1
- package/dist/src/mcp-tools/coherence/memory-gate.d.ts.map +1 -1
- package/dist/src/mcp-tools/coherence/quantum-topology.d.ts.map +1 -1
- package/dist/src/mcp-tools/coherence/spectral-analyze.d.ts.map +1 -1
- package/dist/src/mcp-tools/coherence/spectral-analyze.js.map +1 -1
- package/dist/src/mcp-tools/coherence/types.d.ts +31 -31
- package/dist/src/mcp-tools/embeddings-tools.js.map +1 -1
- package/dist/src/mcp-tools/github-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/github-tools.js +30 -8
- package/dist/src/mcp-tools/github-tools.js.map +1 -1
- package/dist/src/mcp-tools/graphify-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/graphify-tools.js +11 -2
- package/dist/src/mcp-tools/graphify-tools.js.map +1 -1
- package/dist/src/mcp-tools/guidance-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/guidance-tools.js.map +1 -1
- package/dist/src/mcp-tools/hive-mind-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/hive-mind-tools.js +56 -35
- package/dist/src/mcp-tools/hive-mind-tools.js.map +1 -1
- package/dist/src/mcp-tools/hooks-embedding.js.map +1 -1
- package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/knowledge-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/knowledge-tools.js +9 -1
- package/dist/src/mcp-tools/knowledge-tools.js.map +1 -1
- package/dist/src/mcp-tools/monograph-compat.d.ts.map +1 -1
- package/dist/src/mcp-tools/monograph-compat.js +56 -14
- package/dist/src/mcp-tools/monograph-compat.js.map +1 -1
- package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/monograph-tools.js +88 -41
- package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
- package/dist/src/mcp-tools/neural-tools.js.map +1 -1
- package/dist/src/mcp-tools/quality/coverage-analysis/prioritize-gaps.d.ts +36 -36
- package/dist/src/mcp-tools/quality/coverage-analysis/prioritize-gaps.d.ts.map +1 -1
- package/dist/src/mcp-tools/quality/security-compliance/detect-secrets.d.ts +4 -4
- package/dist/src/mcp-tools/quality/security-compliance/detect-secrets.d.ts.map +1 -1
- package/dist/src/mcp-tools/security-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/security-tools.js.map +1 -1
- package/dist/src/mcp-tools/swarm-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/swarm-tools.js +6 -4
- package/dist/src/mcp-tools/swarm-tools.js.map +1 -1
- package/dist/src/mcp-tools/system-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/system-tools.js +4 -4
- package/dist/src/mcp-tools/system-tools.js.map +1 -1
- package/dist/src/mcp-tools/transfer-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/types.d.ts +20 -0
- package/dist/src/mcp-tools/types.d.ts.map +1 -1
- package/dist/src/mcp-tools/types.js +36 -1
- package/dist/src/mcp-tools/types.js.map +1 -1
- package/dist/src/memory/ewc-consolidation.d.ts +12 -0
- package/dist/src/memory/ewc-consolidation.d.ts.map +1 -1
- package/dist/src/memory/ewc-consolidation.js.map +1 -1
- package/dist/src/memory/hnsw-operations.d.ts +13 -1
- package/dist/src/memory/hnsw-operations.d.ts.map +1 -1
- package/dist/src/memory/hnsw-operations.js +66 -14
- package/dist/src/memory/hnsw-operations.js.map +1 -1
- package/dist/src/memory/intelligence.d.ts.map +1 -1
- package/dist/src/memory/intelligence.js +99 -3
- package/dist/src/memory/intelligence.js.map +1 -1
- package/dist/src/memory/memory-bridge.d.ts +7 -0
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +20 -0
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/memory/memory-initializer.d.ts.map +1 -1
- package/dist/src/memory/sona-optimizer.d.ts +12 -0
- package/dist/src/memory/sona-optimizer.d.ts.map +1 -1
- package/dist/src/memory/sona-optimizer.js.map +1 -1
- package/dist/src/monovector/coverage-router.js.map +1 -1
- package/dist/src/monovector/coverage-tools.d.ts.map +1 -1
- package/dist/src/monovector/diff-classifier.d.ts.map +1 -1
- package/dist/src/monovector/diff-classifier.js.map +1 -1
- package/dist/src/monovector/init-state.js.map +1 -1
- package/dist/src/observability/replay-reader.d.ts.map +1 -1
- package/dist/src/orgrt/broker.d.ts +26 -0
- package/dist/src/orgrt/broker.d.ts.map +1 -0
- package/dist/src/orgrt/broker.js +73 -0
- package/dist/src/orgrt/broker.js.map +1 -0
- package/dist/src/orgrt/bus.d.ts +5 -1
- package/dist/src/orgrt/bus.d.ts.map +1 -1
- package/dist/src/orgrt/bus.js +5 -1
- package/dist/src/orgrt/bus.js.map +1 -1
- package/dist/src/orgrt/daemon.d.ts +46 -0
- package/dist/src/orgrt/daemon.d.ts.map +1 -1
- package/dist/src/orgrt/daemon.js +201 -18
- package/dist/src/orgrt/daemon.js.map +1 -1
- package/dist/src/orgrt/forwarder.d.ts +10 -5
- package/dist/src/orgrt/forwarder.d.ts.map +1 -1
- package/dist/src/orgrt/forwarder.js +138 -8
- package/dist/src/orgrt/forwarder.js.map +1 -1
- package/dist/src/orgrt/inbox.d.ts +11 -0
- package/dist/src/orgrt/inbox.d.ts.map +1 -0
- package/dist/src/orgrt/inbox.js +56 -0
- package/dist/src/orgrt/inbox.js.map +1 -0
- package/dist/src/orgrt/mailbox.d.ts.map +1 -1
- package/dist/src/orgrt/policy.d.ts +5 -1
- package/dist/src/orgrt/policy.d.ts.map +1 -1
- package/dist/src/orgrt/policy.js +58 -5
- package/dist/src/orgrt/policy.js.map +1 -1
- package/dist/src/orgrt/provider.d.ts.map +1 -1
- package/dist/src/orgrt/provider.js +1 -0
- package/dist/src/orgrt/provider.js.map +1 -1
- package/dist/src/orgrt/scheduler.d.ts +13 -0
- package/dist/src/orgrt/scheduler.d.ts.map +1 -0
- package/dist/src/orgrt/scheduler.js +48 -0
- package/dist/src/orgrt/scheduler.js.map +1 -0
- package/dist/src/orgrt/server.d.ts +9 -0
- package/dist/src/orgrt/server.d.ts.map +1 -0
- package/dist/src/orgrt/server.js +39 -0
- package/dist/src/orgrt/server.js.map +1 -0
- package/dist/src/orgrt/session.d.ts +10 -1
- package/dist/src/orgrt/session.d.ts.map +1 -1
- package/dist/src/orgrt/session.js +24 -1
- package/dist/src/orgrt/session.js.map +1 -1
- package/dist/src/orgrt/test-loop.d.ts +12 -0
- package/dist/src/orgrt/test-loop.d.ts.map +1 -0
- package/dist/src/orgrt/test-loop.js +108 -0
- package/dist/src/orgrt/test-loop.js.map +1 -0
- package/dist/src/orgrt/types.d.ts +52 -52
- package/dist/src/output.d.ts +29 -29
- package/dist/src/output.d.ts.map +1 -1
- package/dist/src/output.js +22 -7
- package/dist/src/output.js.map +1 -1
- package/dist/src/parser.d.ts +32 -0
- package/dist/src/parser.d.ts.map +1 -1
- package/dist/src/parser.js +130 -5
- package/dist/src/parser.js.map +1 -1
- package/dist/src/pricing/model-pricing.d.ts.map +1 -1
- package/dist/src/production/circuit-breaker.d.ts.map +1 -1
- package/dist/src/production/circuit-breaker.js.map +1 -1
- package/dist/src/production/error-handler.d.ts.map +1 -1
- package/dist/src/production/error-handler.js.map +1 -1
- package/dist/src/production/monitoring.d.ts.map +1 -1
- package/dist/src/production/monitoring.js.map +1 -1
- package/dist/src/production/rate-limiter.d.ts.map +1 -1
- package/dist/src/production/rate-limiter.js.map +1 -1
- package/dist/src/production/retry.d.ts.map +1 -1
- package/dist/src/production/retry.js.map +1 -1
- package/dist/src/prompt.d.ts.map +1 -1
- package/dist/src/prompt.js.map +1 -1
- package/dist/src/routing/embed-worker.js +13 -1
- package/dist/src/routing/embed-worker.js.map +1 -1
- package/dist/src/routing/llm-caller.js.map +1 -1
- package/dist/src/routing/route-layer-factory.d.ts.map +1 -1
- package/dist/src/routing/route-layer-factory.js +31 -0
- package/dist/src/routing/route-layer-factory.js.map +1 -1
- package/dist/src/services/config-file-manager.d.ts +21 -0
- package/dist/src/services/config-file-manager.d.ts.map +1 -1
- package/dist/src/services/config-file-manager.js +60 -7
- package/dist/src/services/config-file-manager.js.map +1 -1
- package/dist/src/services/crash-reporter.d.ts.map +1 -1
- package/dist/src/services/crash-reporter.js +2 -1
- package/dist/src/services/crash-reporter.js.map +1 -1
- package/dist/src/suggest.d.ts.map +1 -1
- package/dist/src/suggest.js.map +1 -1
- package/dist/src/transfer/export.js.map +1 -1
- package/dist/src/transfer/ipfs/client.js.map +1 -1
- package/dist/src/transfer/ipfs/upload.js.map +1 -1
- package/dist/src/transfer/models/seraphine.d.ts.map +1 -1
- package/dist/src/transfer/storage/gcs.js.map +1 -1
- package/dist/src/transfer/store/discovery.d.ts.map +1 -1
- package/dist/src/transfer/store/discovery.js.map +1 -1
- package/dist/src/transfer/store/download.d.ts.map +1 -1
- package/dist/src/transfer/store/download.js.map +1 -1
- package/dist/src/transfer/store/index.d.ts.map +1 -1
- package/dist/src/transfer/store/index.js.map +1 -1
- package/dist/src/transfer/store/publish.d.ts.map +1 -1
- package/dist/src/transfer/store/publish.js.map +1 -1
- package/dist/src/transfer/store/search.js.map +1 -1
- package/dist/src/types.d.ts +2 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/ui/dashboard.html +243 -49
- package/dist/src/ui/orgs-files.js +192 -0
- package/dist/src/ui/orgs.html +79 -11
- package/dist/src/ui/server.mjs +346 -139
- package/dist/src/update/checker.js.map +1 -1
- package/dist/src/update/rate-limiter.js.map +1 -1
- package/dist/src/utils/input-guards.d.ts +9 -0
- package/dist/src/utils/input-guards.d.ts.map +1 -1
- package/dist/src/utils/input-guards.js +39 -5
- package/dist/src/utils/input-guards.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +15 -15
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const fs = require('fs');
|
|
16
|
+
const { claimLock, releaseLock } = require('./utils/fs-helpers.cjs');
|
|
16
17
|
|
|
17
18
|
// Resolve base dir at require-time so tests can inject CLAUDE_PROJECT_DIR
|
|
18
19
|
// per fresh require() call.
|
|
@@ -35,6 +36,76 @@ function ensureDataDir() {
|
|
|
35
36
|
try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch (_) {}
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
// ── multi-process-safe store flush (P1-14) ──────────────────────────────────
|
|
40
|
+
// STORE_FILE (auto-memory-store.json) is read once into `_entries` at init()
|
|
41
|
+
// and can be written by more than one code path — consolidate() and
|
|
42
|
+
// bootstrapFromDb() — each of which may be running in a *different* process
|
|
43
|
+
// (e.g. the long-lived MCP server process and a short-lived CJS hook
|
|
44
|
+
// subprocess launched concurrently). Without a lock + re-read-before-write,
|
|
45
|
+
// whichever process flushes last silently erases whatever the other process
|
|
46
|
+
// added since its own load. mergeAndWriteStore() re-reads the current
|
|
47
|
+
// on-disk file immediately before writing, merges the caller's updates in by
|
|
48
|
+
// `id` (keeping whichever record has the newer `ts`), and writes atomically
|
|
49
|
+
// via tmp+rename, guarded by a short advisory lock.
|
|
50
|
+
|
|
51
|
+
const STORE_LOCK_FILE = STORE_FILE + '.lock';
|
|
52
|
+
const STORE_LOCK_STALE_MS = 10 * 1000; // single JSON write — 10s is generous
|
|
53
|
+
|
|
54
|
+
// Uses fs-helpers.cjs's claimLock/releaseLock: unlike an unlink-then-recreate
|
|
55
|
+
// stale-break, it reclaims via an atomic rename, so two processes that both
|
|
56
|
+
// decide the lock is stale can't both end up believing they hold it (a real
|
|
57
|
+
// TOCTOU race a previous unlink-then-create version of this had — best-effort
|
|
58
|
+
// either way, since a missed lock still merges via mergeAndWriteStore's
|
|
59
|
+
// re-read, this just narrows the window further).
|
|
60
|
+
function claimStoreLock() {
|
|
61
|
+
return claimLock(STORE_LOCK_FILE, STORE_LOCK_STALE_MS);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function releaseStoreLock() {
|
|
65
|
+
releaseLock(STORE_LOCK_FILE);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Merge `updates` (new or changed entries) into the on-disk store and write
|
|
70
|
+
* the result atomically. Re-reads STORE_FILE immediately before writing so a
|
|
71
|
+
* concurrent flush from another process is merged, not clobbered. When both
|
|
72
|
+
* sides have an entry with the same `id`, the one with the newer `ts` wins.
|
|
73
|
+
*/
|
|
74
|
+
function mergeAndWriteStore(updates, capTo) {
|
|
75
|
+
var acquired = claimStoreLock();
|
|
76
|
+
if (!acquired) acquired = claimStoreLock(); // one retry — lock is short-lived
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
var diskRaw = safeReadJson(STORE_FILE);
|
|
80
|
+
var diskEntries = Array.isArray(diskRaw) ? diskRaw : [];
|
|
81
|
+
var merged = new Map();
|
|
82
|
+
for (var i = 0; i < diskEntries.length; i++) {
|
|
83
|
+
var d = diskEntries[i];
|
|
84
|
+
if (d && d.id) merged.set(String(d.id), d);
|
|
85
|
+
}
|
|
86
|
+
for (var j = 0; j < (updates || []).length; j++) {
|
|
87
|
+
var u = updates[j];
|
|
88
|
+
if (!u || !u.id) continue;
|
|
89
|
+
var key = String(u.id);
|
|
90
|
+
var existing = merged.get(key);
|
|
91
|
+
if (!existing || (u.ts || 0) >= (existing.ts || 0)) {
|
|
92
|
+
merged.set(key, u);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
var result = Array.from(merged.values());
|
|
96
|
+
if (capTo && result.length > capTo) {
|
|
97
|
+
result.sort(function(a, b) { return (b.ts || 0) - (a.ts || 0); });
|
|
98
|
+
result = result.slice(0, capTo);
|
|
99
|
+
}
|
|
100
|
+
var tmpPath = STORE_FILE + '.' + process.pid + '.tmp';
|
|
101
|
+
fs.writeFileSync(tmpPath, JSON.stringify(result, null, 2), 'utf-8');
|
|
102
|
+
fs.renameSync(tmpPath, STORE_FILE);
|
|
103
|
+
return result;
|
|
104
|
+
} finally {
|
|
105
|
+
if (acquired) releaseStoreLock();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
38
109
|
function safeReadJson(filePath) {
|
|
39
110
|
try {
|
|
40
111
|
if (!fs.existsSync(filePath)) return null;
|
|
@@ -274,7 +345,11 @@ function consolidate() {
|
|
|
274
345
|
}
|
|
275
346
|
|
|
276
347
|
if (newStoreEntries.length > 0) {
|
|
277
|
-
//
|
|
348
|
+
// Decide which of newStoreEntries are worth adding, based on a read of
|
|
349
|
+
// the store as of "now". The actual write below re-reads immediately
|
|
350
|
+
// before flushing (mergeAndWriteStore), so a concurrent process's writes
|
|
351
|
+
// in between are merged rather than clobbered — this initial read is
|
|
352
|
+
// only used for the near-duplicate decision, not as the write's base.
|
|
278
353
|
var existing = safeReadJson(STORE_FILE);
|
|
279
354
|
var store = Array.isArray(existing) ? existing : [];
|
|
280
355
|
var existingIds = new Set(store.map(function(e) { return e && e.id; }));
|
|
@@ -283,25 +358,23 @@ function consolidate() {
|
|
|
283
358
|
var fileSetKey = function(e) { return (e.files || []).slice().sort().join('|'); };
|
|
284
359
|
var seenSummaries = new Set(store.map(function(e) { return e && e.summary; }));
|
|
285
360
|
var seenFileSets = new Set(store.filter(function(e) { return e && Array.isArray(e.files) && e.files.length > 0; }).map(fileSetKey));
|
|
361
|
+
var toAdd = [];
|
|
286
362
|
var addedCount = 0;
|
|
287
363
|
for (var j = 0; j < newStoreEntries.length; j++) {
|
|
288
364
|
var ne = newStoreEntries[j];
|
|
289
365
|
if (existingIds.has(ne.id)) continue;
|
|
290
366
|
if (seenSummaries.has(ne.summary)) continue;
|
|
291
367
|
if (ne.files && ne.files.length > 0 && seenFileSets.has(fileSetKey(ne))) continue;
|
|
292
|
-
|
|
368
|
+
toAdd.push(ne);
|
|
293
369
|
seenSummaries.add(ne.summary);
|
|
294
370
|
if (ne.files && ne.files.length > 0) seenFileSets.add(fileSetKey(ne));
|
|
295
371
|
addedCount++;
|
|
296
372
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
373
|
+
if (toAdd.length > 0) {
|
|
374
|
+
try {
|
|
375
|
+
mergeAndWriteStore(toAdd, 200);
|
|
376
|
+
} catch (_) {}
|
|
301
377
|
}
|
|
302
|
-
try {
|
|
303
|
-
fs.writeFileSync(STORE_FILE, JSON.stringify(store, null, 2), 'utf-8');
|
|
304
|
-
} catch (_) {}
|
|
305
378
|
count += addedCount;
|
|
306
379
|
}
|
|
307
380
|
|
|
@@ -395,19 +468,20 @@ function bootstrapFromDb(db) {
|
|
|
395
468
|
if (!db || _entries.length >= 5) return 0;
|
|
396
469
|
try {
|
|
397
470
|
var hubs = db.prepare(
|
|
398
|
-
"SELECT n.name, n.label, n.file, COUNT(e.id) AS deg " +
|
|
399
|
-
"FROM nodes n JOIN edges e ON (e.
|
|
400
|
-
"WHERE n.label IN ('File','Function','Class') AND n.
|
|
471
|
+
"SELECT n.name, n.label, n.file_path AS file, COUNT(e.id) AS deg " +
|
|
472
|
+
"FROM nodes n JOIN edges e ON (e.source_id = n.id OR e.target_id = n.id) " +
|
|
473
|
+
"WHERE n.label IN ('File','Function','Class') AND n.file_path NOT LIKE '%node_modules%' AND n.file_path NOT LIKE '%dist/%' " +
|
|
401
474
|
"GROUP BY n.id ORDER BY deg DESC LIMIT 10"
|
|
402
475
|
).all();
|
|
403
476
|
if (hubs.length === 0) return 0;
|
|
404
477
|
var existingIds = new Set(_entries.map(function(e) { return e.id; }));
|
|
478
|
+
var newHubEntries = [];
|
|
405
479
|
var added = 0;
|
|
406
480
|
for (var hi = 0; hi < hubs.length; hi++) {
|
|
407
481
|
var h = hubs[hi];
|
|
408
482
|
var bId = 'bootstrap-hub-' + hi;
|
|
409
483
|
if (existingIds.has(bId)) continue;
|
|
410
|
-
|
|
484
|
+
var hubEntry = {
|
|
411
485
|
id: bId,
|
|
412
486
|
type: 'hub',
|
|
413
487
|
content: h.name + ' (' + h.label + ') — ' + (h.file || '').replace(CWD + '/', '') + ' (' + h.deg + ' connections)',
|
|
@@ -415,12 +489,17 @@ function bootstrapFromDb(db) {
|
|
|
415
489
|
confidence: 0.4,
|
|
416
490
|
files: h.file ? [h.file] : [],
|
|
417
491
|
ts: Date.now(),
|
|
418
|
-
}
|
|
492
|
+
};
|
|
493
|
+
_entries.push(hubEntry);
|
|
494
|
+
newHubEntries.push(hubEntry);
|
|
419
495
|
added++;
|
|
420
496
|
}
|
|
421
497
|
if (added > 0) {
|
|
422
498
|
ensureDataDir();
|
|
423
|
-
|
|
499
|
+
// mergeAndWriteStore re-reads STORE_FILE immediately before writing, so
|
|
500
|
+
// this only ever adds these hub entries into whatever is currently on
|
|
501
|
+
// disk — it never overwrites entries a concurrent process added.
|
|
502
|
+
try { mergeAndWriteStore(newHubEntries, 200); } catch (_) {}
|
|
424
503
|
}
|
|
425
504
|
return added;
|
|
426
505
|
} catch (_) { return 0; }
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const fs = require('fs');
|
|
12
|
+
const { atomicWriteFileSync } = require('./utils/fs-helpers.cjs');
|
|
12
13
|
|
|
13
14
|
const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
14
15
|
|
|
@@ -54,7 +55,11 @@ function readCurrent() {
|
|
|
54
55
|
function writeCurrent(data) {
|
|
55
56
|
ensureDir();
|
|
56
57
|
try {
|
|
57
|
-
|
|
58
|
+
// P2-21: tmp+rename — current.json is read+written by metric()/update()
|
|
59
|
+
// from multiple concurrent hook processes (pre-task, post-edit, etc. can
|
|
60
|
+
// all fire in one batched tool-call message); a plain writeFileSync lets
|
|
61
|
+
// a concurrent reader observe a partially-written file.
|
|
62
|
+
atomicWriteFileSync(CURRENT_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
58
63
|
} catch (_) {}
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -18,6 +18,7 @@ const fs = require('fs');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { execSync, spawnSync } = require('child_process');
|
|
20
20
|
const os = require('os');
|
|
21
|
+
const { cleanEntries } = require('./utils/fs-helpers.cjs');
|
|
21
22
|
|
|
22
23
|
// Configuration
|
|
23
24
|
const CONFIG = {
|
|
@@ -197,8 +198,7 @@ function getModelFromSessionJSONL() {
|
|
|
197
198
|
if (!fs.existsSync(projectsDir)) return null;
|
|
198
199
|
|
|
199
200
|
// Most recently modified JSONL = current (or latest) session
|
|
200
|
-
const files =
|
|
201
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
201
|
+
const files = cleanEntries(projectsDir, f => f.endsWith('.jsonl'))
|
|
202
202
|
.map(f => ({ f, mt: (() => { try { return fs.statSync(path.join(projectsDir, f)).mtimeMs; } catch { return 0; } })() }))
|
|
203
203
|
.sort((a, b) => b.mt - a.mt);
|
|
204
204
|
if (files.length === 0) return null;
|
|
@@ -303,7 +303,7 @@ function getLearningStats() {
|
|
|
303
303
|
try {
|
|
304
304
|
const sessDir = path.join(CWD, '.claude', 'sessions');
|
|
305
305
|
if (fs.existsSync(sessDir)) {
|
|
306
|
-
sessions =
|
|
306
|
+
sessions = cleanEntries(sessDir, f => f.endsWith('.json')).length;
|
|
307
307
|
}
|
|
308
308
|
} catch { /* ignore */ }
|
|
309
309
|
|
|
@@ -357,7 +357,7 @@ function getSecurityStatus() {
|
|
|
357
357
|
try {
|
|
358
358
|
const scanDir = path.join(CWD, '.claude', 'security-scans');
|
|
359
359
|
if (fs.existsSync(scanDir)) {
|
|
360
|
-
scanCount =
|
|
360
|
+
scanCount = cleanEntries(scanDir, f => f.endsWith('.json')).length;
|
|
361
361
|
}
|
|
362
362
|
} catch { /* ignore */ }
|
|
363
363
|
|
|
@@ -379,7 +379,7 @@ function getSwarmStatus() {
|
|
|
379
379
|
const regDir = path.join(CWD, '.monomind', 'agents', 'registrations');
|
|
380
380
|
if (fs.existsSync(regDir)) {
|
|
381
381
|
try {
|
|
382
|
-
const files =
|
|
382
|
+
const files = cleanEntries(regDir, f => f.endsWith('.json'));
|
|
383
383
|
const liveCount = files.filter(f => {
|
|
384
384
|
try {
|
|
385
385
|
return (now - fs.statSync(path.join(regDir, f)).mtimeMs) < agentRegTtlMs;
|
|
@@ -488,7 +488,7 @@ function getADRStatus() {
|
|
|
488
488
|
for (const adrPath of adrPaths) {
|
|
489
489
|
try {
|
|
490
490
|
if (fs.existsSync(adrPath)) {
|
|
491
|
-
const files =
|
|
491
|
+
const files = cleanEntries(adrPath, f =>
|
|
492
492
|
f.endsWith('.md') && (f.startsWith('ADR-') || f.startsWith('adr-') || /^\d{4}-/.test(f))
|
|
493
493
|
);
|
|
494
494
|
// Report actual count — don't guess compliance without reading files
|
|
@@ -523,7 +523,7 @@ function getHooksStatus() {
|
|
|
523
523
|
try {
|
|
524
524
|
const hooksDir = path.join(CWD, '.claude', 'hooks');
|
|
525
525
|
if (fs.existsSync(hooksDir)) {
|
|
526
|
-
const hookFiles =
|
|
526
|
+
const hookFiles = cleanEntries(hooksDir, f => f.endsWith('.js') || f.endsWith('.sh')).length;
|
|
527
527
|
total = Math.max(total, hookFiles);
|
|
528
528
|
enabled = Math.max(enabled, hookFiles);
|
|
529
529
|
}
|
|
@@ -870,7 +870,7 @@ function getLoopStatus() {
|
|
|
870
870
|
const now = Date.now();
|
|
871
871
|
const loops = [];
|
|
872
872
|
try {
|
|
873
|
-
const files =
|
|
873
|
+
const files = cleanEntries(loopsDir, f =>
|
|
874
874
|
f.endsWith('.json') && !f.includes('-hil') && !f.endsWith('.stop'));
|
|
875
875
|
for (const f of files) {
|
|
876
876
|
const d = readJSON(path.join(loopsDir, f));
|
|
@@ -895,7 +895,7 @@ function getHILPending() {
|
|
|
895
895
|
if (!fs.existsSync(loopsDir)) return { pending: 0 };
|
|
896
896
|
let pending = 0;
|
|
897
897
|
try {
|
|
898
|
-
const files =
|
|
898
|
+
const files = cleanEntries(loopsDir, f => f.endsWith('-hil.md'));
|
|
899
899
|
for (const f of files) {
|
|
900
900
|
try {
|
|
901
901
|
const txt = fs.readFileSync(path.join(loopsDir, f), 'utf-8');
|
|
@@ -932,7 +932,7 @@ function getActiveOrgs() {
|
|
|
932
932
|
const runsDir = path.join(orgsDir, orgName, 'runs');
|
|
933
933
|
if (!fs.existsSync(runsDir)) continue;
|
|
934
934
|
try {
|
|
935
|
-
const files =
|
|
935
|
+
const files = cleanEntries(runsDir, f => f.endsWith('.jsonl'));
|
|
936
936
|
if (!files.length) continue;
|
|
937
937
|
files.sort();
|
|
938
938
|
const latest = files[files.length - 1];
|
|
@@ -351,7 +351,7 @@ function parseSessionFile(filePath, project, seenMsgIds, dateStart, dateEnd) {
|
|
|
351
351
|
function collectJsonlFiles(dirPath) {
|
|
352
352
|
var files = [];
|
|
353
353
|
var entries;
|
|
354
|
-
try { entries = fs.readdirSync(dirPath); } catch (e) { return files; }
|
|
354
|
+
try { entries = fs.readdirSync(dirPath).filter(function(f) { return !f.startsWith('._'); }); } catch (e) { return files; }
|
|
355
355
|
for (var i = 0; i < entries.length; i++) {
|
|
356
356
|
var e = entries[i];
|
|
357
357
|
if (e.endsWith('.jsonl')) {
|
|
@@ -360,7 +360,7 @@ function collectJsonlFiles(dirPath) {
|
|
|
360
360
|
// Check subagents
|
|
361
361
|
var subDir = path.join(dirPath, e, 'subagents');
|
|
362
362
|
var subFiles;
|
|
363
|
-
try { subFiles = fs.readdirSync(subDir); } catch (_) { continue; }
|
|
363
|
+
try { subFiles = fs.readdirSync(subDir).filter(function(f) { return !f.startsWith('._'); }); } catch (_) { continue; }
|
|
364
364
|
for (var j = 0; j < subFiles.length; j++) {
|
|
365
365
|
if (subFiles[j].endsWith('.jsonl')) {
|
|
366
366
|
files.push(path.join(subDir, subFiles[j]));
|
|
@@ -389,7 +389,7 @@ function unsanitize(name) {
|
|
|
389
389
|
function parseAllSessions(dateStart, dateEnd) {
|
|
390
390
|
var projectsDir = getClaudeProjectsDir();
|
|
391
391
|
var projectDirs;
|
|
392
|
-
try { projectDirs = fs.readdirSync(projectsDir); } catch (e) { return []; }
|
|
392
|
+
try { projectDirs = fs.readdirSync(projectsDir).filter(function(f) { return !f.startsWith('._'); }); } catch (e) { return []; }
|
|
393
393
|
|
|
394
394
|
var seenMsgIds = new Set();
|
|
395
395
|
var projectMap = {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Shared agent-registration purge logic.
|
|
3
|
+
//
|
|
4
|
+
// .monomind/agents/registrations/*.json accumulate one file per subagent
|
|
5
|
+
// spawn. The only purge used to be the FIFO-oldest-removal + stale sweep in
|
|
6
|
+
// task-handler.cjs's handlePostTask, which only fires on TeammateIdle/
|
|
7
|
+
// TaskCompleted — so any session that spawns agents but never emits those
|
|
8
|
+
// events (crashes, single-shot Task calls outside a team, etc.) leaked
|
|
9
|
+
// registrations forever. agent-start-handler.cjs already reads this
|
|
10
|
+
// directory on every subagent start, so it also runs this same stale sweep.
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const { cleanEntries } = require('./fs-helpers.cjs');
|
|
15
|
+
|
|
16
|
+
const DEFAULT_MAX_AGE_MS = 30 * 60 * 1000; // 30 minutes
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Delete registration files older than maxAgeMs. Returns the number of
|
|
20
|
+
* registrations remaining after the purge (or null if regDir doesn't exist
|
|
21
|
+
* or an error occurred).
|
|
22
|
+
*/
|
|
23
|
+
function purgeStaleRegistrations(regDir, maxAgeMs) {
|
|
24
|
+
maxAgeMs = maxAgeMs || DEFAULT_MAX_AGE_MS;
|
|
25
|
+
try {
|
|
26
|
+
if (!fs.existsSync(regDir)) return null;
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
for (const f of cleanEntries(regDir, f => f.endsWith('.json'))) {
|
|
29
|
+
try {
|
|
30
|
+
if (now - fs.statSync(path.join(regDir, f)).mtimeMs > maxAgeMs) {
|
|
31
|
+
fs.unlinkSync(path.join(regDir, f));
|
|
32
|
+
}
|
|
33
|
+
} catch { /* ignore */ }
|
|
34
|
+
}
|
|
35
|
+
return cleanEntries(regDir, f => f.endsWith('.json')).length;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { purgeStaleRegistrations, DEFAULT_MAX_AGE_MS };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Shared fs helpers for hook handlers.
|
|
3
|
+
//
|
|
4
|
+
// exFAT / macOS AppleDouble junk files (`._foo.json`, `.__protocol.md`, etc.)
|
|
5
|
+
// get created whenever files touch an exFAT volume (external drives, some
|
|
6
|
+
// network shares) and then get picked up by naive `readdirSync().filter(...)`
|
|
7
|
+
// calls as if they were real data — corrupting counts, getting parsed as
|
|
8
|
+
// JSON and failing, or (worst case) surfacing as garbage entries in
|
|
9
|
+
// Claude Code's own skill/command list. Every readdir in the hook handlers
|
|
10
|
+
// should route through `cleanEntries()` so this is filtered exactly once.
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read a directory and filter out exFAT AppleDouble junk (`._*`) before
|
|
17
|
+
* applying the caller's own filter predicate.
|
|
18
|
+
* @param {string} dir - directory to read
|
|
19
|
+
* @param {(name: string) => boolean} [filterFn] - optional additional filter
|
|
20
|
+
* @returns {string[]} entry names (not full paths), junk-free
|
|
21
|
+
*/
|
|
22
|
+
function cleanEntries(dir, filterFn) {
|
|
23
|
+
let entries;
|
|
24
|
+
try {
|
|
25
|
+
entries = fs.readdirSync(dir);
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
entries = entries.filter(f => !f.startsWith('._'));
|
|
30
|
+
return filterFn ? entries.filter(filterFn) : entries;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Same as cleanEntries but returns full paths.
|
|
35
|
+
*/
|
|
36
|
+
function cleanEntryPaths(dir, filterFn) {
|
|
37
|
+
return cleanEntries(dir, filterFn).map(f => path.join(dir, f));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Synchronous sleep (blocks the event loop) — used for tiny lock-retry
|
|
42
|
+
* backoffs where an async setTimeout would require restructuring a
|
|
43
|
+
* short-lived hook script into a promise chain for no real benefit.
|
|
44
|
+
* Falls back to a busy-wait if Atomics.wait is unavailable.
|
|
45
|
+
*/
|
|
46
|
+
function sleepSync(ms) {
|
|
47
|
+
try {
|
|
48
|
+
const sab = new SharedArrayBuffer(4);
|
|
49
|
+
Atomics.wait(new Int32Array(sab), 0, 0, ms);
|
|
50
|
+
} catch {
|
|
51
|
+
const end = Date.now() + ms;
|
|
52
|
+
while (Date.now() < end) { /* spin */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Write a file atomically: write to a unique temp path then rename into
|
|
58
|
+
* place. `fs.renameSync` is atomic on POSIX and Windows (same volume), so
|
|
59
|
+
* concurrent readers of `filePath` never observe a partially-written file
|
|
60
|
+
* (torn read) — they see either the old complete file or the new complete
|
|
61
|
+
* file, never a half-written one. Does not by itself prevent lost updates
|
|
62
|
+
* from concurrent read-modify-write cycles — pair with `claimLock` when
|
|
63
|
+
* that matters (see hook-latency.json / swarm-activity.json writers).
|
|
64
|
+
*/
|
|
65
|
+
function atomicWriteFileSync(filePath, data, encoding) {
|
|
66
|
+
const dir = path.dirname(filePath);
|
|
67
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
|
|
68
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
69
|
+
fs.writeFileSync(tmp, data, encoding);
|
|
70
|
+
fs.renameSync(tmp, filePath);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Claim an exclusive lock file for short-lived critical sections (e.g. a
|
|
75
|
+
* rebuild-cooldown or spawn-once guard). Uses `wx` (exclusive create) so
|
|
76
|
+
* the claim itself is atomic — no read-check-write window.
|
|
77
|
+
*
|
|
78
|
+
* On contention, a lock older than `staleMs` is treated as abandoned (the
|
|
79
|
+
* holder crashed or never released it) and broken via an atomic RENAME
|
|
80
|
+
* (never unlink-then-create): renaming the stale lock path away is only
|
|
81
|
+
* ever won by exactly one racing process, so a concurrent process that
|
|
82
|
+
* also decided the lock was stale cannot delete *this* process's
|
|
83
|
+
* freshly-reclaimed lock out from under it (the TOCTOU race an
|
|
84
|
+
* unlink-then-create approach is vulnerable to). The loser of the rename
|
|
85
|
+
* race backs off and retries the whole claim sequence from the top rather
|
|
86
|
+
* than assuming it owns anything.
|
|
87
|
+
*
|
|
88
|
+
* Returns true if the lock was claimed (caller must eventually call
|
|
89
|
+
* `releaseLock`), false if another live/fresh owner holds it.
|
|
90
|
+
*/
|
|
91
|
+
function claimLock(lockPath, staleMs, maxAttempts) {
|
|
92
|
+
staleMs = staleMs || 30000;
|
|
93
|
+
maxAttempts = maxAttempts || 3;
|
|
94
|
+
try { fs.mkdirSync(path.dirname(lockPath), { recursive: true }); } catch { /* ignore */ }
|
|
95
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
96
|
+
try {
|
|
97
|
+
fs.writeFileSync(lockPath, String(process.pid), { flag: 'wx' });
|
|
98
|
+
return true;
|
|
99
|
+
} catch {
|
|
100
|
+
let stat;
|
|
101
|
+
try {
|
|
102
|
+
stat = fs.statSync(lockPath);
|
|
103
|
+
} catch {
|
|
104
|
+
// Lock vanished between our failed wx-create and this stat check —
|
|
105
|
+
// retry the create immediately.
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (Date.now() - stat.mtimeMs < staleMs) return false; // held by a live/fresh owner
|
|
109
|
+
|
|
110
|
+
const claimedName = `${lockPath}.${process.pid}.${Date.now()}.stale`;
|
|
111
|
+
try {
|
|
112
|
+
fs.renameSync(lockPath, claimedName);
|
|
113
|
+
} catch {
|
|
114
|
+
// Someone else already renamed the stale lock away — they're
|
|
115
|
+
// claiming it. Back off briefly and retry from the top rather than
|
|
116
|
+
// proceeding as if we own it.
|
|
117
|
+
sleepSync(5);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
try { fs.unlinkSync(claimedName); } catch { /* ignore */ }
|
|
121
|
+
// We won the rename race — loop back and attempt the wx-create fresh.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Release a lock previously claimed by this process via `claimLock`. */
|
|
128
|
+
function releaseLock(lockPath) {
|
|
129
|
+
try {
|
|
130
|
+
if (Number(fs.readFileSync(lockPath, 'utf-8')) === process.pid) fs.unlinkSync(lockPath);
|
|
131
|
+
} catch { /* ignore */ }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Run `fn` while holding a lock (see `claimLock`) so a read-modify-write
|
|
136
|
+
* cycle is protected end-to-end, not just the final write. If the lock
|
|
137
|
+
* can't be claimed after retries, `fn` still runs (never block a hook) but
|
|
138
|
+
* without lost-update protection — best-effort, matching this codebase's
|
|
139
|
+
* "hooks must not block the session" rule.
|
|
140
|
+
*/
|
|
141
|
+
function withLock(lockPath, fn, staleMs, maxAttempts) {
|
|
142
|
+
const acquired = claimLock(lockPath, staleMs, maxAttempts);
|
|
143
|
+
try {
|
|
144
|
+
return fn();
|
|
145
|
+
} finally {
|
|
146
|
+
if (acquired) releaseLock(lockPath);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Append a JSONL line and rotate the file down to `maxLines` (keep the
|
|
152
|
+
* most recent lines) when it exceeds that count — mirrors the size-cap
|
|
153
|
+
* pattern already used for intelligence-outcomes.jsonl (500 lines) and
|
|
154
|
+
* episodes.jsonl. Prevents unbounded JSONL growth. Rotation write is
|
|
155
|
+
* atomic (tmp+rename).
|
|
156
|
+
*/
|
|
157
|
+
function appendJsonlWithRotation(filePath, line, maxLines) {
|
|
158
|
+
maxLines = maxLines || 500;
|
|
159
|
+
try { fs.mkdirSync(path.dirname(filePath), { recursive: true }); } catch { /* ignore */ }
|
|
160
|
+
fs.appendFileSync(filePath, line.endsWith('\n') ? line : line + '\n');
|
|
161
|
+
try {
|
|
162
|
+
const st = fs.statSync(filePath);
|
|
163
|
+
// Cheap heuristic to skip reading huge files line-by-line on every
|
|
164
|
+
// append: only bother counting lines once the file is already
|
|
165
|
+
// suspiciously large relative to a plausible maxLines * avg-line-size.
|
|
166
|
+
if (st.size < 4096) return;
|
|
167
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
|
|
168
|
+
if (lines.length > maxLines) {
|
|
169
|
+
atomicWriteFileSync(filePath, lines.slice(-maxLines).join('\n') + '\n', 'utf-8');
|
|
170
|
+
}
|
|
171
|
+
} catch { /* non-fatal — rotation is best-effort */ }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
cleanEntries,
|
|
176
|
+
cleanEntryPaths,
|
|
177
|
+
sleepSync,
|
|
178
|
+
atomicWriteFileSync,
|
|
179
|
+
claimLock,
|
|
180
|
+
releaseLock,
|
|
181
|
+
withLock,
|
|
182
|
+
appendJsonlWithRotation,
|
|
183
|
+
};
|
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
|
|
7
7
|
const { _openMonographDb } = require('./monograph.cjs');
|
|
8
|
+
const { cleanEntries, atomicWriteFileSync } = require('./fs-helpers.cjs');
|
|
8
9
|
|
|
9
10
|
const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
10
11
|
|
|
@@ -82,7 +83,7 @@ function _triggerCollectMdFiles(dir, _depth) {
|
|
|
82
83
|
if (depth > 5) return []; // cap recursion depth to prevent stack overflow DoS
|
|
83
84
|
var results = [];
|
|
84
85
|
try {
|
|
85
|
-
var entries =
|
|
86
|
+
var entries = cleanEntries(dir);
|
|
86
87
|
for (var i = 0; i < entries.length; i++) {
|
|
87
88
|
if (results.length >= 200) break; // cap total file count to prevent DoS
|
|
88
89
|
var full = path.join(dir, entries[i]);
|
|
@@ -135,8 +136,11 @@ function scanMicroAgentTriggers(prompt) {
|
|
|
135
136
|
if (!cacheLoaded) {
|
|
136
137
|
patterns = _triggerBuildIndex(agentDir);
|
|
137
138
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
// P2-21: tmp+rename — multiple concurrent hook processes (route-handler
|
|
140
|
+
// invocations fire per prompt/subagent) can all miss the cache at once
|
|
141
|
+
// and rebuild+write trigger-index.json simultaneously; atomic write
|
|
142
|
+
// prevents a concurrent reader from parsing a half-written file.
|
|
143
|
+
atomicWriteFileSync(indexPath, JSON.stringify({ patterns: patterns, builtAt: new Date().toISOString(), totalAgentsScanned: patterns.length }));
|
|
140
144
|
} catch (e) {}
|
|
141
145
|
}
|
|
142
146
|
|
|
@@ -334,9 +338,9 @@ function _autoIndexKnowledge(knowledgeDir) {
|
|
|
334
338
|
} catch (e) { /* graph not available yet */ }
|
|
335
339
|
|
|
336
340
|
try {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
341
|
+
// Same torn-read hazard as trigger-index.json above — atomic write.
|
|
342
|
+
atomicWriteFileSync(chunksFile, newLines.length > 0 ? newLines.join('\n') + '\n' : '', 'utf-8');
|
|
343
|
+
atomicWriteFileSync(hashFile, contentHash, 'utf-8');
|
|
340
344
|
} catch (e) {}
|
|
341
345
|
return newLines.length;
|
|
342
346
|
}
|