@monoes/monomindcli 2.8.4 → 2.9.1
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/auto-memory-hook.mjs +0 -0
- package/.claude/helpers/handlers/route-handler.cjs +12 -0
- package/.claude/helpers/handlers/session-restore-handler.cjs +28 -7
- package/.claude/helpers/hook-handler.cjs +0 -0
- package/.claude/helpers/utils/micro-agents.cjs +22 -0
- package/.claude/skills/monodesign/scripts/live/sveltekit-adapter.mjs +11 -6
- package/.claude/skills/monodesign/scripts/live-inject.mjs +20 -7
- package/.claude/skills/monodesign/scripts/live-server.mjs +38 -2
- package/LICENSE +191 -0
- package/README.md +8 -6
- package/dist/src/browser/dashboard/server.d.ts +5 -0
- package/dist/src/browser/dashboard/server.d.ts.map +1 -1
- package/dist/src/browser/dashboard/server.js +59 -4
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/browser/dashboard/ui.html +18 -8
- package/dist/src/browser/workflow/store.d.ts.map +1 -1
- package/dist/src/browser/workflow/store.js +3 -2
- package/dist/src/browser/workflow/store.js.map +1 -1
- package/dist/src/capabilities/cap-documents.d.ts.map +1 -1
- package/dist/src/capabilities/cap-documents.js +14 -9
- package/dist/src/capabilities/cap-documents.js.map +1 -1
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +22 -18
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/download-embeddings.d.ts +8 -0
- package/dist/src/commands/download-embeddings.d.ts.map +1 -0
- package/dist/src/commands/download-embeddings.js +34 -0
- package/dist/src/commands/download-embeddings.js.map +1 -0
- package/dist/src/commands/hooks.d.ts.map +1 -1
- package/dist/src/commands/hooks.js +14 -0
- package/dist/src/commands/hooks.js.map +1 -1
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +4 -0
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/init-wizard.js +1 -1
- package/dist/src/commands/init-wizard.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +48 -1
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/monograph.d.ts.map +1 -1
- package/dist/src/commands/monograph.js +35 -1
- package/dist/src/commands/monograph.js.map +1 -1
- package/dist/src/commands/org-observe.d.ts +10 -0
- package/dist/src/commands/org-observe.d.ts.map +1 -1
- package/dist/src/commands/org-observe.js +195 -31
- package/dist/src/commands/org-observe.js.map +1 -1
- package/dist/src/commands/org.d.ts +1 -0
- package/dist/src/commands/org.d.ts.map +1 -1
- package/dist/src/commands/org.js +139 -3
- package/dist/src/commands/org.js.map +1 -1
- package/dist/src/commands/route.d.ts +1 -9
- package/dist/src/commands/route.d.ts.map +1 -1
- package/dist/src/commands/route.js +42 -65
- package/dist/src/commands/route.js.map +1 -1
- package/dist/src/commands/security-cve.d.ts.map +1 -1
- package/dist/src/commands/security-cve.js +2 -1
- package/dist/src/commands/security-cve.js.map +1 -1
- package/dist/src/index.d.ts +0 -10
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -15
- package/dist/src/index.js.map +1 -1
- package/dist/src/init/copy-assets.d.ts +17 -0
- package/dist/src/init/copy-assets.d.ts.map +1 -0
- package/dist/src/init/copy-assets.js +271 -0
- package/dist/src/init/copy-assets.js.map +1 -0
- package/dist/src/init/executor.d.ts +2 -30
- package/dist/src/init/executor.d.ts.map +1 -1
- package/dist/src/init/executor.js +56 -2493
- package/dist/src/init/executor.js.map +1 -1
- package/dist/src/init/helpers-generator.d.ts.map +1 -1
- package/dist/src/init/helpers-generator.js +9 -0
- package/dist/src/init/helpers-generator.js.map +1 -1
- package/dist/src/init/settings-generator.d.ts.map +1 -1
- package/dist/src/init/settings-generator.js +0 -4
- package/dist/src/init/settings-generator.js.map +1 -1
- package/dist/src/init/shared.d.ts +112 -0
- package/dist/src/init/shared.d.ts.map +1 -0
- package/dist/src/init/shared.js +475 -0
- package/dist/src/init/shared.js.map +1 -0
- package/dist/src/init/statusline-generator.d.ts.map +1 -1
- package/dist/src/init/statusline-generator.js +37 -0
- package/dist/src/init/statusline-generator.js.map +1 -1
- package/dist/src/init/types.d.ts +1 -1
- package/dist/src/init/types.d.ts.map +1 -1
- package/dist/src/init/upgrade.d.ts +34 -0
- package/dist/src/init/upgrade.d.ts.map +1 -0
- package/dist/src/init/upgrade.js +477 -0
- package/dist/src/init/upgrade.js.map +1 -0
- package/dist/src/init/write-antigravity.d.ts +13 -0
- package/dist/src/init/write-antigravity.d.ts.map +1 -0
- package/dist/src/init/write-antigravity.js +117 -0
- package/dist/src/init/write-antigravity.js.map +1 -0
- package/dist/src/init/write-capabilities.d.ts +9 -0
- package/dist/src/init/write-capabilities.d.ts.map +1 -0
- package/dist/src/init/write-capabilities.js +392 -0
- package/dist/src/init/write-capabilities.js.map +1 -0
- package/dist/src/init/write-claude.d.ts +25 -0
- package/dist/src/init/write-claude.d.ts.map +1 -0
- package/dist/src/init/write-claude.js +279 -0
- package/dist/src/init/write-claude.js.map +1 -0
- package/dist/src/init/write-kimicode.d.ts +19 -0
- package/dist/src/init/write-kimicode.d.ts.map +1 -0
- package/dist/src/init/write-kimicode.js +223 -0
- package/dist/src/init/write-kimicode.js.map +1 -0
- package/dist/src/init/write-opencode.d.ts +14 -0
- package/dist/src/init/write-opencode.d.ts.map +1 -0
- package/dist/src/init/write-opencode.js +130 -0
- package/dist/src/init/write-opencode.js.map +1 -0
- package/dist/src/init/write-runtime-config.d.ts +14 -0
- package/dist/src/init/write-runtime-config.d.ts.map +1 -0
- package/dist/src/init/write-runtime-config.js +214 -0
- package/dist/src/init/write-runtime-config.js.map +1 -0
- package/dist/src/init/write-sample-org.d.ts +9 -0
- package/dist/src/init/write-sample-org.d.ts.map +1 -0
- package/dist/src/init/write-sample-org.js +39 -0
- package/dist/src/init/write-sample-org.js.map +1 -0
- package/dist/src/knowledge/eval/golden-set.js +1 -1
- package/dist/src/knowledge/eval/golden-set.js.map +1 -1
- package/dist/src/mcp-tools/analyze-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/analyze-tools.js +2 -25
- package/dist/src/mcp-tools/analyze-tools.js.map +1 -1
- package/dist/src/mcp-tools/auto-install.d.ts.map +1 -1
- package/dist/src/mcp-tools/auto-install.js +2 -1
- package/dist/src/mcp-tools/auto-install.js.map +1 -1
- package/dist/src/mcp-tools/hooks-routing.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-routing.js +26 -0
- package/dist/src/mcp-tools/hooks-routing.js.map +1 -1
- package/dist/src/mcp-tools/memory-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/memory-tools.js +2 -37
- package/dist/src/mcp-tools/memory-tools.js.map +1 -1
- package/dist/src/mcp-tools/monograph/agent-history-tools.d.ts +5 -0
- package/dist/src/mcp-tools/monograph/agent-history-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/monograph/agent-history-tools.js +177 -0
- package/dist/src/mcp-tools/monograph/agent-history-tools.js.map +1 -0
- package/dist/src/mcp-tools/monograph/build-tools.d.ts +9 -0
- package/dist/src/mcp-tools/monograph/build-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/monograph/build-tools.js +299 -0
- package/dist/src/mcp-tools/monograph/build-tools.js.map +1 -0
- package/dist/src/mcp-tools/monograph/group-tools.d.ts +10 -0
- package/dist/src/mcp-tools/monograph/group-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/monograph/group-tools.js +225 -0
- package/dist/src/mcp-tools/monograph/group-tools.js.map +1 -0
- package/dist/src/mcp-tools/monograph/health-tools.d.ts +6 -0
- package/dist/src/mcp-tools/monograph/health-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/monograph/health-tools.js +113 -0
- package/dist/src/mcp-tools/monograph/health-tools.js.map +1 -0
- package/dist/src/mcp-tools/monograph/impact-tools.d.ts +9 -0
- package/dist/src/mcp-tools/monograph/impact-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/monograph/impact-tools.js +532 -0
- package/dist/src/mcp-tools/monograph/impact-tools.js.map +1 -0
- package/dist/src/mcp-tools/monograph/index.d.ts +10 -0
- package/dist/src/mcp-tools/monograph/index.d.ts.map +1 -0
- package/dist/src/mcp-tools/monograph/index.js +73 -0
- package/dist/src/mcp-tools/monograph/index.js.map +1 -0
- package/dist/src/mcp-tools/monograph/query-tools.d.ts +11 -0
- package/dist/src/mcp-tools/monograph/query-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/monograph/query-tools.js +500 -0
- package/dist/src/mcp-tools/monograph/query-tools.js.map +1 -0
- package/dist/src/mcp-tools/monograph/shared.d.ts +64 -0
- package/dist/src/mcp-tools/monograph/shared.d.ts.map +1 -0
- package/dist/src/mcp-tools/monograph/shared.js +215 -0
- package/dist/src/mcp-tools/monograph/shared.js.map +1 -0
- package/dist/src/mcp-tools/monograph/visualize-tools.d.ts +10 -0
- package/dist/src/mcp-tools/monograph/visualize-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/monograph/visualize-tools.js +382 -0
- package/dist/src/mcp-tools/monograph/visualize-tools.js.map +1 -0
- package/dist/src/mcp-tools/monograph-tools.d.ts +4 -23
- package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/monograph-tools.js +4 -2439
- package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
- package/dist/src/mcp-tools/monomind-tools.d.ts +8 -0
- package/dist/src/mcp-tools/monomind-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/monomind-tools.js +13 -1
- package/dist/src/mcp-tools/monomind-tools.js.map +1 -1
- package/dist/src/mcp-tools/quality/security-compliance/detect-secrets.d.ts.map +1 -1
- package/dist/src/mcp-tools/quality/security-compliance/detect-secrets.js +31 -2
- package/dist/src/mcp-tools/quality/security-compliance/detect-secrets.js.map +1 -1
- package/dist/src/mcp-tools/terminal-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/terminal-tools.js +31 -1
- package/dist/src/mcp-tools/terminal-tools.js.map +1 -1
- package/dist/src/mcp-tools/types.d.ts +8 -27
- package/dist/src/mcp-tools/types.d.ts.map +1 -1
- package/dist/src/mcp-tools/types.js +9 -91
- package/dist/src/mcp-tools/types.js.map +1 -1
- package/dist/src/memory/file-permissions.d.ts +15 -0
- package/dist/src/memory/file-permissions.d.ts.map +1 -0
- package/dist/src/memory/file-permissions.js +25 -0
- package/dist/src/memory/file-permissions.js.map +1 -0
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +94 -34
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/memory/memory-crud.d.ts.map +1 -1
- package/dist/src/memory/memory-crud.js +298 -275
- package/dist/src/memory/memory-crud.js.map +1 -1
- package/dist/src/memory/memory-initializer.d.ts +2 -4
- package/dist/src/memory/memory-initializer.d.ts.map +1 -1
- package/dist/src/memory/memory-initializer.js +53 -76
- package/dist/src/memory/memory-initializer.js.map +1 -1
- package/dist/src/memory/memory-migrations.d.ts +1 -14
- package/dist/src/memory/memory-migrations.d.ts.map +1 -1
- package/dist/src/memory/memory-migrations.js +52 -105
- package/dist/src/memory/memory-migrations.js.map +1 -1
- package/dist/src/memory/sona-optimizer.d.ts +1 -1
- package/dist/src/memory/sona-optimizer.js +1 -1
- package/dist/src/monovector/index.d.ts +16 -19
- package/dist/src/monovector/index.d.ts.map +1 -1
- package/dist/src/monovector/index.js +50 -8
- package/dist/src/monovector/index.js.map +1 -1
- package/dist/src/orgrt/approvals.d.ts +16 -0
- package/dist/src/orgrt/approvals.d.ts.map +1 -0
- package/dist/src/orgrt/approvals.js +72 -0
- package/dist/src/orgrt/approvals.js.map +1 -0
- package/dist/src/orgrt/bus.d.ts.map +1 -1
- package/dist/src/orgrt/bus.js +21 -0
- package/dist/src/orgrt/bus.js.map +1 -1
- package/dist/src/orgrt/checkpoint-ops.d.ts +19 -0
- package/dist/src/orgrt/checkpoint-ops.d.ts.map +1 -0
- package/dist/src/orgrt/checkpoint-ops.js +219 -0
- package/dist/src/orgrt/checkpoint-ops.js.map +1 -0
- package/dist/src/orgrt/checkpoint.d.ts +33 -1
- package/dist/src/orgrt/checkpoint.d.ts.map +1 -1
- package/dist/src/orgrt/checkpoint.js +50 -6
- package/dist/src/orgrt/checkpoint.js.map +1 -1
- package/dist/src/orgrt/cross-org.d.ts +32 -0
- package/dist/src/orgrt/cross-org.d.ts.map +1 -0
- package/dist/src/orgrt/cross-org.js +256 -0
- package/dist/src/orgrt/cross-org.js.map +1 -0
- package/dist/src/orgrt/daemon.d.ts +131 -157
- package/dist/src/orgrt/daemon.d.ts.map +1 -1
- package/dist/src/orgrt/daemon.js +356 -966
- package/dist/src/orgrt/daemon.js.map +1 -1
- package/dist/src/orgrt/decisions.d.ts +33 -0
- package/dist/src/orgrt/decisions.d.ts.map +1 -0
- package/dist/src/orgrt/decisions.js +169 -0
- package/dist/src/orgrt/decisions.js.map +1 -0
- package/dist/src/orgrt/kimicode-runner.d.ts.map +1 -1
- package/dist/src/orgrt/kimicode-runner.js +5 -1
- package/dist/src/orgrt/kimicode-runner.js.map +1 -1
- package/dist/src/orgrt/opencode-runner.d.ts.map +1 -1
- package/dist/src/orgrt/opencode-runner.js +10 -2
- package/dist/src/orgrt/opencode-runner.js.map +1 -1
- package/dist/src/orgrt/org-memory.d.ts +45 -0
- package/dist/src/orgrt/org-memory.d.ts.map +1 -0
- package/dist/src/orgrt/org-memory.js +220 -0
- package/dist/src/orgrt/org-memory.js.map +1 -0
- package/dist/src/orgrt/policy.d.ts +4 -0
- package/dist/src/orgrt/policy.d.ts.map +1 -1
- package/dist/src/orgrt/policy.js +13 -1
- package/dist/src/orgrt/policy.js.map +1 -1
- package/dist/src/orgrt/prechecks.d.ts +15 -0
- package/dist/src/orgrt/prechecks.d.ts.map +1 -0
- package/dist/src/orgrt/prechecks.js +21 -0
- package/dist/src/orgrt/prechecks.js.map +1 -0
- package/dist/src/orgrt/questions.d.ts +39 -0
- package/dist/src/orgrt/questions.d.ts.map +1 -0
- package/dist/src/orgrt/questions.js +135 -0
- package/dist/src/orgrt/questions.js.map +1 -0
- package/dist/src/orgrt/remote.d.ts +25 -0
- package/dist/src/orgrt/remote.d.ts.map +1 -0
- package/dist/src/orgrt/remote.js +73 -0
- package/dist/src/orgrt/remote.js.map +1 -0
- package/dist/src/orgrt/scheduler-integration.d.ts +19 -0
- package/dist/src/orgrt/scheduler-integration.d.ts.map +1 -0
- package/dist/src/orgrt/scheduler-integration.js +90 -0
- package/dist/src/orgrt/scheduler-integration.js.map +1 -0
- package/dist/src/orgrt/server.d.ts.map +1 -1
- package/dist/src/orgrt/server.js +70 -2
- package/dist/src/orgrt/server.js.map +1 -1
- package/dist/src/orgrt/session.d.ts +30 -1
- package/dist/src/orgrt/session.d.ts.map +1 -1
- package/dist/src/orgrt/session.js +184 -53
- package/dist/src/orgrt/session.js.map +1 -1
- package/dist/src/orgrt/state-detector.d.ts +19 -0
- package/dist/src/orgrt/state-detector.d.ts.map +1 -0
- package/dist/src/orgrt/state-detector.js +59 -0
- package/dist/src/orgrt/state-detector.js.map +1 -0
- package/dist/src/orgrt/task-dag.d.ts +27 -0
- package/dist/src/orgrt/task-dag.d.ts.map +1 -0
- package/dist/src/orgrt/task-dag.js +100 -0
- package/dist/src/orgrt/task-dag.js.map +1 -0
- package/dist/src/orgrt/tool-fence.d.ts +7 -4
- package/dist/src/orgrt/tool-fence.d.ts.map +1 -1
- package/dist/src/orgrt/tool-fence.js +47 -6
- package/dist/src/orgrt/tool-fence.js.map +1 -1
- package/dist/src/orgrt/types.d.ts +57 -2
- package/dist/src/orgrt/types.d.ts.map +1 -1
- package/dist/src/orgrt/types.js +34 -3
- package/dist/src/orgrt/types.js.map +1 -1
- package/dist/src/routing/model-download.d.ts +49 -0
- package/dist/src/routing/model-download.d.ts.map +1 -0
- package/dist/src/routing/model-download.js +106 -0
- package/dist/src/routing/model-download.js.map +1 -0
- package/dist/src/services/crash-reporter.d.ts +5 -0
- package/dist/src/services/crash-reporter.d.ts.map +1 -1
- package/dist/src/services/crash-reporter.js +49 -0
- package/dist/src/services/crash-reporter.js.map +1 -1
- package/dist/src/transfer/anonymization/index.d.ts +18 -15
- package/dist/src/transfer/anonymization/index.d.ts.map +1 -1
- package/dist/src/transfer/anonymization/index.js +7 -119
- package/dist/src/transfer/anonymization/index.js.map +1 -1
- package/dist/src/ui/dashboard.html +606 -7
- package/dist/src/ui/orgs.html +7 -1
- package/dist/src/ui/routes-monograph.mjs +6 -0
- package/dist/src/ui/server.mjs +327 -7
- package/dist/src/update/checker.d.ts.map +1 -1
- package/dist/src/update/checker.js +2 -1
- package/dist/src/update/checker.js.map +1 -1
- package/dist/src/update/executor.d.ts.map +1 -1
- package/dist/src/update/executor.js +7 -3
- package/dist/src/update/executor.js.map +1 -1
- package/dist/src/update/index.d.ts +0 -1
- package/dist/src/update/index.d.ts.map +1 -1
- package/dist/src/update/index.js +5 -24
- package/dist/src/update/index.js.map +1 -1
- package/dist/src/utils/db-mutex.d.ts +11 -0
- package/dist/src/utils/db-mutex.d.ts.map +1 -0
- package/dist/src/utils/db-mutex.js +23 -0
- package/dist/src/utils/db-mutex.js.map +1 -0
- package/dist/src/utils/input-guards.d.ts +23 -0
- package/dist/src/utils/input-guards.d.ts.map +1 -1
- package/dist/src/utils/input-guards.js +72 -0
- package/dist/src/utils/input-guards.js.map +1 -1
- package/dist/src/utils/npm-command.d.ts +10 -0
- package/dist/src/utils/npm-command.d.ts.map +1 -0
- package/dist/src/utils/npm-command.js +12 -0
- package/dist/src/utils/npm-command.js.map +1 -0
- package/dist/src/utils/paths.d.ts +28 -0
- package/dist/src/utils/paths.d.ts.map +1 -0
- package/dist/src/utils/paths.js +108 -0
- package/dist/src/utils/paths.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -17
- package/scripts/download-embedding-model.mjs +97 -0
- package/scripts/publish.sh +0 -0
- package/scripts/sync-claude-assets.sh +0 -0
- package/dist/src/production/circuit-breaker.d.ts +0 -121
- package/dist/src/production/circuit-breaker.d.ts.map +0 -1
- package/dist/src/production/circuit-breaker.js +0 -262
- package/dist/src/production/circuit-breaker.js.map +0 -1
- package/dist/src/production/error-handler.d.ts +0 -119
- package/dist/src/production/error-handler.d.ts.map +0 -1
- package/dist/src/production/error-handler.js +0 -324
- package/dist/src/production/error-handler.js.map +0 -1
- package/dist/src/production/index.d.ts +0 -6
- package/dist/src/production/index.d.ts.map +0 -1
- package/dist/src/production/index.js +0 -6
- package/dist/src/production/index.js.map +0 -1
- package/dist/src/production/monitoring.d.ts +0 -181
- package/dist/src/production/monitoring.d.ts.map +0 -1
- package/dist/src/production/monitoring.js +0 -375
- package/dist/src/production/monitoring.js.map +0 -1
- package/dist/src/production/rate-limiter.d.ts +0 -125
- package/dist/src/production/rate-limiter.d.ts.map +0 -1
- package/dist/src/production/rate-limiter.js +0 -218
- package/dist/src/production/rate-limiter.js.map +0 -1
- package/dist/src/production/retry.d.ts +0 -48
- package/dist/src/production/retry.d.ts.map +0 -1
- package/dist/src/production/retry.js +0 -189
- package/dist/src/production/retry.js.map +0 -1
- package/dist/src/transfer/types.d.ts +0 -245
- package/dist/src/transfer/types.d.ts.map +0 -1
- package/dist/src/transfer/types.js +0 -6
- package/dist/src/transfer/types.js.map +0 -1
package/dist/src/orgrt/daemon.js
CHANGED
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
// packages/@monomind/cli/src/orgrt/daemon.ts
|
|
2
2
|
// monolean: single-process inter-org — upgrade path = daemon-to-daemon HTTP when multi-host is real
|
|
3
|
-
import { readFileSync, mkdirSync,
|
|
3
|
+
import { readFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
4
5
|
import { join, isAbsolute } from 'node:path';
|
|
6
|
+
import { writeJsonFileAtomic } from '../utils/json-file.js';
|
|
5
7
|
import { reapOrphanedSdkProcesses } from '../utils/resource-governor.js';
|
|
6
8
|
import { OrgBus } from './bus.js';
|
|
7
9
|
import { PolicyEngine } from './policy.js';
|
|
8
10
|
import { Mailbox } from './mailbox.js';
|
|
9
11
|
import { runAgentSession } from './session.js';
|
|
10
12
|
import { attachForwarder } from './forwarder.js';
|
|
11
|
-
import { BrokerLease,
|
|
12
|
-
import {
|
|
13
|
+
import { BrokerLease, normalizeCredential } from './broker.js';
|
|
14
|
+
import { drainInbox } from './inbox.js';
|
|
13
15
|
import { OrgDefSchema, ORG_DIR } from './types.js';
|
|
16
|
+
import { TaskDag } from './task-dag.js';
|
|
14
17
|
import { summarizeRun, readRunEvents, readHistory, historyFile } from './reporting.js';
|
|
15
|
-
import {
|
|
18
|
+
import { getResourceLimits, configureResourceLimits } from '../utils/resource-governor.js';
|
|
16
19
|
import { OpencodeAgentRunner } from './opencode-runner.js';
|
|
17
20
|
import { KimiCodeAgentRunner } from './kimicode-runner.js';
|
|
18
|
-
import { captureCheckpoint
|
|
21
|
+
import { captureCheckpoint } from './checkpoint.js';
|
|
22
|
+
// ── Extracted module imports ────────────────────────────────────────────
|
|
23
|
+
import * as approvalOps from './approvals.js';
|
|
24
|
+
import * as questionOps from './questions.js';
|
|
25
|
+
import * as decisionOps from './decisions.js';
|
|
26
|
+
import * as crossOrg from './cross-org.js';
|
|
27
|
+
import * as scheduler from './scheduler-integration.js';
|
|
28
|
+
import * as orgMemory from './org-memory.js';
|
|
29
|
+
import * as checkpointOps from './checkpoint-ops.js';
|
|
19
30
|
/** OpenTelemetry tracing helper - creates spans for major operations */
|
|
20
31
|
class OtelTracer {
|
|
21
32
|
enabled = false;
|
|
@@ -46,35 +57,74 @@ class OtelTracer {
|
|
|
46
57
|
* for a sibling mid-build or mid-test to finish and flush its work. A hard
|
|
47
58
|
* stop keeps the short bound — see finishStop. */
|
|
48
59
|
const COMPLETE_DRAIN_MS = 5 * 60_000;
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
/** Resolve which AgentRunner hosts an org's role sessions.
|
|
61
|
+
* Precedence: role `runtime` field > org def `runtime` field >
|
|
62
|
+
* MONOMIND_RUNTIME env > undefined (the default path, where session.ts
|
|
63
|
+
* falls back to ClaudeAgentRunner). Returning undefined for the default
|
|
64
|
+
* path keeps Claude/Antigravity orgs byte-for-byte unchanged. Callers
|
|
65
|
+
* pass `role.runtime ?? def.runtime` as `orgRuntime` (see resolveRoleRunner). */
|
|
66
|
+
export function resolveRunner(orgRuntime) {
|
|
67
|
+
const selected = orgRuntime ?? process.env.MONOMIND_RUNTIME;
|
|
68
|
+
if (selected === 'opencode')
|
|
69
|
+
return new OpencodeAgentRunner();
|
|
70
|
+
if (selected === 'kimicode')
|
|
71
|
+
return new KimiCodeAgentRunner();
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
/** Per-session variant: a role's own `runtime` field wins over the org-level
|
|
75
|
+
* one (and the env var) — including `role.runtime === 'claude'`, which forces
|
|
76
|
+
* the default Claude path even when the org/env select another runtime.
|
|
77
|
+
* Roles without a `runtime` inherit the org-level resolution unchanged. */
|
|
78
|
+
export function resolveRoleRunner(roleRuntime, orgRuntime) {
|
|
79
|
+
return resolveRunner(roleRuntime ?? orgRuntime);
|
|
80
|
+
}
|
|
81
|
+
/** Per-role token budget: a role's own `budget_tokens` wins; otherwise the
|
|
82
|
+
* even split of run_config.budget_tokens across all roles. */
|
|
83
|
+
export function roleTokenBudget(role, def) {
|
|
84
|
+
return role.budget_tokens ?? Math.floor((def.run_config.budget_tokens ?? 1_000_000) / def.roles.length);
|
|
85
|
+
}
|
|
86
|
+
/** Bounded ring buffer for agent terminal scrollback. */
|
|
87
|
+
export class ScrollbackBuffer {
|
|
88
|
+
maxLines;
|
|
89
|
+
lines = [];
|
|
90
|
+
constructor(maxLines = 500) {
|
|
91
|
+
this.maxLines = maxLines;
|
|
92
|
+
}
|
|
93
|
+
push(line) {
|
|
94
|
+
this.lines.push(line);
|
|
95
|
+
if (this.lines.length > this.maxLines)
|
|
96
|
+
this.lines.splice(0, this.lines.length - this.maxLines);
|
|
97
|
+
}
|
|
98
|
+
snapshot() { return [...this.lines]; }
|
|
99
|
+
clear() { this.lines.length = 0; }
|
|
100
|
+
}
|
|
53
101
|
export class OrgDaemon {
|
|
54
102
|
root;
|
|
55
103
|
opts;
|
|
56
|
-
orgs = new Map();
|
|
57
|
-
waking = new Set();
|
|
58
|
-
globalSubscribers = new Set();
|
|
59
|
-
leases = new Map();
|
|
60
|
-
forwarders = new Map();
|
|
61
|
-
watchdogs = new Map();
|
|
62
|
-
stopping = new Map();
|
|
63
|
-
otel = new OtelTracer();
|
|
64
|
-
approvals = new Map();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// reset only by an explicit (non-restart) startOrg.
|
|
104
|
+
/** @internal */ orgs = new Map();
|
|
105
|
+
/** @internal */ waking = new Set();
|
|
106
|
+
/** @internal */ globalSubscribers = new Set();
|
|
107
|
+
/** @internal */ leases = new Map();
|
|
108
|
+
/** @internal */ forwarders = new Map();
|
|
109
|
+
/** @internal */ watchdogs = new Map();
|
|
110
|
+
/** @internal */ stopping = new Map();
|
|
111
|
+
/** @internal */ otel = new OtelTracer();
|
|
112
|
+
/** @internal */ approvals = new Map();
|
|
113
|
+
/** @internal */ approvalLocks = new Map();
|
|
114
|
+
/** @internal */ gatesLocks = new Map();
|
|
115
|
+
/** @internal */ questionsLocks = new Map();
|
|
116
|
+
/** @internal */ spawning = new Map();
|
|
70
117
|
static MAX_BOSS_RESTARTS = 2;
|
|
71
118
|
static BOSS_RESTART_BACKOFF_MS = [10_000, 30_000];
|
|
72
|
-
bossRestartCounts = new Map();
|
|
73
|
-
restarting = new Set();
|
|
119
|
+
/** @internal */ bossRestartCounts = new Map();
|
|
120
|
+
/** @internal */ restarting = new Set();
|
|
74
121
|
// #3: recognizes provider context-window-overflow errors so the boss can be told
|
|
75
122
|
// to chunk the work instead of re-dispatching the same oversized task verbatim.
|
|
76
123
|
static CONTEXT_LIMIT_RE = /context[- ]?(window|length|size|limit)|maximum context|exceeds?.{0,12}(context|token)|too many tokens|prompt is too long/i;
|
|
77
|
-
|
|
124
|
+
/** @internal */ recallUsage = new Map();
|
|
125
|
+
/** @internal */ orgLearnedRuns = new Set();
|
|
126
|
+
/** @internal */ abandoned = new Map();
|
|
127
|
+
constructor(/** @internal */ root, /** @internal */ opts = {}) {
|
|
78
128
|
this.root = root;
|
|
79
129
|
this.opts = opts;
|
|
80
130
|
}
|
|
@@ -91,16 +141,84 @@ export class OrgDaemon {
|
|
|
91
141
|
}
|
|
92
142
|
listOrgs() { return [...this.orgs.values()]; }
|
|
93
143
|
getOrg(name) { return this.orgs.get(name); }
|
|
144
|
+
/** Hot-reload an org definition from disk without stopping running sessions.
|
|
145
|
+
* Applies: goal, run_config, schedule. New roles are added as pending (lazy-spawnable).
|
|
146
|
+
* Removed roles are NOT killed — they finish their current work and won't be re-spawned.
|
|
147
|
+
* Returns a summary of what changed. */
|
|
148
|
+
reloadOrgDef(name) {
|
|
149
|
+
const running = this.orgs.get(name);
|
|
150
|
+
if (!running)
|
|
151
|
+
throw new Error(`org ${name} is not running`);
|
|
152
|
+
const defPath = join(this.root, ORG_DIR, `${name}.json`);
|
|
153
|
+
const newDef = OrgDefSchema.parse(JSON.parse(readFileSync(defPath, 'utf8')));
|
|
154
|
+
const changed = [];
|
|
155
|
+
const newRoles = [];
|
|
156
|
+
const removedRoles = [];
|
|
157
|
+
if (newDef.goal !== running.def.goal) {
|
|
158
|
+
running.def.goal = newDef.goal;
|
|
159
|
+
changed.push('goal');
|
|
160
|
+
}
|
|
161
|
+
const oldRc = running.def.run_config;
|
|
162
|
+
const newRc = newDef.run_config;
|
|
163
|
+
for (const key of new Set([...Object.keys(oldRc), ...Object.keys(newRc)])) {
|
|
164
|
+
if (JSON.stringify(oldRc[key]) !== JSON.stringify(newRc[key])) {
|
|
165
|
+
oldRc[key] = newRc[key];
|
|
166
|
+
changed.push(`run_config.${key}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const existingRoleIds = new Set(running.def.roles.map(r => r.id));
|
|
170
|
+
const newRoleIds = new Set(newDef.roles.map(r => r.id));
|
|
171
|
+
for (const role of newDef.roles) {
|
|
172
|
+
if (!existingRoleIds.has(role.id)) {
|
|
173
|
+
running.def.roles.push(role);
|
|
174
|
+
if (!running.pendingRoles)
|
|
175
|
+
running.pendingRoles = new Map();
|
|
176
|
+
running.pendingRoles.set(role.id, role);
|
|
177
|
+
newRoles.push(role.id);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const id of existingRoleIds) {
|
|
181
|
+
if (!newRoleIds.has(id))
|
|
182
|
+
removedRoles.push(id);
|
|
183
|
+
}
|
|
184
|
+
running.bus.emit({ type: 'audit', reason: 'hot-reload',
|
|
185
|
+
msg: `org def reloaded: ${changed.length} fields changed, ${newRoles.length} new roles, ${removedRoles.length} removed roles`,
|
|
186
|
+
data: { changed, newRoles, removedRoles } });
|
|
187
|
+
return { changed, newRoles, removedRoles };
|
|
188
|
+
}
|
|
94
189
|
/** Names of the orgs this daemon currently has running. Snapshot — safe to
|
|
95
190
|
* iterate while stopOrg() mutates the underlying map. */
|
|
96
191
|
listRunning() { return [...this.orgs.keys()]; }
|
|
192
|
+
/** Hook for the SSE server — registers a listener for all bus events across all orgs. */
|
|
193
|
+
onBusEvent = (fn) => { this.subscribe(fn); };
|
|
194
|
+
/** Snapshot of all running orgs for dashboard initial load. */
|
|
195
|
+
getStatusSnapshot = () => {
|
|
196
|
+
const orgs = [];
|
|
197
|
+
for (const [name, running] of this.orgs) {
|
|
198
|
+
const roles = [];
|
|
199
|
+
for (const [roleId, agent] of running.agents) {
|
|
200
|
+
roles.push({
|
|
201
|
+
id: roleId, status: agent.status,
|
|
202
|
+
worktree: agent.worktreePath ?? null,
|
|
203
|
+
metrics: agent.metrics,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
orgs.push({
|
|
207
|
+
name, run: running.run,
|
|
208
|
+
roles,
|
|
209
|
+
pendingRoles: running.pendingRoles ? [...running.pendingRoles.keys()] : [],
|
|
210
|
+
tasks: running.taskDag?.all() ?? [],
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return { orgs };
|
|
214
|
+
};
|
|
97
215
|
/** Resolve run_config.workspace to 'repo' | 'isolated' | an absolute path.
|
|
98
216
|
* A relative path is resolved against the project root rather than the
|
|
99
217
|
* daemon's cwd, which is not the same directory when `org serve` is started
|
|
100
218
|
* from a subdirectory. */
|
|
101
219
|
workspaceSetting(def) {
|
|
102
220
|
const ws = def.run_config.workspace ?? 'repo';
|
|
103
|
-
if (ws === 'repo' || ws === 'isolated' || ws === 'worktree')
|
|
221
|
+
if (ws === 'repo' || ws === 'isolated' || ws === 'worktree' || ws === 'worktree-per-role')
|
|
104
222
|
return ws;
|
|
105
223
|
return isAbsolute(ws) ? ws : join(this.root, ws);
|
|
106
224
|
}
|
|
@@ -137,11 +255,13 @@ export class OrgDaemon {
|
|
|
137
255
|
try {
|
|
138
256
|
// Remove stale worktree from a previous run
|
|
139
257
|
if (existsSync(worktreePath)) {
|
|
140
|
-
|
|
258
|
+
// R4: bound the call — a wedged git hook (git-lfs, gc lock, gpg sign
|
|
259
|
+
// prompt) would otherwise hang the whole daemon indefinitely.
|
|
260
|
+
execSync(`git worktree remove --force "${worktreePath}"`, { cwd: this.root, stdio: 'ignore', timeout: 30_000 });
|
|
141
261
|
}
|
|
142
262
|
}
|
|
143
263
|
catch { /* best-effort cleanup */ }
|
|
144
|
-
execSync(`git worktree add "${worktreePath}" HEAD --detach`, { cwd: this.root, stdio: 'ignore' });
|
|
264
|
+
execSync(`git worktree add "${worktreePath}" HEAD --detach`, { cwd: this.root, stdio: 'ignore', timeout: 30_000 });
|
|
145
265
|
cwd = worktreePath;
|
|
146
266
|
}
|
|
147
267
|
else {
|
|
@@ -159,6 +279,19 @@ export class OrgDaemon {
|
|
|
159
279
|
if (!process.env.MONOMIND_MAX_SDK_PROCS && getResourceLimits().maxSdkProcesses < def.roles.length) {
|
|
160
280
|
configureResourceLimits({ maxSdkProcesses: def.roles.length });
|
|
161
281
|
}
|
|
282
|
+
// Validate per-role providers before spawning anything (fail-fast: a
|
|
283
|
+
// missing env var discovered 10 minutes into a run wastes the entire run).
|
|
284
|
+
const { resolveProviderEnv: validateProvider } = await import('./provider.js');
|
|
285
|
+
for (const role of def.roles) {
|
|
286
|
+
if (role.provider) {
|
|
287
|
+
try {
|
|
288
|
+
validateProvider(role.provider);
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
throw new Error(`org ${name}: role "${role.id}" provider validation failed — ${err instanceof Error ? err.message : err}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
162
295
|
const bus = new OrgBus(name, run, dir);
|
|
163
296
|
// Lightweight in-memory tail for busEvents() (test-loop, /api/history).
|
|
164
297
|
// Full events (including Write content snapshots) live on disk in bus.jsonl;
|
|
@@ -220,6 +353,7 @@ export class OrgDaemon {
|
|
|
220
353
|
this.forwarders.set(name, attachForwarder(bus, this.opts.controlJson ?? join(this.root, '.monomind/control.json')));
|
|
221
354
|
const running = { def, run, bus, agents: new Map(), busEvents: () => [...collected], workdir: cwd };
|
|
222
355
|
this.orgs.set(name, running);
|
|
356
|
+
// Even-split budget; a role's own budget_tokens overrides it (roleTokenBudget).
|
|
223
357
|
const perRoleBudget = Math.floor((def.run_config.budget_tokens ?? 1_000_000) / def.roles.length);
|
|
224
358
|
// Single boss-selection rule for kickoff AND org_complete gating — the
|
|
225
359
|
// session layer previously keyed the tool on reports_to===null while the
|
|
@@ -250,21 +384,53 @@ export class OrgDaemon {
|
|
|
250
384
|
// scheduleDeferredSpawn() once resources free up, without re-running the
|
|
251
385
|
// gate logic or duplicating the session-wiring below.
|
|
252
386
|
const spawnRole = (role) => {
|
|
387
|
+
let roleCwd = cwd;
|
|
388
|
+
if (ws === 'worktree-per-role' && role.id !== bossRole.id) {
|
|
389
|
+
const wtPath = join(this.root, ORG_DIR, name, `worktree-${role.id}`);
|
|
390
|
+
try {
|
|
391
|
+
// Q7: top-level `import { execSync }` replaces the inlined
|
|
392
|
+
// `require('node:child_process')` that broke ESM at runtime —
|
|
393
|
+
// vitest's CJS shim masked it in tests but the built package
|
|
394
|
+
// threw "require is not defined" in real Node ESM execution.
|
|
395
|
+
if (existsSync(wtPath)) {
|
|
396
|
+
try {
|
|
397
|
+
execSync(`git worktree remove --force "${wtPath}"`, { cwd: this.root, stdio: 'ignore', timeout: 30_000 });
|
|
398
|
+
}
|
|
399
|
+
catch { /* best-effort */ }
|
|
400
|
+
}
|
|
401
|
+
execSync(`git worktree add "${wtPath}" HEAD --detach`, { cwd: this.root, stdio: 'ignore', timeout: 30_000 });
|
|
402
|
+
roleCwd = wtPath;
|
|
403
|
+
}
|
|
404
|
+
catch { /* fallback to shared cwd if git worktree fails */ }
|
|
405
|
+
}
|
|
253
406
|
const mailbox = new Mailbox();
|
|
254
|
-
const policy = new PolicyEngine(role.id, { maxTokens: perRoleBudget, ...(role.policy ?? {}) }, bus,
|
|
255
|
-
const runtime = { mailbox, policy, status: 'running', done: Promise.resolve(), metrics: { tokens: 0, costUsd: 0 } };
|
|
407
|
+
const policy = new PolicyEngine(role.id, { maxTokens: role.budget_tokens ?? perRoleBudget, ...(role.policy ?? {}) }, bus, roleCwd);
|
|
408
|
+
const runtime = { mailbox, policy, status: 'running', done: Promise.resolve(), metrics: { tokens: 0, costUsd: 0 }, worktreePath: roleCwd !== cwd ? roleCwd : undefined, scrollback: new ScrollbackBuffer() };
|
|
256
409
|
const sessionOpts = {
|
|
257
|
-
org: name, role, bus, policy, mailbox, cwd, def,
|
|
410
|
+
org: name, role, bus, policy, mailbox, cwd: roleCwd, def,
|
|
258
411
|
maxTurns: role.max_turns_per_message ?? def.run_config.max_turns_per_message,
|
|
259
412
|
lastMessageId: () => runtime.lastMessageId,
|
|
413
|
+
onOutput: (line) => runtime.scrollback.push(line),
|
|
260
414
|
deliver: (from, to, subject, body) => this.deliver(name, from, to, subject, body),
|
|
261
415
|
askHuman: (r, question) => this.askHuman(name, r, question),
|
|
416
|
+
onGate: (r, gateName, gateDesc) => this.createGate(name, r, gateName, gateDesc),
|
|
417
|
+
circuitBreaker: (() => {
|
|
418
|
+
const cb = def.run_config.circuit_breaker;
|
|
419
|
+
if (!cb)
|
|
420
|
+
return undefined;
|
|
421
|
+
return { threshold: cb.failure_threshold ?? 5, state: { failures: 0, tripped: false } };
|
|
422
|
+
})(),
|
|
262
423
|
beforeTool: (r, toolName) => this.checkApproval(name, r, toolName),
|
|
263
424
|
onComplete: role.id === bossRole.id
|
|
264
425
|
? (r, outcome, summary) => {
|
|
265
426
|
bus.emit({ type: 'status', from: r, reason: 'org-complete', msg: `run outcome: ${outcome}`, data: { outcome, summary } });
|
|
266
427
|
}
|
|
267
428
|
: undefined,
|
|
429
|
+
// #11: a boss that overflows its context window isn't a crash (it keeps
|
|
430
|
+
// returning +0-token errors forever), so without this the idle watchdog
|
|
431
|
+
// just nudges it for ~30 min before idle-stopping. Restart the whole org
|
|
432
|
+
// with fresh sessions instead — bounded by MAX_BOSS_RESTARTS.
|
|
433
|
+
onContextLimit: role.id === bossRole.id ? () => this.scheduleBossRestart(name) : undefined,
|
|
268
434
|
recall: async (r, q) => {
|
|
269
435
|
const answer = await this.recallOrgMemory(name, def, q, r);
|
|
270
436
|
bus.emit({ type: 'status', from: r, reason: 'org-recall', msg: `recall: ${q.slice(0, 80)}`, data: { hits: answer.hits } });
|
|
@@ -289,15 +455,24 @@ export class OrgDaemon {
|
|
|
289
455
|
});
|
|
290
456
|
return text;
|
|
291
457
|
},
|
|
458
|
+
createTask: (r, title, assignee, deps) => {
|
|
459
|
+
return this.dagCreateTask(name, r, title, assignee, deps);
|
|
460
|
+
},
|
|
461
|
+
completeTask: (r, taskId, result) => {
|
|
462
|
+
return this.dagCompleteTask(name, r, taskId, result);
|
|
463
|
+
},
|
|
464
|
+
listTasks: () => {
|
|
465
|
+
const running = this.orgs.get(name);
|
|
466
|
+
return JSON.stringify(running?.taskDag?.all() ?? [], null, 2);
|
|
467
|
+
},
|
|
292
468
|
queryFn: this.opts.queryFn,
|
|
293
|
-
// Runner resolution: explicit >
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
: undefined),
|
|
469
|
+
// Runner resolution: explicit opts.runner > role `runtime` field >
|
|
470
|
+
// org def `runtime` field > MONOMIND_RUNTIME env (opencode/kimicode) >
|
|
471
|
+
// undefined (session.ts falls back to ClaudeAgentRunner via queryFn).
|
|
472
|
+
// Leaving it undefined for the default path is what keeps
|
|
473
|
+
// Claude/Antigravity orgs byte-for-byte unchanged. Session opts are
|
|
474
|
+
// built per role here in spawnRole, so each role gets its own runner.
|
|
475
|
+
runner: this.opts.runner ?? resolveRoleRunner(role.runtime, def.runtime),
|
|
301
476
|
};
|
|
302
477
|
// Supervised session: transient crashes (provider blips, network) restart
|
|
303
478
|
// with backoff; a crash with the mailbox already closed, or one that
|
|
@@ -413,6 +588,7 @@ export class OrgDaemon {
|
|
|
413
588
|
}
|
|
414
589
|
running.pendingRoles = pendingRoles;
|
|
415
590
|
running.spawnRole = spawnRole;
|
|
591
|
+
running.taskDag = new TaskDag();
|
|
416
592
|
if (worktreePath)
|
|
417
593
|
running.worktreePath = worktreePath;
|
|
418
594
|
// Crash cleanup: reap SDK children if this process exits abnormally.
|
|
@@ -429,6 +605,25 @@ export class OrgDaemon {
|
|
|
429
605
|
};
|
|
430
606
|
process.on('exit', crashCleanup);
|
|
431
607
|
running._crashCleanup = crashCleanup;
|
|
608
|
+
// Stale-base drift detection: if the working tree is too many commits behind
|
|
609
|
+
// its tracking branch, warn or refuse to start. Best-effort — git may not be
|
|
610
|
+
// available, or the repo may have no tracking branch.
|
|
611
|
+
const staleThreshold = def.run_config.stale_base_threshold;
|
|
612
|
+
if (staleThreshold && staleThreshold > 0 && cwd === this.root) {
|
|
613
|
+
try {
|
|
614
|
+
const { execSync } = await import('node:child_process');
|
|
615
|
+
const behind = execSync('git rev-list --count HEAD..@{upstream} 2>/dev/null', { cwd, encoding: 'utf8', timeout: 10_000 }).trim();
|
|
616
|
+
const count = parseInt(behind, 10);
|
|
617
|
+
if (!isNaN(count) && count > staleThreshold) {
|
|
618
|
+
bus.emit({
|
|
619
|
+
type: 'audit', reason: 'stale-base',
|
|
620
|
+
msg: `working tree is ${count} commits behind upstream (threshold: ${staleThreshold}) — consider pulling before running`,
|
|
621
|
+
data: { behind: count, threshold: staleThreshold },
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
catch { /* no upstream tracking or git unavailable — skip silently */ }
|
|
626
|
+
}
|
|
432
627
|
const boss = bossRole;
|
|
433
628
|
// Cross-run memory: brief the coordinator on the previous run so scheduled
|
|
434
629
|
// orgs accumulate instead of starting cold every interval.
|
|
@@ -462,6 +657,10 @@ export class OrgDaemon {
|
|
|
462
657
|
const wd = setInterval(() => {
|
|
463
658
|
if (this.restarting.has(name))
|
|
464
659
|
return; // boss auto-restart in flight — don't nudge or stop
|
|
660
|
+
// A pending gate means the org is legitimately waiting for human input
|
|
661
|
+
const pendingGates = this.readGates(name).gates.filter(g => g.status === 'pending');
|
|
662
|
+
if (pendingGates.length > 0)
|
|
663
|
+
return;
|
|
465
664
|
const idleFor = Date.now() - lastActivity;
|
|
466
665
|
if (idleFor < idleMs) {
|
|
467
666
|
nudgedAt = 0;
|
|
@@ -518,540 +717,12 @@ export class OrgDaemon {
|
|
|
518
717
|
bus.emit({ type: 'status', msg: `drained ${queued.length} queued message(s) from inbox` });
|
|
519
718
|
return running;
|
|
520
719
|
}
|
|
521
|
-
/**
|
|
522
|
-
* for capacity in the background (bounded) and spawn it the moment resources
|
|
523
|
-
* free up, instead of silently running the org shorthanded for its whole life.
|
|
524
|
-
* Bails quietly if the org is stopped (or restarted under the same name)
|
|
525
|
-
* before capacity returns; `running` is compared by identity, not `name`,
|
|
526
|
-
* so a stale retry can never spawn into a different run. */
|
|
527
|
-
scheduleDeferredSpawn(name, running, role, spawnRole) {
|
|
528
|
-
const MAX_ATTEMPTS = 6; // ~30 min of retrying before giving up loudly
|
|
529
|
-
(async () => {
|
|
530
|
-
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
531
|
-
const waited = await waitForCapacity(5 * 60_000);
|
|
532
|
-
if (this.orgs.get(name) !== running)
|
|
533
|
-
return; // org stopped/restarted — abandon quietly
|
|
534
|
-
if (waited.ok) {
|
|
535
|
-
running.bus.emit({ type: 'audit', from: role.id, reason: 'resource-recovered',
|
|
536
|
-
msg: `resources recovered after ${attempt} retr${attempt === 1 ? 'y' : 'ies'} — spawning deferred role "${role.id}"` });
|
|
537
|
-
// Drain messages queued while the role was deferred BEFORE spawning
|
|
538
|
-
// to prevent race condition where messages arrive during spawn window
|
|
539
|
-
const queued = drainInbox(this.root, name);
|
|
540
|
-
spawnRole(role);
|
|
541
|
-
for (const msg of queued) {
|
|
542
|
-
const agent = running.agents.get(msg.toRole);
|
|
543
|
-
if (agent && !agent.mailbox.isClosed) {
|
|
544
|
-
running.bus.emit({ type: 'xorg', from: msg.fromQualified, to: `${name}:${msg.toRole}`, subject: msg.subject, msg: msg.body });
|
|
545
|
-
agent.mailbox.push(this.mailBody(name, running, `[message from ${msg.fromQualified}] subject: ${msg.subject}`, msg.body, `inbox-${msg.ts}-${Math.random().toString(36).slice(2, 8)}`));
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
running.bus.emit({ type: 'audit', from: role.id, reason: 'resource-pressure',
|
|
551
|
-
msg: `still under pressure (attempt ${attempt}/${MAX_ATTEMPTS}) — retrying "${role.id}" spawn: ${waited.reason}` });
|
|
552
|
-
}
|
|
553
|
-
const missing = this.abandoned.get(name) ?? new Set();
|
|
554
|
-
missing.add(role.id);
|
|
555
|
-
this.abandoned.set(name, missing);
|
|
556
|
-
this.persistState(name, 'running', running.run);
|
|
557
|
-
running.bus.emit({ type: 'audit', from: role.id, reason: 'resource-abandoned',
|
|
558
|
-
msg: `giving up spawning "${role.id}" after ${MAX_ATTEMPTS} retries — org will run without this role until manually restarted` });
|
|
559
|
-
})().catch(err => console.error(`org ${name}: deferred spawn of "${role.id}" failed:`, err instanceof Error ? err.message : err));
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Resolves an org_send `to` address ("role" for same-org, "org:role" for
|
|
563
|
-
* cross-org) into its parts. Centralizes the one addressing rule that
|
|
564
|
-
* matters (an "own-org:role" self-prefix is intra-org, not cross-org) so
|
|
565
|
-
* deliver()/deliverRemote() don't each re-derive it — the qualified `to`
|
|
566
|
-
* string returned is always the canonical display form for that address.
|
|
567
|
-
*/
|
|
568
|
-
resolveAddress(fromOrg, to) {
|
|
569
|
-
const cross = to.includes(':');
|
|
570
|
-
if (!cross)
|
|
571
|
-
return { cross: false, orgName: fromOrg, role: to, qualified: to };
|
|
572
|
-
const [orgName, role] = to.split(':', 2);
|
|
573
|
-
if (orgName === fromOrg)
|
|
574
|
-
return { cross: false, orgName, role, qualified: role }; // self-prefixed — still intra-org
|
|
575
|
-
return { cross: true, orgName, role, qualified: to };
|
|
576
|
-
}
|
|
577
|
-
/** Mailbox bodies are unbounded — a pasted 20KB file would persist in the
|
|
578
|
-
* recipient's context for the whole run. Bodies over MAIL_BODY_MAX are
|
|
579
|
-
* written to <org workdir>/.mail/<message-id>.md and replaced with a ~1KB
|
|
580
|
-
* digest plus a pointer; smaller messages stay byte-identical. */
|
|
581
|
-
mailBody(orgName, org, header, body, id) {
|
|
582
|
-
if (body.length <= MAIL_BODY_MAX)
|
|
583
|
-
return `${header}\n\n${body}`;
|
|
584
|
-
const mailDir = join(org?.workdir ?? join(this.root, ORG_DIR, orgName), '.mail');
|
|
585
|
-
const file = join(mailDir, `${id.replace(/[^a-zA-Z0-9_-]/g, '_')}.md`);
|
|
586
|
-
try {
|
|
587
|
-
mkdirSync(mailDir, { recursive: true });
|
|
588
|
-
writeFileSync(file, body);
|
|
589
|
-
return `${header}\n\n${body.slice(0, MAIL_DIGEST_CHARS)}\n\n[... truncated — full text at ${file} — Read it if needed]`;
|
|
590
|
-
}
|
|
591
|
-
catch {
|
|
592
|
-
return `${header}\n\n${body}`; // digest write failed — deliver in full rather than lose content
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
/** Route a message. to = "role" (same org) or "org:role" (cross-org). Returns a receipt string. */
|
|
596
|
-
async deliver(fromOrg, fromRole, to, subject, body) {
|
|
597
|
-
const { cross, orgName: targetOrgName, role: targetRole, qualified: toQualified } = this.resolveAddress(fromOrg, to);
|
|
598
|
-
const targetOrg = this.orgs.get(targetOrgName);
|
|
599
|
-
const src = this.orgs.get(fromOrg);
|
|
600
|
-
// Lazy spawn: if the role is pending (not yet spawned), spawn it now.
|
|
601
|
-
// ATOMIC GUARD: Check spawning Set to prevent duplicate spawns from concurrent messages
|
|
602
|
-
const spawning = this.spawning.get(targetOrgName) ?? new Set();
|
|
603
|
-
this.spawning.set(targetOrgName, spawning);
|
|
604
|
-
if (targetOrg && !targetOrg.agents.has(targetRole) && targetOrg.pendingRoles?.has(targetRole) && !spawning.has(targetRole)) {
|
|
605
|
-
const role = targetOrg.pendingRoles.get(targetRole);
|
|
606
|
-
targetOrg.pendingRoles.delete(targetRole);
|
|
607
|
-
spawning.add(targetRole); // Mark as spawning before async work
|
|
608
|
-
const check = checkResources();
|
|
609
|
-
if (!check.ok) {
|
|
610
|
-
const waited = await waitForCapacity(60_000);
|
|
611
|
-
spawning.delete(targetRole); // Clear spawning flag after check
|
|
612
|
-
if (!waited.ok) {
|
|
613
|
-
targetOrg.bus.emit({ type: 'audit', from: targetRole, reason: 'resource-skip',
|
|
614
|
-
msg: `deferring lazy spawn of "${targetRole}": ${waited.reason}` });
|
|
615
|
-
// Queue the triggering message so it survives the deferred spawn — without
|
|
616
|
-
// this the sender got "queued" but the message was silently lost.
|
|
617
|
-
// B5 FIX: Queue FIRST, then schedule spawn only if queue succeeds.
|
|
618
|
-
// If queueing fails, we return the error without modifying spawn state.
|
|
619
|
-
const queued = queueMessage(this.root, targetOrgName, {
|
|
620
|
-
fromQualified: cross ? `${fromOrg}:${fromRole}` : fromRole,
|
|
621
|
-
toRole: targetRole, subject, body, ts: Date.now(),
|
|
622
|
-
});
|
|
623
|
-
if (!queued) {
|
|
624
|
-
src?.bus.emit({ type: 'audit', from: fromRole, to: toQualified, msg: `queue failed: ${subject}`, reason: 'queue-failed' });
|
|
625
|
-
return `ERROR: could not queue message for ${toQualified} (disk full or permissions)`;
|
|
626
|
-
}
|
|
627
|
-
this.scheduleDeferredSpawn(targetOrgName, targetOrg, role, targetOrg.spawnRole);
|
|
628
|
-
return `queued for ${toQualified} (role starting — waiting for resources)`;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
targetOrg.spawnRole(role);
|
|
632
|
-
spawning.delete(targetRole); // Clear spawning flag after spawn completes
|
|
633
|
-
targetOrg.bus.emit({ type: 'status', from: targetRole, msg: `lazy-spawned on first message from ${fromRole}` });
|
|
634
|
-
}
|
|
635
|
-
if (!targetOrg || !targetOrg.agents.has(targetRole)) {
|
|
636
|
-
if (cross && this.opts.crossProcess)
|
|
637
|
-
return this.deliverRemote(fromOrg, fromRole, targetOrgName, targetRole, toQualified, subject, body, src);
|
|
638
|
-
// Queue + auto-wake: if the org definition exists locally but isn't running, spool the message and start it
|
|
639
|
-
if (cross && this.hasOrgDef(targetOrgName)) {
|
|
640
|
-
const queued = queueMessage(this.root, targetOrgName, { fromQualified: `${fromOrg}:${fromRole}`, toRole: targetRole, subject, body, ts: Date.now() });
|
|
641
|
-
if (!queued) {
|
|
642
|
-
src?.bus.emit({ type: 'audit', from: fromRole, to: toQualified, msg: `queue failed: ${subject}`, reason: 'queue-failed' });
|
|
643
|
-
return `ERROR: could not queue message for ${toQualified} (disk full or permissions)`;
|
|
644
|
-
}
|
|
645
|
-
src?.bus.emit({ type: 'xorg', from: `${fromOrg}:${fromRole}`, to: toQualified, subject, msg: body, data: { queued: true } });
|
|
646
|
-
this.autoWake(targetOrgName);
|
|
647
|
-
return `queued for ${toQualified} (org starting)`;
|
|
648
|
-
}
|
|
649
|
-
src?.bus.emit({ type: 'audit', from: fromRole, to: toQualified, msg: `undeliverable: ${subject}`, reason: 'unknown recipient' });
|
|
650
|
-
return `ERROR: unknown recipient "${toQualified}" (known: ${[...(targetOrg?.agents.keys() ?? this.orgs.keys())].join(', ')})`;
|
|
651
|
-
}
|
|
652
|
-
const targetAgent = targetOrg.agents.get(targetRole);
|
|
653
|
-
if (targetAgent.status === 'crashed') {
|
|
654
|
-
src?.bus.emit({ type: 'audit', from: fromRole, to: toQualified, msg: `undeliverable: ${subject}`, reason: 'recipient crashed (retry budget exhausted)' });
|
|
655
|
-
return `ERROR: recipient "${toQualified}" crashed and will not recover this run — message not delivered (${targetAgent.error ?? 'unknown error'})`;
|
|
656
|
-
}
|
|
657
|
-
if (targetAgent.mailbox.isClosed) {
|
|
658
|
-
// Distinguish two cases that used to share one drop:
|
|
659
|
-
// - org mid-shutdown: nothing will ever read the queue again — the
|
|
660
|
-
// message genuinely can't be delivered, so report the real outcome.
|
|
661
|
-
// - agent session ended but the org is alive (budget exhaustion,
|
|
662
|
-
// turn limit, crash-restart in flight): the result is still
|
|
663
|
-
// valuable, so persist it to the inbox. The boss-restart/next-run
|
|
664
|
-
// drainInbox will deliver it instead of the work vanishing.
|
|
665
|
-
if (this.stopping.has(targetOrgName)) {
|
|
666
|
-
src?.bus.emit({ type: 'audit', from: fromRole, to: toQualified, msg: `undeliverable: ${subject}`, reason: 'target mailbox closed (org shutting down)' });
|
|
667
|
-
return `ERROR: recipient "${toQualified}" is shutting down — message not delivered`;
|
|
668
|
-
}
|
|
669
|
-
const q = queueMessage(this.root, targetOrgName, { fromQualified: `${fromOrg}:${fromRole}`, toRole: targetRole, subject, body, ts: Date.now() });
|
|
670
|
-
src?.bus.emit({ type: 'audit', from: fromRole, to: toQualified, msg: `recipient session closed — queued to inbox: ${subject}`, data: { queued: q } });
|
|
671
|
-
return `queued to inbox for ${toQualified} (recipient session closed; will be delivered on restart)`;
|
|
672
|
-
}
|
|
673
|
-
// Track message chain: link this message to the target's last message (the one being responded to)
|
|
674
|
-
const targetAgentSrc = targetOrg === src ? src?.agents.get(targetRole) : undefined;
|
|
675
|
-
const parentId = targetAgentSrc?.lastMessageId;
|
|
676
|
-
const evt = { from: cross ? `${fromOrg}:${fromRole}` : fromRole, to: toQualified, subject, msg: body, parentId };
|
|
677
|
-
const emitted = src?.bus.emit({ type: cross ? 'xorg' : 'message', ...evt });
|
|
678
|
-
if (cross && targetOrg !== src)
|
|
679
|
-
targetOrg.bus.emit({ type: 'xorg', ...evt });
|
|
680
|
-
// Store message ID for the target (so responses can link to it)
|
|
681
|
-
if (targetAgentSrc && emitted)
|
|
682
|
-
targetAgentSrc.lastMessageId = emitted.id;
|
|
683
|
-
// Also track the source agent's last sent message for cross-org visibility
|
|
684
|
-
const srcAgent = src?.agents.get(fromRole);
|
|
685
|
-
if (srcAgent && emitted)
|
|
686
|
-
srcAgent.lastMessageId = emitted.id;
|
|
687
|
-
targetAgent.mailbox.push(this.mailBody(targetOrgName, targetOrg, `[message from ${evt.from}] subject: ${subject}`, body, emitted?.id ?? `mail-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`));
|
|
688
|
-
return `delivered to ${toQualified}`;
|
|
689
|
-
}
|
|
690
|
-
/** Cross-process leg of deliver(): ask the machine-local broker who hosts targetOrgName, then POST over HTTP.
|
|
691
|
-
* `to` here is always the fully-qualified "org:role" display form (resolveAddress already normalized it). */
|
|
692
|
-
async deliverRemote(fromOrg, fromRole, targetOrgName, targetRole, to, subject, body, src) {
|
|
693
|
-
const remote = lookupOrg(targetOrgName, this.opts.brokerDir);
|
|
694
|
-
if (!remote) {
|
|
695
|
-
// No remote host either — queue + auto-wake if the org def exists locally
|
|
696
|
-
if (this.hasOrgDef(targetOrgName)) {
|
|
697
|
-
const queued = queueMessage(this.root, targetOrgName, { fromQualified: `${fromOrg}:${fromRole}`, toRole: targetRole, subject, body, ts: Date.now() });
|
|
698
|
-
if (!queued) {
|
|
699
|
-
src?.bus.emit({ type: 'audit', from: fromRole, to, msg: `queue failed: ${subject}`, reason: 'queue-failed' });
|
|
700
|
-
return `ERROR: could not queue message for ${to} (disk full or permissions)`;
|
|
701
|
-
}
|
|
702
|
-
src?.bus.emit({ type: 'xorg', from: `${fromOrg}:${fromRole}`, to, subject, msg: body, data: { queued: true } });
|
|
703
|
-
this.autoWake(targetOrgName);
|
|
704
|
-
return `queued for ${to} (org starting)`;
|
|
705
|
-
}
|
|
706
|
-
src?.bus.emit({ type: 'audit', from: fromRole, to, msg: `undeliverable: ${subject}`, reason: 'unknown recipient' });
|
|
707
|
-
return `ERROR: unknown recipient "${to}" (no local org, and no process on this machine has org "${targetOrgName}" registered)`;
|
|
708
|
-
}
|
|
709
|
-
try {
|
|
710
|
-
const res = await fetch(`${remote.url}/api/xdeliver`, {
|
|
711
|
-
method: 'POST',
|
|
712
|
-
headers: {
|
|
713
|
-
'Content-Type': 'application/json',
|
|
714
|
-
...(remote.credential ? { 'x-monomind-cred': remote.credential } : {}),
|
|
715
|
-
},
|
|
716
|
-
body: JSON.stringify({ fromOrg, fromRole, toOrg: targetOrgName, toRole: targetRole, subject, body }),
|
|
717
|
-
signal: AbortSignal.timeout(10_000),
|
|
718
|
-
});
|
|
719
|
-
const data = await res.json().catch(() => ({}));
|
|
720
|
-
if (res.ok && data.ok) {
|
|
721
|
-
src?.bus.emit({ type: 'xorg', from: `${fromOrg}:${fromRole}`, to, subject, msg: body });
|
|
722
|
-
return data.receipt ?? `delivered to ${to} (remote)`;
|
|
723
|
-
}
|
|
724
|
-
src?.bus.emit({ type: 'audit', from: fromRole, to, msg: `remote delivery rejected: ${data.error ?? res.status}`, reason: 'remote-delivery-rejected' });
|
|
725
|
-
return `ERROR: remote org "${to}" rejected delivery: ${data.error ?? res.status}`;
|
|
726
|
-
}
|
|
727
|
-
catch (err) {
|
|
728
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
729
|
-
src?.bus.emit({ type: 'audit', from: fromRole, to, msg: `remote delivery failed: ${message}`, reason: 'remote-delivery-failed' });
|
|
730
|
-
return `ERROR: remote org "${targetOrgName}" unreachable: ${message}`;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
/** Inbound handler for cross-process delivery — called by the server's POST /api/xdeliver route
|
|
734
|
-
* when ANOTHER process's deliverRemote() reaches this daemon. Pushes straight into the target
|
|
735
|
-
* agent's mailbox; the agent picks it up on its own next turn (see Mailbox — never interrupts). */
|
|
736
|
-
receiveRemote(toOrg, toRole, fromQualified, subject, body) {
|
|
737
|
-
const org = this.orgs.get(toOrg);
|
|
738
|
-
if (!org) {
|
|
739
|
-
// Org not running — queue the message and auto-wake if the def exists
|
|
740
|
-
if (this.hasOrgDef(toOrg)) {
|
|
741
|
-
const queued = queueMessage(this.root, toOrg, { fromQualified, toRole, subject, body, ts: Date.now() });
|
|
742
|
-
if (!queued) {
|
|
743
|
-
return { ok: false, error: `could not queue message for ${toOrg}:${toRole} (disk full or permissions)` };
|
|
744
|
-
}
|
|
745
|
-
this.autoWake(toOrg);
|
|
746
|
-
return { ok: true, receipt: `queued for ${toOrg}:${toRole} (org waking)` };
|
|
747
|
-
}
|
|
748
|
-
return { ok: false, error: `org "${toOrg}" not hosted here` };
|
|
749
|
-
}
|
|
750
|
-
// Lazy-spawn pending roles on cross-process delivery (matches deliver/answerQuestion)
|
|
751
|
-
// ATOMIC GUARD: Check spawning Set to prevent duplicate spawns from concurrent messages
|
|
752
|
-
const spawning = this.spawning.get(toOrg) ?? new Set();
|
|
753
|
-
this.spawning.set(toOrg, spawning);
|
|
754
|
-
if (!org.agents.has(toRole) && org.pendingRoles?.has(toRole) && !spawning.has(toRole)) {
|
|
755
|
-
const role = org.pendingRoles.get(toRole);
|
|
756
|
-
org.pendingRoles.delete(toRole);
|
|
757
|
-
spawning.add(toRole); // Mark as spawning before async work
|
|
758
|
-
// Resource gate check before spawning (prevents bypass in cross-process delivery)
|
|
759
|
-
const check = checkResources();
|
|
760
|
-
if (!check.ok) {
|
|
761
|
-
spawning.delete(toRole); // Clear spawning flag after check
|
|
762
|
-
org.bus.emit({ type: 'audit', from: toRole, reason: 'resource-pressure',
|
|
763
|
-
msg: `cross-process lazy spawn deferred: ${check.reason}` });
|
|
764
|
-
// B4 FIX: Queue the triggering message FIRST, then schedule spawn only if queue succeeds.
|
|
765
|
-
// This matches the pattern in deliver() and prevents message loss if queue fails.
|
|
766
|
-
const queued = queueMessage(this.root, toOrg, { fromQualified, toRole, subject, body, ts: Date.now() });
|
|
767
|
-
if (!queued) {
|
|
768
|
-
return { ok: false, error: `could not queue message for ${toOrg}:${toRole} (disk full or permissions)` };
|
|
769
|
-
}
|
|
770
|
-
this.scheduleDeferredSpawn(toOrg, org, role, org.spawnRole);
|
|
771
|
-
return { ok: true, receipt: `queued for ${toOrg}:${toRole} (role starting — waiting for resources)` };
|
|
772
|
-
}
|
|
773
|
-
org.spawnRole?.(role);
|
|
774
|
-
spawning.delete(toRole); // Clear spawning flag after spawn completes
|
|
775
|
-
org.bus.emit({ type: 'status', from: toRole, msg: `lazy-spawned on remote delivery from ${fromQualified}` });
|
|
776
|
-
}
|
|
777
|
-
const agent = org.agents.get(toRole);
|
|
778
|
-
if (!agent)
|
|
779
|
-
return { ok: false, error: `role "${toRole}" not found in org "${toOrg}"` };
|
|
780
|
-
if (agent.status === 'crashed')
|
|
781
|
-
return { ok: false, error: `role "${toRole}" in org "${toOrg}" crashed and will not recover this run` };
|
|
782
|
-
if (agent.mailbox.isClosed)
|
|
783
|
-
return { ok: false, error: `role "${toRole}" in org "${toOrg}" is shutting down` };
|
|
784
|
-
const messageEvent = org.bus.emit({ type: 'xorg', from: fromQualified, to: `${toOrg}:${toRole}`, subject, msg: body });
|
|
785
|
-
agent.lastMessageId = messageEvent.id; // Track last message ID for response threading
|
|
786
|
-
agent.mailbox.push(this.mailBody(toOrg, org, `[message from ${fromQualified}] subject: ${subject}`, body, messageEvent.id));
|
|
787
|
-
return { ok: true, receipt: `delivered to ${toOrg}:${toRole} (remote)` };
|
|
788
|
-
}
|
|
720
|
+
/** @internal */
|
|
789
721
|
hasOrgDef(name) {
|
|
790
722
|
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(name))
|
|
791
723
|
return false;
|
|
792
724
|
return existsSync(join(this.root, ORG_DIR, `${name}.json`));
|
|
793
725
|
}
|
|
794
|
-
questionsPath(org) {
|
|
795
|
-
return join(this.root, ORG_DIR, org, 'questions.json');
|
|
796
|
-
}
|
|
797
|
-
readQuestions(org) {
|
|
798
|
-
try {
|
|
799
|
-
return JSON.parse(readFileSync(this.questionsPath(org), 'utf8'));
|
|
800
|
-
}
|
|
801
|
-
catch {
|
|
802
|
-
return { questions: [] };
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
writeQuestions(org, data) {
|
|
806
|
-
const dest = this.questionsPath(org);
|
|
807
|
-
mkdirSync(join(this.root, ORG_DIR, org), { recursive: true });
|
|
808
|
-
const tmp = `${dest}.${process.pid}.tmp`;
|
|
809
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
810
|
-
renameSync(tmp, dest);
|
|
811
|
-
}
|
|
812
|
-
/** Check if an action requires human approval (beforeTool hook for guardrails). Returns
|
|
813
|
-
* the approval decision: true = approved, false = denied, null = pending (requires human input). */
|
|
814
|
-
async checkApproval(org, role, action) {
|
|
815
|
-
const approvalKey = `${org}:${role}:${action}`;
|
|
816
|
-
const pending = this.approvals.get(org) ?? [];
|
|
817
|
-
const existing = pending.find(a => a.roleId === role && a.action === action);
|
|
818
|
-
// If already approved/denied, return that decision
|
|
819
|
-
if (existing && existing.approved !== null)
|
|
820
|
-
return existing.approved;
|
|
821
|
-
// Require human approval for sensitive actions
|
|
822
|
-
const sensitiveActions = ['Bash', 'WebFetch', 'WebSearch', 'org_complete'];
|
|
823
|
-
if (sensitiveActions.includes(action)) {
|
|
824
|
-
// Queue for approval
|
|
825
|
-
if (!existing) {
|
|
826
|
-
pending.push({ roleId: role, action, question: `Approve ${action} tool call?`, ts: Date.now(), approved: null });
|
|
827
|
-
this.approvals.set(org, pending);
|
|
828
|
-
}
|
|
829
|
-
// Persist to approvals.json
|
|
830
|
-
const approvalsPath = join(this.root, ORG_DIR, org, 'approvals.json');
|
|
831
|
-
mkdirSync(join(this.root, ORG_DIR, org), { recursive: true });
|
|
832
|
-
writeFileSync(approvalsPath, JSON.stringify({ approvals: pending }, null, 2));
|
|
833
|
-
// Emit a question event for the dashboard
|
|
834
|
-
const running = this.orgs.get(org);
|
|
835
|
-
running?.bus.emit({ type: 'question', from: role, data: { question: `Approval required for ${action}`, action } });
|
|
836
|
-
return null; // Pending human approval
|
|
837
|
-
}
|
|
838
|
-
return true; // Auto-approved for non-sensitive actions
|
|
839
|
-
}
|
|
840
|
-
/** Approve or deny a pending action (called by dashboard or CLI). */
|
|
841
|
-
async setApproval(org, role, action, approved) {
|
|
842
|
-
const pending = this.approvals.get(org) ?? [];
|
|
843
|
-
const item = pending.find(a => a.roleId === role && a.action === action);
|
|
844
|
-
if (!item)
|
|
845
|
-
return { ok: false, error: `No pending approval found for ${role} action ${action}` };
|
|
846
|
-
item.approved = approved;
|
|
847
|
-
item.ts = Date.now();
|
|
848
|
-
// Persist updated approval state
|
|
849
|
-
const approvalsPath = join(this.root, ORG_DIR, org, 'approvals.json');
|
|
850
|
-
writeFileSync(approvalsPath, JSON.stringify({ approvals: pending }, null, 2));
|
|
851
|
-
// Notify the waiting agent via its mailbox
|
|
852
|
-
const running = this.orgs.get(org);
|
|
853
|
-
const agent = running?.agents.get(role);
|
|
854
|
-
if (agent && !agent.mailbox.isClosed) {
|
|
855
|
-
agent.mailbox.push(`[approval] ${action}: ${approved ? 'APPROVED' : 'DENIED'}`);
|
|
856
|
-
}
|
|
857
|
-
running?.bus.emit({ type: 'status', from: role, msg: `Approval ${approved ? 'granted' : 'denied'} for ${action}` });
|
|
858
|
-
return { ok: true };
|
|
859
|
-
}
|
|
860
|
-
/** Time-travel debugging: replay from a specific checkpoint by run ID.
|
|
861
|
-
* Creates a fresh daemon instance and replays events from the target run's bus.jsonl. */
|
|
862
|
-
async replayFrom(name, run) {
|
|
863
|
-
const runDir = join(this.root, ORG_DIR, name, run);
|
|
864
|
-
if (!existsSync(runDir))
|
|
865
|
-
return null;
|
|
866
|
-
const busFile = join(runDir, 'bus.jsonl');
|
|
867
|
-
if (!existsSync(busFile))
|
|
868
|
-
return null;
|
|
869
|
-
// Create a replay org with a fresh run ID
|
|
870
|
-
const replayRun = `replay-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
871
|
-
const replayDir = join(this.root, ORG_DIR, name, replayRun);
|
|
872
|
-
mkdirSync(replayDir, { recursive: true });
|
|
873
|
-
// Read original events
|
|
874
|
-
const events = readFileSync(busFile, 'utf8').split('\n').filter(Boolean)
|
|
875
|
-
.map(l => { try {
|
|
876
|
-
return JSON.parse(l);
|
|
877
|
-
}
|
|
878
|
-
catch {
|
|
879
|
-
return null;
|
|
880
|
-
} })
|
|
881
|
-
.filter((e) => e !== null);
|
|
882
|
-
if (!events.length)
|
|
883
|
-
return null;
|
|
884
|
-
// Load org definition
|
|
885
|
-
const defPath = join(this.root, ORG_DIR, `${name}.json`);
|
|
886
|
-
if (!existsSync(defPath))
|
|
887
|
-
return null;
|
|
888
|
-
const def = OrgDefSchema.parse(JSON.parse(readFileSync(defPath, 'utf8')));
|
|
889
|
-
// Create replay bus
|
|
890
|
-
const bus = new OrgBus(name, replayRun, replayDir);
|
|
891
|
-
const MAX_COLLECTED = 1000;
|
|
892
|
-
const collected = [];
|
|
893
|
-
bus.subscribe(e => {
|
|
894
|
-
const slim = e.data?.content != null
|
|
895
|
-
? { ...e, data: { ...e.data, content: undefined } }
|
|
896
|
-
: e;
|
|
897
|
-
collected.push(slim);
|
|
898
|
-
if (collected.length > MAX_COLLECTED)
|
|
899
|
-
collected.splice(0, collected.length - MAX_COLLECTED);
|
|
900
|
-
for (const fn of this.globalSubscribers)
|
|
901
|
-
fn(e);
|
|
902
|
-
});
|
|
903
|
-
const running = { def, run: replayRun, bus, agents: new Map(), busEvents: () => [...collected] };
|
|
904
|
-
// Reemit events into the replay bus with updated timestamps
|
|
905
|
-
const startTime = Date.now();
|
|
906
|
-
for (const e of events) {
|
|
907
|
-
const replayEvent = { ...e, org: name, run: replayRun, ts: startTime };
|
|
908
|
-
bus.emit(replayEvent);
|
|
909
|
-
}
|
|
910
|
-
this.orgs.set(name, running);
|
|
911
|
-
bus.emit({ type: 'status', msg: `replay started from ${run} (${events.length} events replayed)` });
|
|
912
|
-
this.persistState(name, 'running', replayRun);
|
|
913
|
-
return running;
|
|
914
|
-
}
|
|
915
|
-
/** Agent-initiated human question (ask_human tool). Persists to questions.json (survives
|
|
916
|
-
* process/dashboard restarts) and emits a 'question' BusEvent so the dashboard's SSE
|
|
917
|
-
* stream and global inbox pick it up in real time. Returns a receipt string for the tool call. */
|
|
918
|
-
async askHuman(org, role, question) {
|
|
919
|
-
const running = this.orgs.get(org);
|
|
920
|
-
const questionId = `q-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
921
|
-
const data = this.readQuestions(org);
|
|
922
|
-
data.questions.push({ questionId, role, question, ts: Date.now(), answer: null, answeredAt: null });
|
|
923
|
-
this.writeQuestions(org, data);
|
|
924
|
-
running?.bus.emit({ type: 'question', from: role, data: { questionId, question } });
|
|
925
|
-
return `Question recorded (id ${questionId}) — a human will answer it; you'll receive the answer as a new message.`;
|
|
926
|
-
}
|
|
927
|
-
/** Delivers a human's answer to a pending ask_human question. If the org is still
|
|
928
|
-
* running, pushes straight into the role's live mailbox (picked up on its very next
|
|
929
|
-
* generator tick — see Mailbox.stream()). If the org has since stopped, queues the
|
|
930
|
-
* answer via the same offline fallback deliver()/receiveRemote() already use
|
|
931
|
-
* (inbox.ts + autoWake) and it's delivered when the org next starts.
|
|
932
|
-
*
|
|
933
|
-
* PERSIST-AFTER-DELIVERY: questions.json is only marked answered once delivery has
|
|
934
|
-
* actually happened (mailbox push, or the message landing in inbox.jsonl). Marking it
|
|
935
|
-
* first meant a rejected delivery — unknown role, mailbox closed mid-shutdown, a
|
|
936
|
-
* queueMessage that threw on a full/read-only disk — left the question recorded as
|
|
937
|
-
* answered while nobody ever received the answer, and the `already answered` guard
|
|
938
|
-
* then refused every retry. The answer was simply gone. The inverse failure (crash
|
|
939
|
-
* between delivery and the write) merely re-shows the question as pending, which a
|
|
940
|
-
* human can act on; a silently swallowed answer is not recoverable. */
|
|
941
|
-
async answerQuestion(org, role, questionId, answer) {
|
|
942
|
-
const data = this.readQuestions(org);
|
|
943
|
-
const idx = data.questions.findIndex(q => q.questionId === questionId);
|
|
944
|
-
if (idx === -1)
|
|
945
|
-
return { ok: false, error: `question "${questionId}" not found for org "${org}"` };
|
|
946
|
-
if (data.questions[idx].answer !== null)
|
|
947
|
-
return { ok: false, error: `question "${questionId}" already answered` };
|
|
948
|
-
const question = data.questions[idx].question;
|
|
949
|
-
// Applied to questions.json ONLY after the delivery below succeeds.
|
|
950
|
-
const markAnswered = () => {
|
|
951
|
-
// Re-read so a question the daemon appended (or answered) meanwhile isn't
|
|
952
|
-
// clobbered by this stale snapshot — merge by questionId, never replace.
|
|
953
|
-
const fresh = this.readQuestions(org);
|
|
954
|
-
const fIdx = fresh.questions.findIndex(q => q.questionId === questionId);
|
|
955
|
-
if (fIdx === -1)
|
|
956
|
-
fresh.questions.push({ ...data.questions[idx], answer, answeredAt: Date.now() });
|
|
957
|
-
else
|
|
958
|
-
fresh.questions[fIdx] = { ...fresh.questions[fIdx], answer, answeredAt: Date.now() };
|
|
959
|
-
this.writeQuestions(org, fresh);
|
|
960
|
-
};
|
|
961
|
-
const running = this.orgs.get(org);
|
|
962
|
-
if (running) {
|
|
963
|
-
// Org IS running — deliver or report a real error, but never fall through to the
|
|
964
|
-
// offline queue+autoWake path below: autoWake() no-ops when this.orgs already has
|
|
965
|
-
// the org (see its own guard), so a role-specific delivery failure here (mailbox
|
|
966
|
-
// closed, role unknown) would otherwise queue the answer forever with no real error
|
|
967
|
-
// and no delivery. Mirrors deliver()'s existing "shutting down" error for the same
|
|
968
|
-
// mid-shutdown-mailbox-closed race.
|
|
969
|
-
// Spawn a lazily-deferred role before giving up on it. Roles are no longer
|
|
970
|
-
// all spawned at boot, so `agents` legitimately lacks a role that simply
|
|
971
|
-
// has not been needed yet — and rejecting on that dropped the human's
|
|
972
|
-
// answer with "role not found" for a role that exists and is about to
|
|
973
|
-
// run. deliver() does the same lookup; a human answer must not be the one
|
|
974
|
-
// delivery path that cannot wake a role.
|
|
975
|
-
if (!running.agents.has(role) && running.pendingRoles?.has(role)) {
|
|
976
|
-
const pending = running.pendingRoles.get(role);
|
|
977
|
-
running.pendingRoles.delete(role);
|
|
978
|
-
running.spawnRole?.(pending);
|
|
979
|
-
}
|
|
980
|
-
const agent = running.agents.get(role);
|
|
981
|
-
if (!agent)
|
|
982
|
-
return { ok: false, error: `role "${role}" not found in org "${org}"` };
|
|
983
|
-
if (agent.mailbox.isClosed)
|
|
984
|
-
return { ok: false, error: `role "${role}" in org "${org}" is shutting down — answer not delivered` };
|
|
985
|
-
try {
|
|
986
|
-
agent.mailbox.push(`[answer from human] question: ${question}\n\nanswer: ${answer}`);
|
|
987
|
-
}
|
|
988
|
-
catch (err) {
|
|
989
|
-
return { ok: false, error: `delivery to "${role}" in org "${org}" failed — answer not recorded (${err instanceof Error ? err.message : String(err)})` };
|
|
990
|
-
}
|
|
991
|
-
markAnswered();
|
|
992
|
-
running.bus.emit({ type: 'status', from: role, msg: 'question answered', data: { questionId } });
|
|
993
|
-
return { ok: true };
|
|
994
|
-
}
|
|
995
|
-
// Org not running at all — queue for delivery on next start, matching deliver()'s
|
|
996
|
-
// existing offline fallback exactly (inbox.ts + autoWake).
|
|
997
|
-
if (!this.hasOrgDef(org))
|
|
998
|
-
return { ok: false, error: `org "${org}" not found (no saved definition)` };
|
|
999
|
-
const queued = queueMessage(this.root, org, {
|
|
1000
|
-
fromQualified: 'human', toRole: role,
|
|
1001
|
-
subject: `answer:${questionId}`,
|
|
1002
|
-
body: `question: ${question}\n\nanswer: ${answer}`,
|
|
1003
|
-
ts: Date.now(),
|
|
1004
|
-
});
|
|
1005
|
-
if (!queued) {
|
|
1006
|
-
return { ok: false, error: `could not queue answer for org "${org}" — answer not recorded (disk full or permissions)` };
|
|
1007
|
-
}
|
|
1008
|
-
markAnswered();
|
|
1009
|
-
this.autoWake(org);
|
|
1010
|
-
return { ok: true };
|
|
1011
|
-
}
|
|
1012
|
-
/** Start an offline org in the background so queued messages get drained.
|
|
1013
|
-
* Fire-and-forget — errors are logged but don't propagate to the sender. */
|
|
1014
|
-
autoWake(name) {
|
|
1015
|
-
if (this.orgs.has(name) || this.waking.has(name))
|
|
1016
|
-
return;
|
|
1017
|
-
this.waking.add(name);
|
|
1018
|
-
this.startOrg(name)
|
|
1019
|
-
.catch(err => { console.error(`auto-wake org "${name}" failed:`, err instanceof Error ? err.message : err); })
|
|
1020
|
-
.finally(() => { this.waking.delete(name); });
|
|
1021
|
-
}
|
|
1022
|
-
/** #4: bounded whole-org restart after the boss terminally crashes. Stops the
|
|
1023
|
-
* dead run and re-launches it with fresh sessions (shedding any bloated
|
|
1024
|
-
* context). Capped at MAX_BOSS_RESTARTS per explicit start so a crashing boss
|
|
1025
|
-
* can't loop forever; beyond the cap it gives up and lets the idle watchdog
|
|
1026
|
-
* shut the run down for a human. */
|
|
1027
|
-
scheduleBossRestart(name) {
|
|
1028
|
-
if (this.stopping.has(name) || this.restarting.has(name))
|
|
1029
|
-
return;
|
|
1030
|
-
const count = this.bossRestartCounts.get(name) ?? 0;
|
|
1031
|
-
const bus = this.orgs.get(name)?.bus;
|
|
1032
|
-
if (count >= OrgDaemon.MAX_BOSS_RESTARTS) {
|
|
1033
|
-
bus?.emit({ type: 'audit', reason: 'boss-restart-exhausted',
|
|
1034
|
-
msg: `boss crashed again after ${count} auto-restart(s) — giving up; manual restart required` });
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
const backoffSchedule = this.opts.bossRestartBackoffMs ?? OrgDaemon.BOSS_RESTART_BACKOFF_MS;
|
|
1038
|
-
const backoff = backoffSchedule[Math.min(count, backoffSchedule.length - 1)];
|
|
1039
|
-
this.bossRestartCounts.set(name, count + 1);
|
|
1040
|
-
this.restarting.add(name);
|
|
1041
|
-
bus?.emit({ type: 'audit', reason: 'boss-restart',
|
|
1042
|
-
msg: `boss crashed — auto-restarting org with fresh sessions in ${Math.round(backoff / 1000)}s (attempt ${count + 1}/${OrgDaemon.MAX_BOSS_RESTARTS})` });
|
|
1043
|
-
const t = setTimeout(() => {
|
|
1044
|
-
if (this.stopping.has(name)) {
|
|
1045
|
-
this.restarting.delete(name);
|
|
1046
|
-
return;
|
|
1047
|
-
} // a manual stop won
|
|
1048
|
-
this.stopOrg(name)
|
|
1049
|
-
.then(() => (this.stopping.has(name) ? null : this.startOrg(name)))
|
|
1050
|
-
.then(() => { this.restarting.delete(name); })
|
|
1051
|
-
.catch(err => { this.restarting.delete(name); console.error(`org ${name}: boss auto-restart failed:`, err instanceof Error ? err.message : err); });
|
|
1052
|
-
}, backoff);
|
|
1053
|
-
t.unref?.();
|
|
1054
|
-
}
|
|
1055
726
|
/** @param opts.drainMs how long to let in-flight agent sessions finish before
|
|
1056
727
|
* reaping. Defaults to the short abort bound; the planned-completion path
|
|
1057
728
|
* passes a far longer window (see COMPLETE_DRAIN_MS). */
|
|
@@ -1175,406 +846,33 @@ export class OrgDaemon {
|
|
|
1175
846
|
// Pass the org directly since we already removed it from the map.
|
|
1176
847
|
if (!this.orgs.has(name))
|
|
1177
848
|
this.persistState(name, 'stopped', org.run, org, stopCheckpoint);
|
|
1178
|
-
// Clean up git
|
|
1179
|
-
if (org.worktreePath) {
|
|
1180
|
-
try {
|
|
1181
|
-
const { execSync } = await import('node:child_process');
|
|
1182
|
-
execSync(`git worktree remove --force "${org.worktreePath}"`, { cwd: this.root, stdio: 'ignore' });
|
|
1183
|
-
}
|
|
1184
|
-
catch { /* best-effort — stale worktree can be cleaned up manually */ }
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
async stopAll() {
|
|
1188
|
-
await Promise.all([
|
|
1189
|
-
...[...this.orgs.keys()].map(n => this.stopOrg(n)),
|
|
1190
|
-
...this.stopping.values(), // detached self-stops still flushing
|
|
1191
|
-
]);
|
|
1192
|
-
}
|
|
1193
|
-
orgMemoryNamespace(name, def) {
|
|
1194
|
-
return def.run_config.memory_namespace ?? `org:${name}`;
|
|
1195
|
-
}
|
|
1196
|
-
/** Store dir for org cross-run memory — inside the org root so the bridge's
|
|
1197
|
-
* path guard accepts it when the daemon runs from the project (the normal
|
|
1198
|
-
* case) and test roots stay isolated. */
|
|
1199
|
-
orgMemoryDbPath() {
|
|
1200
|
-
return join(this.root, '.monomind', 'org-memory');
|
|
1201
|
-
}
|
|
1202
|
-
/** The memory bridge's traversal guard silently redirects out-of-tree paths
|
|
1203
|
-
* to the per-project default store. For an org rooted outside cwd (tests,
|
|
1204
|
-
* unusual daemon setups) that redirect would write into the WRONG project's
|
|
1205
|
-
* memory — verify the guard kept our path, and skip org memory otherwise. */
|
|
1206
|
-
async orgMemoryUsable() {
|
|
849
|
+
// Clean up git worktrees — shared (workspace: 'worktree') and per-role.
|
|
1207
850
|
try {
|
|
1208
|
-
const {
|
|
1209
|
-
|
|
1210
|
-
const got = bridgeGetDbPath(want);
|
|
1211
|
-
const { realpathSync } = await import('node:fs');
|
|
1212
|
-
const real = (p) => { try {
|
|
1213
|
-
return realpathSync(p);
|
|
1214
|
-
}
|
|
1215
|
-
catch {
|
|
1216
|
-
return p;
|
|
1217
|
-
} };
|
|
1218
|
-
return real(got) === real(want);
|
|
1219
|
-
}
|
|
1220
|
-
catch {
|
|
1221
|
-
return false;
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
/** Namespace for a role's PRIVATE memories, inside the org memory DB. */
|
|
1225
|
-
agentMemoryNamespace(name, def, role) {
|
|
1226
|
-
return `agent:${this.orgMemoryNamespace(name, def)}:${role}`;
|
|
1227
|
-
}
|
|
1228
|
-
/** org_remember implementation: a deliberate write to org-shared or
|
|
1229
|
-
* role-private memory (both in the org memory DB, split by namespace). */
|
|
1230
|
-
async rememberOrgMemory(name, def, role, content, scope, run) {
|
|
1231
|
-
try {
|
|
1232
|
-
if (!(await this.orgMemoryUsable()))
|
|
1233
|
-
return 'org memory is not available in this environment.';
|
|
1234
|
-
const { bridgeStoreEntry } = await import('../memory/memory-bridge.js');
|
|
1235
|
-
const namespace = scope === 'agent' ? this.agentMemoryNamespace(name, def, role) : this.orgMemoryNamespace(name, def);
|
|
1236
|
-
const res = await bridgeStoreEntry({
|
|
1237
|
-
key: `mem-${run}-${Date.now().toString(36)}`,
|
|
1238
|
-
value: content.slice(0, 20_000),
|
|
1239
|
-
namespace,
|
|
1240
|
-
dbPath: this.orgMemoryDbPath(),
|
|
1241
|
-
tags: [scope, role],
|
|
1242
|
-
metadata: { origin_refs: [`run:${run}`], by: role },
|
|
1243
|
-
});
|
|
1244
|
-
if (res?.duplicate)
|
|
1245
|
-
return `Already remembered (near-duplicate exists) — reinforced instead.`;
|
|
1246
|
-
return res?.success ? `Remembered (${scope} scope).` : `Could not store memory${res?.error ? `: ${res.error}` : ''}.`;
|
|
1247
|
-
}
|
|
1248
|
-
catch (err) {
|
|
1249
|
-
return `org_remember failed (${err instanceof Error ? err.message : 'error'})`;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
/** Entry IDs served by org_recall during the current run, per org. This is
|
|
1253
|
-
* the usage record the run-outcome auto-rating consumes (cognee's
|
|
1254
|
-
* used_graph_element_ids pattern) — recall renders text to the agent, so
|
|
1255
|
-
* the IDs must be captured here or they're gone. */
|
|
1256
|
-
recallUsage = new Map();
|
|
1257
|
-
/** org_recall implementation: search the org's memory namespace via the
|
|
1258
|
-
* memory bridge (semantic when the local model is available, tokenized
|
|
1259
|
-
* keyword otherwise). Failures return a message, never throw into the tool. */
|
|
1260
|
-
async recallOrgMemory(name, def, query, role) {
|
|
1261
|
-
try {
|
|
1262
|
-
if (!(await this.orgMemoryUsable()))
|
|
1263
|
-
return { text: 'org memory is not available in this environment.', hits: 0 };
|
|
1264
|
-
const bridge = await import('../memory/memory-bridge.js');
|
|
1265
|
-
// Shared org memory plus the caller's private agent scope, merged by score.
|
|
1266
|
-
const [shared, priv] = await Promise.all([
|
|
1267
|
-
bridge.bridgeSearchEntries({
|
|
1268
|
-
query, namespace: this.orgMemoryNamespace(name, def), limit: 5, dbPath: this.orgMemoryDbPath(),
|
|
1269
|
-
}),
|
|
1270
|
-
role ? bridge.bridgeSearchEntries({
|
|
1271
|
-
query, namespace: this.agentMemoryNamespace(name, def, role), limit: 3, dbPath: this.orgMemoryDbPath(),
|
|
1272
|
-
}) : null,
|
|
1273
|
-
]);
|
|
1274
|
-
const results = [
|
|
1275
|
-
...(shared?.results ?? []),
|
|
1276
|
-
...(priv?.results ?? []).map(r => ({ ...r, key: `${r.key} (private)` })),
|
|
1277
|
-
].sort((a, b) => b.score - a.score).slice(0, 6);
|
|
1278
|
-
if (!results.length)
|
|
1279
|
-
return { text: 'No matching org memory found — this may be the first run covering this topic.', hits: 0 };
|
|
1280
|
-
const ids = results.map(r => r.id).filter(Boolean);
|
|
1281
|
-
let used = this.recallUsage.get(name);
|
|
1282
|
-
if (!used) {
|
|
1283
|
-
used = new Set();
|
|
1284
|
-
this.recallUsage.set(name, used);
|
|
1285
|
-
}
|
|
1286
|
-
for (const id of ids)
|
|
1287
|
-
used.add(id);
|
|
1288
|
-
// Frequency reinforcement is immediate; the feedback rating waits for the
|
|
1289
|
-
// run outcome (positive-only — see storeRunMemory).
|
|
1290
|
-
bridge.bridgeRecordUsage({ entryIds: ids, dbPath: this.orgMemoryDbPath() }).catch(() => { });
|
|
1291
|
-
let text = results.map((r, i) => `${i + 1}. [${r.key}] ${r.content.slice(0, 500)}`).join('\n\n');
|
|
1292
|
-
// Structured knowledge: relationship triplets from the org KG, when any.
|
|
1293
|
-
try {
|
|
1294
|
-
const kg = await import('../memory/memory-kg.js');
|
|
1295
|
-
const graph = await kg.kgSearch({ query, dbPath: this.orgMemoryDbPath(), limit: 5 });
|
|
1296
|
-
if (graph.context)
|
|
1297
|
-
text += `\n\nKnowledge graph:\n${graph.context.slice(0, 1024)}`;
|
|
1298
|
-
}
|
|
1299
|
-
catch { /* best effort */ }
|
|
1300
|
-
return { text, hits: results.length };
|
|
1301
|
-
}
|
|
1302
|
-
catch (err) {
|
|
1303
|
-
return { text: `org memory unavailable (${err instanceof Error ? err.message : 'error'})`, hits: 0 };
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
/** knowledge_search implementation for org agents: the user's Second Brain
|
|
1307
|
-
* (this project's documents + the personal global brain), merged with the
|
|
1308
|
-
* same project-first ranking every other surface uses. Failures return a
|
|
1309
|
-
* message, never throw into the tool call. */
|
|
1310
|
-
async searchProjectKnowledge(query) {
|
|
1311
|
-
try {
|
|
1312
|
-
const { searchKnowledge } = await import('../knowledge/document-pipeline.js');
|
|
1313
|
-
const excerpts = await searchKnowledge(query, { rootDir: this.root, limit: 3, store: 'all' });
|
|
1314
|
-
if (!excerpts.length)
|
|
1315
|
-
return { text: 'No matching documents in the Second Brain for that query.', hits: 0 };
|
|
1316
|
-
const text = excerpts.map((e, i) => `${i + 1}. [${e.filePath || 'unknown'}${e.scope === 'global' ? ' · global' : ''}] (${e.similarity.toFixed(2)})\n${e.text.slice(0, 400)}`).join('\n\n');
|
|
1317
|
-
return { text, hits: excerpts.length };
|
|
1318
|
-
}
|
|
1319
|
-
catch (err) {
|
|
1320
|
-
return { text: `knowledge search unavailable (${err instanceof Error ? err.message : 'error'})`, hits: 0 };
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
/** org_learn implementation: merge coordinator-extracted entities/relations/
|
|
1324
|
-
* rules into the org's knowledge graph (LLM extraction happens inside the
|
|
1325
|
-
* agent's own subscription-auth SDK session — no separate LLM call here). */
|
|
1326
|
-
orgLearnedRuns = new Set();
|
|
1327
|
-
async learnOrgKnowledge(name, run, payload) {
|
|
1328
|
-
try {
|
|
1329
|
-
if (!(await this.orgMemoryUsable()))
|
|
1330
|
-
return 'org memory is not available in this environment.';
|
|
1331
|
-
const kg = await import('../memory/memory-kg.js');
|
|
1332
|
-
const dbPath = this.orgMemoryDbPath();
|
|
1333
|
-
const originRef = `run:${run}`;
|
|
1334
|
-
const graph = await kg.kgIngest({
|
|
1335
|
-
nodes: (payload.nodes ?? []),
|
|
1336
|
-
edges: (payload.edges ?? []),
|
|
1337
|
-
originRef, dbPath,
|
|
1338
|
-
});
|
|
1339
|
-
const rules = Array.isArray(payload.rules) && payload.rules.length
|
|
1340
|
-
? await kg.kgIngestRules({ rules: payload.rules, originRef, dbPath })
|
|
1341
|
-
: null;
|
|
1342
|
-
this.orgLearnedRuns.add(`${name}:${run}`);
|
|
1343
|
-
const parts = [
|
|
1344
|
-
`entities: +${graph.nodesAdded} new, ${graph.nodesMerged} merged`,
|
|
1345
|
-
`relations: +${graph.edgesAdded} new, ${graph.edgesMerged} merged`,
|
|
1346
|
-
];
|
|
1347
|
-
if (rules)
|
|
1348
|
-
parts.push(`rules: ${rules.accepted} accepted, ${rules.verdicts.filter(v => v.verdict === 'already_known').length} already known`);
|
|
1349
|
-
return `Recorded in org knowledge graph — ${parts.join('; ')}. Rollback ref: ${originRef}.`;
|
|
1350
|
-
}
|
|
1351
|
-
catch (err) {
|
|
1352
|
-
return `org_learn failed (${err instanceof Error ? err.message : 'error'})`;
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
/** Persist the run's outcome into cross-run org memory so org_recall (and
|
|
1356
|
-
* future runs) can find it by meaning, not just recency. Best-effort. */
|
|
1357
|
-
async storeRunMemory(name, def, run, summary) {
|
|
1358
|
-
try {
|
|
1359
|
-
if (!(await this.orgMemoryUsable()))
|
|
1360
|
-
return;
|
|
1361
|
-
const { bridgeStoreEntry } = await import('../memory/memory-bridge.js');
|
|
1362
|
-
const when = summary.endedAt ? new Date(summary.endedAt).toISOString().slice(0, 10) : '';
|
|
1363
|
-
const lines = [
|
|
1364
|
-
`Org run ${run}${when ? ` (${when})` : ''} — goal: ${def.goal}`,
|
|
1365
|
-
summary.outcome
|
|
1366
|
-
? `Outcome: ${summary.outcome.status} — ${summary.outcome.summary}`
|
|
1367
|
-
: `Outcome: not recorded (${summary.messages} messages exchanged)`,
|
|
1368
|
-
summary.assets.length ? `Assets produced: ${summary.assets.slice(0, 10).join(', ')}` : '',
|
|
1369
|
-
summary.crashes.length ? `Crashed agents: ${summary.crashes.join(', ')}` : '',
|
|
1370
|
-
].filter(Boolean);
|
|
1371
|
-
await bridgeStoreEntry({
|
|
1372
|
-
key: `run-${run}`,
|
|
1373
|
-
value: lines.join('\n'),
|
|
1374
|
-
namespace: this.orgMemoryNamespace(name, def),
|
|
1375
|
-
dbPath: this.orgMemoryDbPath(),
|
|
1376
|
-
upsert: true,
|
|
1377
|
-
});
|
|
1378
|
-
// Heuristic KG fallback: if the coordinator never called org_learn this
|
|
1379
|
-
// run, extract lower-trust entities from the outcome summary so the
|
|
1380
|
-
// graph still accumulates something. LLM-quality extraction only comes
|
|
1381
|
-
// from org_learn (the agent's own session).
|
|
1382
|
-
if (!this.orgLearnedRuns.delete(`${name}:${run}`)) {
|
|
851
|
+
const { execSync } = await import('node:child_process');
|
|
852
|
+
if (org.worktreePath) {
|
|
1383
853
|
try {
|
|
1384
|
-
|
|
1385
|
-
const extracted = kg.heuristicExtract(lines.join('\n'), { sourceName: `run:${run}` });
|
|
1386
|
-
if (extracted.nodes.length) {
|
|
1387
|
-
await kg.kgIngest({ ...extracted, originRef: `run:${run}`, dbPath: this.orgMemoryDbPath() });
|
|
1388
|
-
}
|
|
854
|
+
execSync(`git worktree remove --force "${org.worktreePath}"`, { cwd: this.root, stdio: 'ignore', timeout: 30_000 });
|
|
1389
855
|
}
|
|
1390
|
-
catch { /* best
|
|
1391
|
-
}
|
|
1392
|
-
// Auto-rate the memories this run recalled — POSITIVE-ONLY: a failed run
|
|
1393
|
-
// proves nothing about the recalled memories (the failure may be entirely
|
|
1394
|
-
// unrelated), so failure never rates them down. Idempotent per run via
|
|
1395
|
-
// the feedback ledger, so a retried stopOrg can't double-apply.
|
|
1396
|
-
const used = this.recallUsage.get(name);
|
|
1397
|
-
this.recallUsage.delete(name);
|
|
1398
|
-
if (used?.size && summary.outcome?.status === 'achieved') {
|
|
1399
|
-
const { bridgeApplyFeedback } = await import('../memory/memory-bridge.js');
|
|
1400
|
-
await bridgeApplyFeedback({
|
|
1401
|
-
entryIds: [...used],
|
|
1402
|
-
score: 0.9,
|
|
1403
|
-
ledgerKey: `org-${name}-${run}`,
|
|
1404
|
-
dbPath: this.orgMemoryDbPath(),
|
|
1405
|
-
}).catch(() => { });
|
|
856
|
+
catch { /* best-effort */ }
|
|
1406
857
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
/** Resume a previous run from its checkpoint (runtime.json state). Reconstructs
|
|
1414
|
-
* the org's agents and mailboxes from the persisted state, enabling time-travel
|
|
1415
|
-
* debugging and run recovery after crashes. Returns the resumed RunningOrg or null.
|
|
1416
|
-
* Pattern 3: Full state restoration including mailbox queues, policy counters,
|
|
1417
|
-
* and session state with TTL and validation. */
|
|
1418
|
-
async resumeOrg(name) {
|
|
1419
|
-
const rtPath = join(this.root, ORG_DIR, name, 'runtime.json');
|
|
1420
|
-
if (!existsSync(rtPath)) {
|
|
1421
|
-
console.error('resumeOrg failed: runtime.json missing for', name);
|
|
1422
|
-
return null;
|
|
1423
|
-
}
|
|
1424
|
-
let rt;
|
|
1425
|
-
try {
|
|
1426
|
-
rt = JSON.parse(readFileSync(rtPath, 'utf8'));
|
|
1427
|
-
}
|
|
1428
|
-
catch (err) {
|
|
1429
|
-
console.error('resumeOrg failed: invalid JSON in runtime.json for', name, err instanceof Error ? err.message : err);
|
|
1430
|
-
return null;
|
|
1431
|
-
}
|
|
1432
|
-
// Allow resume from 'stopped' orgs - the checkpoint contains the running state to restore
|
|
1433
|
-
if (!rt?.run || !rt?.checkpoint) {
|
|
1434
|
-
console.error('resumeOrg failed: invalid runtime state for', name, 'status:', rt?.status, 'run:', rt?.run, 'checkpoint:', !!rt?.checkpoint);
|
|
1435
|
-
return null;
|
|
1436
|
-
}
|
|
1437
|
-
// Pattern 3: Checkpoint TTL validation - expire stale checkpoints
|
|
1438
|
-
if (rt.checkpoint && isCheckpointExpired(rt.checkpoint)) {
|
|
1439
|
-
console.error('resumeOrg failed: checkpoint expired for', name, 'updated:', rt.checkpoint.updated);
|
|
1440
|
-
return null;
|
|
1441
|
-
}
|
|
1442
|
-
// Pattern 3: Checksum validation - detect corrupted state
|
|
1443
|
-
if (rt.checkpoint && !validateCheckpoint(rt.checkpoint)) {
|
|
1444
|
-
console.error('resumeOrg failed: checkpoint validation failed for', name);
|
|
1445
|
-
return null;
|
|
1446
|
-
}
|
|
1447
|
-
// Load the org definition
|
|
1448
|
-
const defPath = join(this.root, ORG_DIR, `${name}.json`);
|
|
1449
|
-
if (!existsSync(defPath)) {
|
|
1450
|
-
console.error('resumeOrg failed: org definition missing for', name);
|
|
1451
|
-
return null;
|
|
1452
|
-
}
|
|
1453
|
-
const def = OrgDefSchema.parse(JSON.parse(readFileSync(defPath, 'utf8')));
|
|
1454
|
-
const dir = join(this.root, ORG_DIR, name, rt.run);
|
|
1455
|
-
// Check if run directory exists
|
|
1456
|
-
if (!existsSync(dir)) {
|
|
1457
|
-
console.error('resumeOrg failed: run directory missing', dir);
|
|
1458
|
-
return null;
|
|
1459
|
-
}
|
|
1460
|
-
// Reconstruct the bus from history
|
|
1461
|
-
const bus = new OrgBus(name, rt.run, dir);
|
|
1462
|
-
const MAX_COLLECTED = 1000;
|
|
1463
|
-
const collected = [];
|
|
1464
|
-
bus.subscribe(e => {
|
|
1465
|
-
const slim = e.data?.content != null
|
|
1466
|
-
? { ...e, data: { ...e.data, content: undefined } }
|
|
1467
|
-
: e;
|
|
1468
|
-
collected.push(slim);
|
|
1469
|
-
if (collected.length > MAX_COLLECTED)
|
|
1470
|
-
collected.splice(0, collected.length - MAX_COLLECTED);
|
|
1471
|
-
for (const fn of this.globalSubscribers)
|
|
1472
|
-
fn(e);
|
|
1473
|
-
});
|
|
1474
|
-
const running = { def, run: rt.run, bus, agents: new Map(), busEvents: () => [...collected] };
|
|
1475
|
-
// Pattern 3: Full checkpoint restoration - reconstruct agents with complete state
|
|
1476
|
-
if (rt.checkpoint) {
|
|
1477
|
-
const checkpoint = rt.checkpoint;
|
|
1478
|
-
// Reconstruct each role from checkpoint state
|
|
1479
|
-
for (const [roleId, roleState] of Object.entries(checkpoint.roleState)) {
|
|
1480
|
-
const role = def.roles.find(r => r.id === roleId);
|
|
1481
|
-
if (!role)
|
|
1482
|
-
continue; // Role no longer exists in org definition
|
|
1483
|
-
const mailbox = new Mailbox();
|
|
1484
|
-
const perRoleBudget = Math.floor((def.run_config.budget_tokens ?? 1_000_000) / def.roles.length);
|
|
1485
|
-
const policy = new PolicyEngine(roleId, { maxTokens: perRoleBudget, ...(role.policy ?? {}) }, bus, this.root);
|
|
1486
|
-
const runtime = {
|
|
1487
|
-
mailbox,
|
|
1488
|
-
policy,
|
|
1489
|
-
status: roleState.status,
|
|
1490
|
-
done: Promise.resolve(),
|
|
1491
|
-
metrics: { tokens: roleState.tokensUsed, costUsd: roleState.costUsd },
|
|
1492
|
-
lastMessageId: roleState.lastMessageId,
|
|
1493
|
-
error: roleState.error,
|
|
1494
|
-
};
|
|
1495
|
-
// Pattern 3: Restore mailbox queue content (not just closed state)
|
|
1496
|
-
if (roleState.mailboxQueue.length > 0) {
|
|
1497
|
-
restoreMailboxQueue(runtime, roleState.mailboxQueue);
|
|
1498
|
-
}
|
|
1499
|
-
// Pattern 3: Restore mailbox closed state
|
|
1500
|
-
if (roleState.mailboxClosed) {
|
|
1501
|
-
mailbox.close();
|
|
1502
|
-
}
|
|
1503
|
-
// Pattern 3: Restore policy usage counters
|
|
1504
|
-
if (roleState.tokensUsed > 0) {
|
|
1505
|
-
policy.setUsage(roleState.tokensUsed);
|
|
1506
|
-
}
|
|
1507
|
-
running.agents.set(roleId, runtime);
|
|
1508
|
-
}
|
|
1509
|
-
// Set up pending roles for lazy spawn (roles not in checkpoint)
|
|
1510
|
-
const reconstructedRoles = new Set(Object.keys(checkpoint.roleState));
|
|
1511
|
-
const pendingRoles = new Map();
|
|
1512
|
-
for (const role of def.roles) {
|
|
1513
|
-
if (!reconstructedRoles.has(role.id)) {
|
|
1514
|
-
pendingRoles.set(role.id, role);
|
|
858
|
+
for (const agent of org.agents.values()) {
|
|
859
|
+
if (agent.worktreePath) {
|
|
860
|
+
try {
|
|
861
|
+
execSync(`git worktree remove --force "${agent.worktreePath}"`, { cwd: this.root, stdio: 'ignore', timeout: 30_000 });
|
|
862
|
+
}
|
|
863
|
+
catch { /* best-effort */ }
|
|
1515
864
|
}
|
|
1516
865
|
}
|
|
1517
|
-
if (pendingRoles.size > 0) {
|
|
1518
|
-
running.pendingRoles = pendingRoles;
|
|
1519
|
-
}
|
|
1520
866
|
}
|
|
1521
|
-
|
|
1522
|
-
if (rt.abandonedRoles) {
|
|
1523
|
-
this.abandoned.set(name, new Set(rt.abandonedRoles));
|
|
1524
|
-
}
|
|
1525
|
-
this.orgs.set(name, running);
|
|
1526
|
-
bus.emit({ type: 'status', msg: `org resumed from checkpoint (${rt.run})` });
|
|
1527
|
-
this.persistState(name, 'running', rt.run);
|
|
1528
|
-
return running;
|
|
867
|
+
catch { /* node:child_process unavailable — skip */ }
|
|
1529
868
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
}
|
|
1536
|
-
const branchRun = `branch-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
1537
|
-
const branchDir = join(this.root, ORG_DIR, name, branchRun);
|
|
1538
|
-
try {
|
|
1539
|
-
mkdirSync(branchDir, { recursive: true });
|
|
1540
|
-
// Copy bus.jsonl to branch
|
|
1541
|
-
const busFile = join(runDir, 'bus.jsonl');
|
|
1542
|
-
if (existsSync(busFile)) {
|
|
1543
|
-
const busContent = readFileSync(busFile, 'utf8');
|
|
1544
|
-
writeFileSync(join(branchDir, 'bus.jsonl'), busContent);
|
|
1545
|
-
}
|
|
1546
|
-
// Create branch marker file
|
|
1547
|
-
writeFileSync(join(branchDir, '.branch-source'), JSON.stringify({ from: run, branchedAt: new Date().toISOString() }));
|
|
1548
|
-
return { ok: true, branchRun };
|
|
1549
|
-
}
|
|
1550
|
-
catch (err) {
|
|
1551
|
-
return { ok: false, error: `failed to create branch: ${err instanceof Error ? err.message : String(err)}` };
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
/** Record a structured decision trace for Rifft-style debugging */
|
|
1555
|
-
recordDecision(org, role, decision) {
|
|
1556
|
-
const running = this.orgs.get(org);
|
|
1557
|
-
if (!running)
|
|
1558
|
-
return;
|
|
1559
|
-
running.bus.emit({
|
|
1560
|
-
type: 'audit',
|
|
1561
|
-
from: role,
|
|
1562
|
-
reason: 'decision-trace',
|
|
1563
|
-
data: {
|
|
1564
|
-
decisionType: decision.type,
|
|
1565
|
-
context: decision.context,
|
|
1566
|
-
reasoning: decision.reasoning,
|
|
1567
|
-
alternatives: decision.alternatives,
|
|
1568
|
-
outcome: decision.outcome,
|
|
1569
|
-
ts: new Date().toISOString()
|
|
1570
|
-
},
|
|
1571
|
-
});
|
|
869
|
+
async stopAll() {
|
|
870
|
+
await Promise.all([
|
|
871
|
+
...[...this.orgs.keys()].map(n => this.stopOrg(n)),
|
|
872
|
+
...this.stopping.values(), // detached self-stops still flushing
|
|
873
|
+
]);
|
|
1572
874
|
}
|
|
1573
|
-
/**
|
|
1574
|
-
* `running` by every status path, so the absence was visible only as one
|
|
1575
|
-
* audit line in the log — a run went 40 minutes with no tester and nothing
|
|
1576
|
-
* said so. Persisted to runtime.json so `org status` can say it out loud. */
|
|
1577
|
-
abandoned = new Map();
|
|
875
|
+
/** @internal */
|
|
1578
876
|
persistState(name, status, run, org, checkpointOverride) {
|
|
1579
877
|
const p = join(this.root, ORG_DIR, name, 'runtime.json');
|
|
1580
878
|
const missing = [...(this.abandoned.get(name) ?? [])];
|
|
@@ -1586,11 +884,15 @@ export class OrgDaemon {
|
|
|
1586
884
|
if (running) {
|
|
1587
885
|
checkpoint = captureCheckpoint(running);
|
|
1588
886
|
}
|
|
1589
|
-
|
|
887
|
+
// C4: writeJsonFileAtomic (tmp + rename) — a direct writeFileSync here
|
|
888
|
+
// could leave runtime.json truncated on Ctrl-C during `org stop`, which
|
|
889
|
+
// would brick every subsequent `org status` / isOrgRunning / scheduler
|
|
890
|
+
// call. The state files in 6 other daemon paths already use this helper.
|
|
891
|
+
writeJsonFileAtomic(p, {
|
|
1590
892
|
status, run, pid: process.pid, updated: new Date().toISOString(),
|
|
1591
893
|
...(missing.length ? { abandonedRoles: missing } : {}),
|
|
1592
894
|
...(checkpoint ? { checkpoint } : {}),
|
|
1593
|
-
}
|
|
895
|
+
});
|
|
1594
896
|
}
|
|
1595
897
|
/** Mark every currently-running org as crashed in runtime.json.
|
|
1596
898
|
* Called from process-level crash handlers — must be synchronous and best-effort. */
|
|
@@ -1598,10 +900,12 @@ export class OrgDaemon {
|
|
|
1598
900
|
for (const [name, org] of this.orgs) {
|
|
1599
901
|
try {
|
|
1600
902
|
const p = join(this.root, ORG_DIR, name, 'runtime.json');
|
|
1601
|
-
|
|
903
|
+
// C4: atomic write — crash handler is the most likely place to hit
|
|
904
|
+
// a partial write since the process is mid-teardown.
|
|
905
|
+
writeJsonFileAtomic(p, {
|
|
1602
906
|
status: 'crashed', run: org.run, pid: process.pid,
|
|
1603
907
|
updated: new Date().toISOString(), closedBy: 'crash-handler',
|
|
1604
|
-
}
|
|
908
|
+
});
|
|
1605
909
|
}
|
|
1606
910
|
catch { /* best effort — filesystem may be unavailable */ }
|
|
1607
911
|
}
|
|
@@ -1615,10 +919,12 @@ export class OrgDaemon {
|
|
|
1615
919
|
try {
|
|
1616
920
|
const p = this.heartbeatPath();
|
|
1617
921
|
mkdirSync(join(this.root, '.monomind'), { recursive: true });
|
|
1618
|
-
|
|
922
|
+
// C4: atomic write — heartbeat corruption is how `org status` reports
|
|
923
|
+
// a phantom daemon after a crash.
|
|
924
|
+
writeJsonFileAtomic(p, {
|
|
1619
925
|
pid: process.pid, updatedAt: new Date().toISOString(),
|
|
1620
926
|
running: this.listRunning(),
|
|
1621
|
-
}
|
|
927
|
+
});
|
|
1622
928
|
}
|
|
1623
929
|
catch { /* best effort */ }
|
|
1624
930
|
}
|
|
@@ -1628,5 +934,89 @@ export class OrgDaemon {
|
|
|
1628
934
|
}
|
|
1629
935
|
catch { /* already gone or never written */ }
|
|
1630
936
|
}
|
|
937
|
+
// ── Delegated methods — extracted to focused modules ──────────────────
|
|
938
|
+
// approvals.ts
|
|
939
|
+
checkApproval(org, role, action) {
|
|
940
|
+
return approvalOps.checkApproval(this, org, role, action);
|
|
941
|
+
}
|
|
942
|
+
async setApproval(org, role, action, approved) {
|
|
943
|
+
return approvalOps.setApproval(this, org, role, action, approved);
|
|
944
|
+
}
|
|
945
|
+
// questions.ts
|
|
946
|
+
async askHuman(org, role, question) {
|
|
947
|
+
return questionOps.askHuman(this, org, role, question);
|
|
948
|
+
}
|
|
949
|
+
async answerQuestion(org, role, questionId, answer) {
|
|
950
|
+
return questionOps.answerQuestion(this, org, role, questionId, answer);
|
|
951
|
+
}
|
|
952
|
+
// decisions.ts
|
|
953
|
+
readGates(org) {
|
|
954
|
+
return decisionOps.readGates(this.root, org);
|
|
955
|
+
}
|
|
956
|
+
async createGate(org, role, name, description) {
|
|
957
|
+
return decisionOps.createGate(this, org, role, name, description);
|
|
958
|
+
}
|
|
959
|
+
async resolveGate(org, gateId, approved, resolution, resolvedBy) {
|
|
960
|
+
return decisionOps.resolveGate(this, org, gateId, approved, resolution, resolvedBy);
|
|
961
|
+
}
|
|
962
|
+
listGates(org, status) {
|
|
963
|
+
return decisionOps.listGates(this, org, status);
|
|
964
|
+
}
|
|
965
|
+
dagCreateTask(org, role, title, assignee, deps) {
|
|
966
|
+
return decisionOps.dagCreateTask(this, org, role, title, assignee, deps);
|
|
967
|
+
}
|
|
968
|
+
dagCompleteTask(org, role, taskId, result) {
|
|
969
|
+
return decisionOps.dagCompleteTask(this, org, role, taskId, result);
|
|
970
|
+
}
|
|
971
|
+
recordDecision(org, role, decision) {
|
|
972
|
+
decisionOps.recordDecision(this, org, role, decision);
|
|
973
|
+
}
|
|
974
|
+
// cross-org.ts
|
|
975
|
+
async deliver(fromOrg, fromRole, to, subject, body) {
|
|
976
|
+
return crossOrg.deliver(this, fromOrg, fromRole, to, subject, body);
|
|
977
|
+
}
|
|
978
|
+
receiveRemote(toOrg, toRole, fromQualified, subject, body) {
|
|
979
|
+
return crossOrg.receiveRemote(this, toOrg, toRole, fromQualified, subject, body);
|
|
980
|
+
}
|
|
981
|
+
mailBody(orgName, org, header, body, id) {
|
|
982
|
+
return crossOrg.mailBody(this.root, orgName, org, header, body, id);
|
|
983
|
+
}
|
|
984
|
+
// scheduler-integration.ts
|
|
985
|
+
/** @internal */
|
|
986
|
+
autoWake(name) { scheduler.autoWake(this, name); }
|
|
987
|
+
scheduleBossRestart(name) { scheduler.scheduleBossRestart(this, name); }
|
|
988
|
+
/** @internal */
|
|
989
|
+
scheduleDeferredSpawn(name, running, role, spawnRole) {
|
|
990
|
+
scheduler.scheduleDeferredSpawn(this, name, running, role, spawnRole);
|
|
991
|
+
}
|
|
992
|
+
// org-memory.ts
|
|
993
|
+
orgMemoryNamespace(name, def) { return orgMemory.orgMemoryNamespace(name, def); }
|
|
994
|
+
orgMemoryDbPath() { return orgMemory.orgMemoryDbPath(this.root); }
|
|
995
|
+
orgMemoryUsable() { return orgMemory.orgMemoryUsable(this.root); }
|
|
996
|
+
async rememberOrgMemory(name, def, role, content, scope, run) {
|
|
997
|
+
return orgMemory.rememberOrgMemory(this.root, name, def, role, content, scope, run);
|
|
998
|
+
}
|
|
999
|
+
async recallOrgMemory(name, def, query, role) {
|
|
1000
|
+
return orgMemory.recallOrgMemory(this, name, def, query, role);
|
|
1001
|
+
}
|
|
1002
|
+
async searchProjectKnowledge(query) {
|
|
1003
|
+
return orgMemory.searchProjectKnowledge(this.root, query);
|
|
1004
|
+
}
|
|
1005
|
+
async learnOrgKnowledge(name, run, payload) {
|
|
1006
|
+
return orgMemory.learnOrgKnowledge(this, name, run, payload);
|
|
1007
|
+
}
|
|
1008
|
+
async storeRunMemory(name, def, run, summary) {
|
|
1009
|
+
return orgMemory.storeRunMemory(this, name, def, run, summary);
|
|
1010
|
+
}
|
|
1011
|
+
// checkpoint-ops.ts
|
|
1012
|
+
async replayFrom(name, run) {
|
|
1013
|
+
return checkpointOps.replayFrom(this, name, run);
|
|
1014
|
+
}
|
|
1015
|
+
async resumeOrg(name) {
|
|
1016
|
+
return checkpointOps.resumeOrg(this, name);
|
|
1017
|
+
}
|
|
1018
|
+
branchCheckpoint(name, run, branchName) {
|
|
1019
|
+
return checkpointOps.branchCheckpoint(this, name, run, branchName);
|
|
1020
|
+
}
|
|
1631
1021
|
}
|
|
1632
1022
|
//# sourceMappingURL=daemon.js.map
|