@monoes/monomindcli 1.14.7 → 1.15.0
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/agents/reengineer-squad/boss.md +113 -0
- package/.claude/agents/reengineer-squad/critic-architect.md +132 -0
- package/.claude/agents/reengineer-squad/git-manager.md +145 -0
- package/.claude/agents/reengineer-squad/idea-generator.md +95 -0
- package/.claude/agents/reengineer-squad/implementer.md +112 -0
- package/.claude/agents/reengineer-squad/integration-planner.md +112 -0
- package/.claude/agents/reengineer-squad/source-analyst.md +103 -0
- package/.claude/agents/reengineer-squad/target-analyst.md +118 -0
- package/.claude/agents/reengineer-squad/tester.md +105 -0
- package/.claude/commands/mastermind/master.md +35 -14
- package/.claude/helpers/handlers/capture-handler.cjs +155 -18
- package/.claude/helpers/monolean-activate.cjs +20 -0
- package/.claude/helpers/monolean-config.cjs +76 -0
- package/.claude/helpers/monolean-instructions.cjs +109 -0
- package/.claude/helpers/monolean-propagate.cjs +9 -0
- package/.claude/helpers/monolean-tracker.cjs +18 -0
- package/.claude/helpers/skill-registry.json +2 -2
- package/.claude/skills/agent-browser-testing/SKILL.md +301 -18
- package/.claude/skills/mastermind/runorg.md +69 -23
- package/.claude/skills/monodesign/SKILL.md +32 -1
- package/.claude/skills/monodesign/adapt.md +53 -0
- package/.claude/skills/monodesign/agents/monodesign-asset-producer.md +100 -0
- package/.claude/skills/monodesign/animate.md +65 -0
- package/.claude/skills/monodesign/audit.md +89 -0
- package/.claude/skills/monodesign/bolder.md +50 -0
- package/.claude/skills/monodesign/clarify.md +64 -0
- package/.claude/skills/monodesign/colorize.md +68 -0
- package/.claude/skills/monodesign/craft.md +51 -0
- package/.claude/skills/monodesign/critique.md +66 -0
- package/.claude/skills/monodesign/delight.md +47 -0
- package/.claude/skills/monodesign/distill.md +56 -0
- package/.claude/skills/monodesign/document.md +80 -0
- package/.claude/skills/monodesign/extract.md +74 -0
- package/.claude/skills/monodesign/harden.md +65 -0
- package/.claude/skills/monodesign/live.md +59 -0
- package/.claude/skills/monodesign/onboard.md +50 -0
- package/.claude/skills/monodesign/optimize.md +64 -0
- package/.claude/skills/monodesign/overdrive.md +56 -0
- package/.claude/skills/monodesign/polish.md +68 -0
- package/.claude/skills/monodesign/quieter.md +57 -0
- package/.claude/skills/monodesign/reference/antipatterns-catalog.md +248 -76
- package/.claude/skills/monodesign/reference/codex.md +107 -0
- package/.claude/skills/monodesign/reference/craft.md +3 -0
- package/.claude/skills/monodesign/reference/hooks.md +99 -0
- package/.claude/skills/monodesign/reference/image-prompts.md +12 -0
- package/.claude/skills/monodesign/shape.md +71 -0
- package/.claude/skills/monodesign/teach.md +69 -0
- package/.claude/skills/monodesign/typeset.md +59 -0
- package/.claude/skills/monolean/SKILL.md +118 -0
- package/.claude/skills/monolean-audit/SKILL.md +41 -0
- package/.claude/skills/monolean-debt/SKILL.md +46 -0
- package/.claude/skills/monolean-help/SKILL.md +60 -0
- package/.claude/skills/monolean-review/SKILL.md +57 -0
- package/bin/cli.js +3 -1
- package/dist/dashboard/server.js +137 -0
- package/dist/src/__tests__/browse-adapters.test.d.ts +2 -0
- package/dist/src/__tests__/browse-adapters.test.d.ts.map +1 -0
- package/dist/src/__tests__/browse-adapters.test.js +51 -0
- package/dist/src/__tests__/browse-adapters.test.js.map +1 -0
- package/dist/src/__tests__/browse-analyzer.test.d.ts +2 -0
- package/dist/src/__tests__/browse-analyzer.test.d.ts.map +1 -0
- package/dist/src/__tests__/browse-analyzer.test.js +68 -0
- package/dist/src/__tests__/browse-analyzer.test.js.map +1 -0
- package/dist/src/__tests__/browse-builtin-handlers.test.d.ts +2 -0
- package/dist/src/__tests__/browse-builtin-handlers.test.d.ts.map +1 -0
- package/dist/src/__tests__/browse-builtin-handlers.test.js +139 -0
- package/dist/src/__tests__/browse-builtin-handlers.test.js.map +1 -0
- package/dist/src/__tests__/browse-cdp.test.d.ts +2 -0
- package/dist/src/__tests__/browse-cdp.test.d.ts.map +1 -0
- package/dist/src/__tests__/browse-cdp.test.js +169 -0
- package/dist/src/__tests__/browse-cdp.test.js.map +1 -0
- package/dist/src/__tests__/browse-dashboard.test.d.ts +2 -0
- package/dist/src/__tests__/browse-dashboard.test.d.ts.map +1 -0
- package/dist/src/__tests__/browse-dashboard.test.js +179 -0
- package/dist/src/__tests__/browse-dashboard.test.js.map +1 -0
- package/dist/src/__tests__/browse-engine.test.d.ts +2 -0
- package/dist/src/__tests__/browse-engine.test.d.ts.map +1 -0
- package/dist/src/__tests__/browse-engine.test.js +122 -0
- package/dist/src/__tests__/browse-engine.test.js.map +1 -0
- package/dist/src/__tests__/browse-expression.test.d.ts +2 -0
- package/dist/src/__tests__/browse-expression.test.d.ts.map +1 -0
- package/dist/src/__tests__/browse-expression.test.js +54 -0
- package/dist/src/__tests__/browse-expression.test.js.map +1 -0
- package/dist/src/__tests__/browse-store.test.d.ts +2 -0
- package/dist/src/__tests__/browse-store.test.d.ts.map +1 -0
- package/dist/src/__tests__/browse-store.test.js +99 -0
- package/dist/src/__tests__/browse-store.test.js.map +1 -0
- package/dist/src/__tests__/browse-workflow-types.test.d.ts +2 -0
- package/dist/src/__tests__/browse-workflow-types.test.d.ts.map +1 -0
- package/dist/src/__tests__/browse-workflow-types.test.js +33 -0
- package/dist/src/__tests__/browse-workflow-types.test.js.map +1 -0
- package/dist/src/browser/action-builder/analyzer.d.ts +11 -0
- package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -0
- package/dist/src/browser/action-builder/analyzer.js +71 -0
- package/dist/src/browser/action-builder/analyzer.js.map +1 -0
- package/dist/src/browser/action-builder/types.d.ts +47 -0
- package/dist/src/browser/action-builder/types.d.ts.map +1 -0
- package/dist/src/browser/action-builder/types.js +2 -0
- package/dist/src/browser/action-builder/types.js.map +1 -0
- package/dist/src/browser/adapters/gemini.d.ts +3 -0
- package/dist/src/browser/adapters/gemini.d.ts.map +1 -0
- package/dist/src/browser/adapters/gemini.js +16 -0
- package/dist/src/browser/adapters/gemini.js.map +1 -0
- package/dist/src/browser/adapters/google.d.ts +3 -0
- package/dist/src/browser/adapters/google.d.ts.map +1 -0
- package/dist/src/browser/adapters/google.js +17 -0
- package/dist/src/browser/adapters/google.js.map +1 -0
- package/dist/src/browser/adapters/index.d.ts +19 -0
- package/dist/src/browser/adapters/index.d.ts.map +1 -0
- package/dist/src/browser/adapters/index.js +23 -0
- package/dist/src/browser/adapters/index.js.map +1 -0
- package/dist/src/browser/adapters/instagram.d.ts +3 -0
- package/dist/src/browser/adapters/instagram.d.ts.map +1 -0
- package/dist/src/browser/adapters/instagram.js +17 -0
- package/dist/src/browser/adapters/instagram.js.map +1 -0
- package/dist/src/browser/adapters/linkedin.d.ts +3 -0
- package/dist/src/browser/adapters/linkedin.d.ts.map +1 -0
- package/dist/src/browser/adapters/linkedin.js +19 -0
- package/dist/src/browser/adapters/linkedin.js.map +1 -0
- package/dist/src/browser/adapters/microsoft.d.ts +3 -0
- package/dist/src/browser/adapters/microsoft.d.ts.map +1 -0
- package/dist/src/browser/adapters/microsoft.js +16 -0
- package/dist/src/browser/adapters/microsoft.js.map +1 -0
- package/dist/src/browser/adapters/x.d.ts +3 -0
- package/dist/src/browser/adapters/x.d.ts.map +1 -0
- package/dist/src/browser/adapters/x.js +19 -0
- package/dist/src/browser/adapters/x.js.map +1 -0
- package/dist/src/browser/dashboard/api-types.d.ts +50 -0
- package/dist/src/browser/dashboard/api-types.d.ts.map +1 -0
- package/dist/src/browser/dashboard/api-types.js +14 -0
- package/dist/src/browser/dashboard/api-types.js.map +1 -0
- package/dist/src/browser/dashboard/server.d.ts +9 -0
- package/dist/src/browser/dashboard/server.d.ts.map +1 -0
- package/dist/src/browser/dashboard/server.js +62 -0
- package/dist/src/browser/dashboard/server.js.map +1 -0
- package/dist/src/browser/dashboard/ui.html +1811 -0
- package/dist/src/browser/workflow/builtin-handlers.d.ts +3 -0
- package/dist/src/browser/workflow/builtin-handlers.d.ts.map +1 -0
- package/dist/src/browser/workflow/builtin-handlers.js +343 -0
- package/dist/src/browser/workflow/builtin-handlers.js.map +1 -0
- package/dist/src/browser/workflow/engine.d.ts +15 -0
- package/dist/src/browser/workflow/engine.d.ts.map +1 -0
- package/dist/src/browser/workflow/engine.js +127 -0
- package/dist/src/browser/workflow/engine.js.map +1 -0
- package/dist/src/browser/workflow/expression.d.ts +4 -0
- package/dist/src/browser/workflow/expression.d.ts.map +1 -0
- package/dist/src/browser/workflow/expression.js +64 -0
- package/dist/src/browser/workflow/expression.js.map +1 -0
- package/dist/src/browser/workflow/store.d.ts +24 -0
- package/dist/src/browser/workflow/store.d.ts.map +1 -0
- package/dist/src/browser/workflow/store.js +145 -0
- package/dist/src/browser/workflow/store.js.map +1 -0
- package/dist/src/browser/workflow/types.d.ts +48 -0
- package/dist/src/browser/workflow/types.d.ts.map +1 -0
- package/dist/src/browser/workflow/types.js +2 -0
- package/dist/src/browser/workflow/types.js.map +1 -0
- package/dist/src/commands/browse-action.d.ts +4 -0
- package/dist/src/commands/browse-action.d.ts.map +1 -0
- package/dist/src/commands/browse-action.js +151 -0
- package/dist/src/commands/browse-action.js.map +1 -0
- package/dist/src/commands/browse-platform.d.ts +4 -0
- package/dist/src/commands/browse-platform.d.ts.map +1 -0
- package/dist/src/commands/browse-platform.js +117 -0
- package/dist/src/commands/browse-platform.js.map +1 -0
- package/dist/src/commands/browse-workflow.d.ts +4 -0
- package/dist/src/commands/browse-workflow.d.ts.map +1 -0
- package/dist/src/commands/browse-workflow.js +153 -0
- package/dist/src/commands/browse-workflow.js.map +1 -0
- package/dist/src/commands/browse.d.ts +10 -6
- package/dist/src/commands/browse.d.ts.map +1 -1
- package/dist/src/commands/browse.js +11 -2154
- package/dist/src/commands/browse.js.map +1 -1
- package/dist/src/commands/design-detect.d.ts +21 -0
- package/dist/src/commands/design-detect.d.ts.map +1 -0
- package/dist/src/commands/design-detect.js +127 -0
- package/dist/src/commands/design-detect.js.map +1 -0
- package/dist/src/commands/design-palette.d.ts +22 -0
- package/dist/src/commands/design-palette.d.ts.map +1 -0
- package/dist/src/commands/design-palette.js +539 -0
- package/dist/src/commands/design-palette.js.map +1 -0
- package/dist/src/commands/hooks-core-commands.d.ts +10 -0
- package/dist/src/commands/hooks-core-commands.d.ts.map +1 -0
- package/dist/src/commands/hooks-core-commands.js +377 -0
- package/dist/src/commands/hooks-core-commands.js.map +1 -0
- package/dist/src/commands/hooks-coverage-commands.d.ts +12 -0
- package/dist/src/commands/hooks-coverage-commands.d.ts.map +1 -0
- package/dist/src/commands/hooks-coverage-commands.js +1217 -0
- package/dist/src/commands/hooks-coverage-commands.js.map +1 -0
- package/dist/src/commands/hooks-coverage-utils.d.ts +42 -0
- package/dist/src/commands/hooks-coverage-utils.d.ts.map +1 -0
- package/dist/src/commands/hooks-coverage-utils.js +220 -0
- package/dist/src/commands/hooks-coverage-utils.js.map +1 -0
- package/dist/src/commands/hooks-extended-commands.d.ts +14 -0
- package/dist/src/commands/hooks-extended-commands.d.ts.map +1 -0
- package/dist/src/commands/hooks-extended-commands.js +579 -0
- package/dist/src/commands/hooks-extended-commands.js.map +1 -0
- package/dist/src/commands/hooks-formatting.d.ts +13 -0
- package/dist/src/commands/hooks-formatting.d.ts.map +1 -0
- package/dist/src/commands/hooks-formatting.js +42 -0
- package/dist/src/commands/hooks-formatting.js.map +1 -0
- package/dist/src/commands/hooks-routing-commands.d.ts +15 -0
- package/dist/src/commands/hooks-routing-commands.d.ts.map +1 -0
- package/dist/src/commands/hooks-routing-commands.js +723 -0
- package/dist/src/commands/hooks-routing-commands.js.map +1 -0
- package/dist/src/commands/hooks-workers.d.ts +9 -0
- package/dist/src/commands/hooks-workers.d.ts.map +1 -0
- package/dist/src/commands/hooks-workers.js +782 -0
- package/dist/src/commands/hooks-workers.js.map +1 -0
- package/dist/src/commands/hooks.d.ts +8 -0
- package/dist/src/commands/hooks.d.ts.map +1 -1
- package/dist/src/commands/hooks.js +179 -4103
- package/dist/src/commands/hooks.js.map +1 -1
- package/dist/src/commands/index.d.ts +1 -0
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +6 -0
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/org.d.ts.map +1 -1
- package/dist/src/commands/org.js +14 -15
- package/dist/src/commands/org.js.map +1 -1
- package/dist/src/commands/tokens.d.ts.map +1 -1
- package/dist/src/commands/tokens.js +77 -1
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/init/executor.d.ts.map +1 -1
- package/dist/src/init/executor.js +18 -8
- package/dist/src/init/executor.js.map +1 -1
- package/dist/src/init/settings-generator.d.ts.map +1 -1
- package/dist/src/init/settings-generator.js +39 -5
- package/dist/src/init/settings-generator.js.map +1 -1
- package/dist/src/init/statusline-generator.d.ts.map +1 -1
- package/dist/src/init/statusline-generator.js +25 -5
- package/dist/src/init/statusline-generator.js.map +1 -1
- package/dist/src/mcp-tools/browser-tools.d.ts +3 -5
- package/dist/src/mcp-tools/browser-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/browser-tools.js +619 -326
- package/dist/src/mcp-tools/browser-tools.js.map +1 -1
- package/dist/src/mcp-tools/hooks-embedding.d.ts +161 -0
- package/dist/src/mcp-tools/hooks-embedding.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-embedding.js +506 -0
- package/dist/src/mcp-tools/hooks-embedding.js.map +1 -0
- package/dist/src/mcp-tools/hooks-intelligence.d.ts +26 -0
- package/dist/src/mcp-tools/hooks-intelligence.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-intelligence.js +1328 -0
- package/dist/src/mcp-tools/hooks-intelligence.js.map +1 -0
- package/dist/src/mcp-tools/hooks-routing.d.ts +27 -0
- package/dist/src/mcp-tools/hooks-routing.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-routing.js +1591 -0
- package/dist/src/mcp-tools/hooks-routing.js.map +1 -0
- package/dist/src/mcp-tools/hooks-tools.d.ts +3 -38
- package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-tools.js +5 -3393
- package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
- package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/monograph-tools.js +24 -14
- package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
- package/dist/src/mcp-tools/workflow-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/workflow-tools.js +54 -1
- package/dist/src/mcp-tools/workflow-tools.js.map +1 -1
- package/dist/src/memory/embedding-operations.d.ts +58 -0
- package/dist/src/memory/embedding-operations.d.ts.map +1 -0
- package/dist/src/memory/embedding-operations.js +299 -0
- package/dist/src/memory/embedding-operations.js.map +1 -0
- package/dist/src/memory/ewc-consolidation.d.ts.map +1 -1
- package/dist/src/memory/ewc-consolidation.js +37 -3
- package/dist/src/memory/ewc-consolidation.js.map +1 -1
- package/dist/src/memory/hnsw-operations.d.ts +130 -0
- package/dist/src/memory/hnsw-operations.d.ts.map +1 -0
- package/dist/src/memory/hnsw-operations.js +400 -0
- package/dist/src/memory/hnsw-operations.js.map +1 -0
- package/dist/src/memory/intelligence.d.ts.map +1 -1
- package/dist/src/memory/intelligence.js +42 -23
- package/dist/src/memory/intelligence.js.map +1 -1
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +52 -8
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/memory/memory-crud.d.ts +67 -0
- package/dist/src/memory/memory-crud.d.ts.map +1 -0
- package/dist/src/memory/memory-crud.js +415 -0
- package/dist/src/memory/memory-crud.js.map +1 -0
- package/dist/src/memory/memory-initializer.d.ts +9 -322
- package/dist/src/memory/memory-initializer.d.ts.map +1 -1
- package/dist/src/memory/memory-initializer.js +17 -1794
- package/dist/src/memory/memory-initializer.js.map +1 -1
- package/dist/src/memory/memory-migrations.d.ts +30 -0
- package/dist/src/memory/memory-migrations.d.ts.map +1 -0
- package/dist/src/memory/memory-migrations.js +134 -0
- package/dist/src/memory/memory-migrations.js.map +1 -0
- package/dist/src/memory/memory-read.d.ts +78 -0
- package/dist/src/memory/memory-read.d.ts.map +1 -0
- package/dist/src/memory/memory-read.js +331 -0
- package/dist/src/memory/memory-read.js.map +1 -0
- package/dist/src/memory/memory-schema.d.ts +13 -0
- package/dist/src/memory/memory-schema.d.ts.map +1 -0
- package/dist/src/memory/memory-schema.js +167 -0
- package/dist/src/memory/memory-schema.js.map +1 -0
- package/dist/src/memory/sona-optimizer.d.ts.map +1 -1
- package/dist/src/memory/sona-optimizer.js +37 -4
- package/dist/src/memory/sona-optimizer.js.map +1 -1
- package/dist/src/monovector/route-outcomes.d.ts.map +1 -1
- package/dist/src/monovector/route-outcomes.js +16 -6
- package/dist/src/monovector/route-outcomes.js.map +1 -1
- package/dist/src/pricing/model-pricing.d.ts +41 -0
- package/dist/src/pricing/model-pricing.d.ts.map +1 -0
- package/dist/src/pricing/model-pricing.js +61 -0
- package/dist/src/pricing/model-pricing.js.map +1 -0
- package/dist/src/ui/.monomind/capture/active-run.json +1 -0
- package/dist/src/ui/.monomind/orgs/system-trial-qa/runs/real-events-1782290897.convs.jsonl +3 -0
- package/dist/src/ui/.monomind/orgs/system-trial-qa/runs/real-events-1782290897.jsonl +11 -0
- package/dist/src/ui/.monomind/orgs/system-trial-qa/runs/rigid-qa-restart-1782288201.jsonl +540 -0
- package/dist/src/ui/.monomind/orgs/system-trial-qa-threads.jsonl +3 -0
- package/dist/src/ui/.monomind/orgs/test-event-fix/runs/rigid-qa-restart-1782288201.jsonl +2 -0
- package/dist/src/ui/MODULARIZATION_PLAN.md +79 -0
- package/dist/src/ui/collector.mjs +23 -13
- package/dist/src/ui/dashboard.html +1652 -13
- package/dist/src/ui/data/known-projects.json +1 -0
- package/dist/src/ui/data/mastermind-events.jsonl +553 -0
- package/dist/src/ui/data/sessions/_index.json +1 -0
- package/dist/src/ui/data/sessions/final-sess-001.jsonl +542 -0
- package/dist/src/ui/data/unknown-events.jsonl +1 -0
- package/dist/src/ui/orgs.html +154 -10
- package/dist/src/ui/server.mjs +1131 -168
- package/dist/src/ui/sse-manager.mjs +119 -0
- package/dist/src/update/checker.js +1 -1
- package/dist/src/update/checker.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/workflow/builtin-handlers.js +321 -0
- package/dist/workflow/engine.js +253 -0
- package/dist/workflow/expression.js +98 -0
- package/dist/workflow/types.js +2 -0
- package/package.json +8 -5
package/dist/src/ui/server.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import os from 'os';
|
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { createRequire } from 'module';
|
|
7
7
|
import { collectAll, getWatchPaths, collectProject, collectSessions, collectSwarm, collectSwarmHistory, appendSwarmHistory, collectSwarmEvents, getSwarmDataSize, cleanSwarmData, collectAgents, collectTokens, collectHooks, collectKnowledge, collectMetrics, collectMemory, collectMemoryFiles, collectSystem } from './collector.mjs';
|
|
8
|
+
import { addSseClient, removeSseClient, broadcast, getSseClientCount, closeSseClients, addMmClient, removeMmClient, broadcastMm, getMmClientCount } from './sse-manager.mjs';
|
|
8
9
|
|
|
9
10
|
const JSONL_SIZE_CAP = 10 * 1024 * 1024; // 10 MB — skip files larger than this in /api/graph
|
|
10
11
|
const buildDocsState = new Map();
|
|
@@ -179,14 +180,96 @@ function pathToSections(filename) {
|
|
|
179
180
|
return ['sessions', 'swarm', 'agents', 'tokens', 'hooks'];
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
// SSE client registry
|
|
183
|
-
const sseClients = new Set();
|
|
184
|
-
// Mastermind real-time event stream clients
|
|
185
|
-
const mmSseClients = new Set();
|
|
183
|
+
// SSE client registry and mastermind SSE clients are managed by sse-manager.mjs
|
|
186
184
|
// Active org run tracking: org -> runId (enables event routing for orgs without runId in payload)
|
|
187
185
|
const activeOrgRuns = new Map();
|
|
188
186
|
// Active session tracking: org -> {sessionId, ts} (enables linking agent events to sessions)
|
|
189
187
|
const activeSessionsByOrg = new Map();
|
|
188
|
+
// Phase 3: Per-org SSE clients for run streaming tail endpoint
|
|
189
|
+
const runStreamClients = new Map(); // orgName → Set<res>
|
|
190
|
+
|
|
191
|
+
// Design doc Issue 2: concurrent write safety. Since server.mjs is the sole writer
|
|
192
|
+
// (all hook processes POST via HTTP), in-process serialization is sufficient.
|
|
193
|
+
// SQLite WAL (Issue 2 Phase 1.5): run events are indexed in an in-memory sql.js database
|
|
194
|
+
// with WAL mode and persisted to .monomind/run-events.db every 1000ms. JSONL files are
|
|
195
|
+
// still written (bash lifecycle scripts write them directly), but SQLite is the query layer
|
|
196
|
+
// for streaming tail replay and startup gap-fill.
|
|
197
|
+
//
|
|
198
|
+
// Serializing write queue — prevents concurrent JSONL corruption (Issue 2 from design doc)
|
|
199
|
+
const _writeQueue = new Map(); // filePath → Promise (in-flight write)
|
|
200
|
+
|
|
201
|
+
// ── sql.js WAL run-event index (Phase 1.5) ──────────────────────────────────
|
|
202
|
+
let _runDb = null; // sql.js in-memory Database
|
|
203
|
+
let _runDbPath = null; // disk path for persistence
|
|
204
|
+
let _runDbPersistTimer = null;
|
|
205
|
+
let _runDbInsertStmt = null; // prepared INSERT statement
|
|
206
|
+
|
|
207
|
+
const _require = createRequire(import.meta.url);
|
|
208
|
+
|
|
209
|
+
async function _initRunDb(monoHome) {
|
|
210
|
+
try {
|
|
211
|
+
const initSqlJs = _require('sql.js');
|
|
212
|
+
const SQL = await initSqlJs();
|
|
213
|
+
_runDbPath = path.join(monoHome, '.monomind', 'run-events.db');
|
|
214
|
+
fs.mkdirSync(path.dirname(_runDbPath), { recursive: true });
|
|
215
|
+
let fileData;
|
|
216
|
+
try { fileData = fs.readFileSync(_runDbPath); } catch (_) {}
|
|
217
|
+
_runDb = fileData ? new SQL.Database(fileData) : new SQL.Database();
|
|
218
|
+
_runDb.run('PRAGMA journal_mode=WAL');
|
|
219
|
+
_runDb.run('PRAGMA synchronous=NORMAL');
|
|
220
|
+
_runDb.run(`CREATE TABLE IF NOT EXISTS run_events (
|
|
221
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
222
|
+
org TEXT NOT NULL,
|
|
223
|
+
run_id TEXT NOT NULL,
|
|
224
|
+
type TEXT NOT NULL,
|
|
225
|
+
raw TEXT NOT NULL,
|
|
226
|
+
ts INTEGER NOT NULL,
|
|
227
|
+
source TEXT DEFAULT 'http',
|
|
228
|
+
UNIQUE(org, run_id, ts, type, raw)
|
|
229
|
+
)`);
|
|
230
|
+
_runDb.run('CREATE INDEX IF NOT EXISTS idx_re_org_id ON run_events(org, id)');
|
|
231
|
+
_runDb.run('CREATE INDEX IF NOT EXISTS idx_re_ts ON run_events(ts)');
|
|
232
|
+
_runDbInsertStmt = _runDb.prepare(
|
|
233
|
+
'INSERT OR IGNORE INTO run_events (org, run_id, type, raw, ts, source) VALUES (?,?,?,?,?,?)'
|
|
234
|
+
);
|
|
235
|
+
// Compact old events at startup: keep last 30 days
|
|
236
|
+
const _cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
237
|
+
_runDb.run('DELETE FROM run_events WHERE ts < ?', [_cutoff]);
|
|
238
|
+
_persistRunDb();
|
|
239
|
+
} catch (_) {
|
|
240
|
+
_runDb = null; // graceful fallback — JSONL path continues to work
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function _persistRunDb() {
|
|
245
|
+
if (!_runDb || !_runDbPath) return;
|
|
246
|
+
clearTimeout(_runDbPersistTimer);
|
|
247
|
+
_runDbPersistTimer = setTimeout(() => {
|
|
248
|
+
try { fs.writeFileSync(_runDbPath, Buffer.from(_runDb.export())); } catch (_) {}
|
|
249
|
+
}, 1000);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function _insertRunEvent(ev, source) {
|
|
253
|
+
if (!_runDb || !_runDbInsertStmt) return;
|
|
254
|
+
try {
|
|
255
|
+
const org = String(ev.org || '').trim();
|
|
256
|
+
const runId = String(ev.runId || '').trim();
|
|
257
|
+
if (!org || !runId) return;
|
|
258
|
+
_runDbInsertStmt.run([org, runId, String(ev.type || ''), JSON.stringify(ev), Number(ev.ts || Date.now()), source || 'http']);
|
|
259
|
+
_persistRunDb();
|
|
260
|
+
} catch (_) {}
|
|
261
|
+
}
|
|
262
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
function appendToFile(filePath, line) {
|
|
265
|
+
const prev = _writeQueue.get(filePath) || Promise.resolve();
|
|
266
|
+
const next = prev.then(() => {
|
|
267
|
+
try { fs.appendFileSync(filePath, line); } catch (_) {}
|
|
268
|
+
});
|
|
269
|
+
_writeQueue.set(filePath, next);
|
|
270
|
+
next.then(() => { if (_writeQueue.get(filePath) === next) _writeQueue.delete(filePath); });
|
|
271
|
+
return next;
|
|
272
|
+
}
|
|
190
273
|
|
|
191
274
|
// Returns the shared git directory parent so run files survive branch switches and
|
|
192
275
|
// are shared across all worktrees. In a worktree, .git is a FILE pointing to the
|
|
@@ -221,6 +304,124 @@ function _getGitMonomindDir(workDir) {
|
|
|
221
304
|
return result;
|
|
222
305
|
}
|
|
223
306
|
|
|
307
|
+
// Returns the monomind home directory for server-level data (capture, control.json, loops).
|
|
308
|
+
// Priority: MONOMIND_HOME env var > walk up from cwd finding .monomind/control.json > cwd fallback
|
|
309
|
+
function getMonomindHome() {
|
|
310
|
+
if (process.env.MONOMIND_HOME) return path.resolve(process.env.MONOMIND_HOME);
|
|
311
|
+
let dir = process.cwd();
|
|
312
|
+
while (dir !== path.dirname(dir)) {
|
|
313
|
+
if (fs.existsSync(path.join(dir, '.monomind', 'control.json'))) return dir;
|
|
314
|
+
dir = path.dirname(dir);
|
|
315
|
+
}
|
|
316
|
+
return process.cwd();
|
|
317
|
+
}
|
|
318
|
+
const MONOMIND_HOME = getMonomindHome();
|
|
319
|
+
|
|
320
|
+
// Resolve an org's project directory by searching across known projects.
|
|
321
|
+
// Returns the first project dir where {dir}/.monomind/orgs/{orgName}.json exists, or null.
|
|
322
|
+
function _resolveOrgProjectDir(orgName, serverRoot) {
|
|
323
|
+
const dirs = new Set([serverRoot]);
|
|
324
|
+
try {
|
|
325
|
+
const kf = path.join(serverRoot, 'data', 'known-projects.json');
|
|
326
|
+
if (fs.existsSync(kf)) JSON.parse(fs.readFileSync(kf, 'utf8')).forEach(p => dirs.add(p));
|
|
327
|
+
} catch(_) {}
|
|
328
|
+
for (const d of dirs) {
|
|
329
|
+
if (fs.existsSync(path.join(d, '.monomind', 'orgs', `${orgName}.json`))) return d;
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Org run state helpers ────────────────────────────────────────────────
|
|
335
|
+
// Reads {name}-runstate.json from disk. Returns null if missing/corrupt.
|
|
336
|
+
function _readRunState(orgName, rootDir) {
|
|
337
|
+
const projDir = _resolveOrgProjectDir(orgName, rootDir) || rootDir;
|
|
338
|
+
const base = _getGitMonomindDir(projDir) || path.join(projDir, '.monomind');
|
|
339
|
+
const file = path.join(base, 'orgs', `${orgName}-runstate.json`);
|
|
340
|
+
if (!fs.existsSync(file)) return null;
|
|
341
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { return null; }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Returns the current runId from runstate (for events that omit it after restart).
|
|
345
|
+
function _getActiveRunId(orgName, rootDir) {
|
|
346
|
+
return _readRunState(orgName, rootDir)?.runId || null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Returns all project dirs allowed for artifact reads (serverRoot + known-projects.json).
|
|
350
|
+
function _getAllowedArtifactDirs(serverRoot) {
|
|
351
|
+
const dirs = [path.resolve(serverRoot)];
|
|
352
|
+
try {
|
|
353
|
+
const kf = path.join(serverRoot, 'data', 'known-projects.json');
|
|
354
|
+
if (fs.existsSync(kf)) JSON.parse(fs.readFileSync(kf, 'utf8')).forEach(p => dirs.push(path.resolve(p)));
|
|
355
|
+
} catch (_) {}
|
|
356
|
+
return dirs;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Detects a basic mime type from file extension for artifact responses.
|
|
360
|
+
function _detectMimeType(filePath) {
|
|
361
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
362
|
+
const map = { '.ts': 'text/typescript', '.js': 'text/javascript', '.mjs': 'text/javascript',
|
|
363
|
+
'.json': 'application/json', '.md': 'text/markdown', '.txt': 'text/plain',
|
|
364
|
+
'.html': 'text/html', '.css': 'text/css', '.py': 'text/x-python',
|
|
365
|
+
'.sh': 'text/x-shellscript', '.yaml': 'text/yaml', '.yml': 'text/yaml',
|
|
366
|
+
'.toml': 'text/plain', '.env': 'text/plain', '.xml': 'text/xml' };
|
|
367
|
+
return map[ext] || 'application/octet-stream';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Writes runstate.json for state-changing events. Debounces lastEventAt for frequent events.
|
|
371
|
+
const _runstateDebouncers = new Map();
|
|
372
|
+
function _updateRunState(event, rootDir) {
|
|
373
|
+
const orgName = String(event.org || '').trim().replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
374
|
+
if (!orgName) return;
|
|
375
|
+
const projDir = _resolveOrgProjectDir(orgName, rootDir) || rootDir;
|
|
376
|
+
const base = _getGitMonomindDir(projDir) || path.join(projDir, '.monomind');
|
|
377
|
+
const orgsDir = path.join(base, 'orgs');
|
|
378
|
+
const file = path.join(orgsDir, `${orgName}-runstate.json`);
|
|
379
|
+
const stateChanging = ['org:start','org:stop','org:agent:online','org:agent:offline'];
|
|
380
|
+
const ts = event.ts || Date.now();
|
|
381
|
+
|
|
382
|
+
if (stateChanging.includes(event.type)) {
|
|
383
|
+
// State-changing: clear any pending debounced write, then write immediately
|
|
384
|
+
const pending = _runstateDebouncers.get(orgName);
|
|
385
|
+
if (pending?.timer) clearTimeout(pending.timer);
|
|
386
|
+
_runstateDebouncers.delete(orgName);
|
|
387
|
+
let cur = null;
|
|
388
|
+
try { cur = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : {}; } catch (_) { cur = {}; }
|
|
389
|
+
if (event.type === 'org:start') {
|
|
390
|
+
cur.runId = event.runId || cur.runId;
|
|
391
|
+
cur.status = 'running';
|
|
392
|
+
cur.startedAt = ts;
|
|
393
|
+
cur.checkpointInterval = event.checkpointInterval || 600000;
|
|
394
|
+
cur.agentStates = {};
|
|
395
|
+
} else if (event.type === 'org:stop') {
|
|
396
|
+
cur.status = 'idle';
|
|
397
|
+
} else if (event.type === 'org:agent:online') {
|
|
398
|
+
cur.agentStates = cur.agentStates || {};
|
|
399
|
+
cur.agentStates[String(event.from || '').trim()] = { status: 'active', lastSeen: ts };
|
|
400
|
+
} else if (event.type === 'org:agent:offline') {
|
|
401
|
+
if (cur.agentStates?.[String(event.from || '').trim()]) {
|
|
402
|
+
cur.agentStates[String(event.from).trim()].status = 'idle';
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
cur.lastEventAt = ts;
|
|
406
|
+
try { fs.mkdirSync(orgsDir, { recursive: true }); fs.writeFileSync(file, JSON.stringify(cur, null, 2)); } catch (_) {}
|
|
407
|
+
} else {
|
|
408
|
+
// Frequent event: debounce lastEventAt write by 5s
|
|
409
|
+
const existing = _runstateDebouncers.get(orgName);
|
|
410
|
+
if (existing?.timer) clearTimeout(existing.timer);
|
|
411
|
+
const timer = setTimeout(() => {
|
|
412
|
+
_runstateDebouncers.delete(orgName);
|
|
413
|
+
try {
|
|
414
|
+
if (!fs.existsSync(file)) return;
|
|
415
|
+
const rs = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
416
|
+
rs.lastEventAt = Date.now();
|
|
417
|
+
fs.writeFileSync(file, JSON.stringify(rs, null, 2));
|
|
418
|
+
} catch (_) {}
|
|
419
|
+
}, 5000);
|
|
420
|
+
_runstateDebouncers.set(orgName, { timer });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// ── End runstate helpers ─────────────────────────────────────────────────
|
|
424
|
+
|
|
224
425
|
// Server state
|
|
225
426
|
let running = false;
|
|
226
427
|
let currentPort = null;
|
|
@@ -228,20 +429,7 @@ let currentUrl = null;
|
|
|
228
429
|
let activeServer = null;
|
|
229
430
|
const activeWatchers = [];
|
|
230
431
|
|
|
231
|
-
|
|
232
|
-
* Broadcasts a data payload to all connected SSE clients.
|
|
233
|
-
* Silently removes clients that have disconnected.
|
|
234
|
-
*/
|
|
235
|
-
function broadcast(data) {
|
|
236
|
-
const msg = `data: ${JSON.stringify(data)}\n\n`;
|
|
237
|
-
for (const client of sseClients) {
|
|
238
|
-
try {
|
|
239
|
-
client.write(msg);
|
|
240
|
-
} catch {
|
|
241
|
-
sseClients.delete(client);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
432
|
+
// broadcast() is imported from sse-manager.mjs
|
|
245
433
|
|
|
246
434
|
/**
|
|
247
435
|
* Opens a URL in the default browser, cross-platform.
|
|
@@ -375,6 +563,20 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
375
563
|
let event = {};
|
|
376
564
|
try { event = JSON.parse(body); } catch (_) {}
|
|
377
565
|
event.ts = event.ts || Date.now();
|
|
566
|
+
// Event type validation: accept any {scope}:{action} pattern — future event types
|
|
567
|
+
// auto-work without whitelist maintenance. Malformed types are logged and rejected.
|
|
568
|
+
if (event.type != null) {
|
|
569
|
+
if (typeof event.type !== 'string' || !/^[a-z][a-z0-9-]*:[a-z][a-z0-9:-]*$/.test(event.type)) {
|
|
570
|
+
try {
|
|
571
|
+
const _badLog = path.join(projectDir || process.cwd(), 'data', 'unknown-events.jsonl');
|
|
572
|
+
fs.mkdirSync(path.dirname(_badLog), { recursive: true });
|
|
573
|
+
fs.appendFileSync(_badLog, JSON.stringify({ ts: Date.now(), type: event.type, body: body.slice(0, 256) }) + '\n');
|
|
574
|
+
} catch (_) {}
|
|
575
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
576
|
+
res.end(JSON.stringify({ ok: false, error: 'invalid event type' }));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
378
580
|
// Use project path from event if provided (multi-project support).
|
|
379
581
|
// Security: path.isAbsolute() alone is insufficient — an attacker can
|
|
380
582
|
// supply event.project="/etc" and cause writes to system directories.
|
|
@@ -387,7 +589,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
387
589
|
&& path.isAbsolute(_rawProject)) {
|
|
388
590
|
// Reject filesystem root and common system directories
|
|
389
591
|
const _norm = path.resolve(_rawProject);
|
|
390
|
-
const _systemPaths = ['/', '/etc', '/usr', '/bin', '/sbin', '/lib', '/lib64', '/boot', '/dev', '/sys', '/proc', '/tmp'];
|
|
592
|
+
const _systemPaths = ['/', '/etc', '/usr', '/bin', '/sbin', '/lib', '/lib64', '/boot', '/dev', '/sys', '/proc', '/tmp', os.tmpdir(), (() => { try { return fs.realpathSync(os.tmpdir()); } catch (_) { return ''; } })()].filter(Boolean);
|
|
391
593
|
if (!_systemPaths.includes(_norm) && !_systemPaths.some(p => _norm.startsWith(p + '/'))) {
|
|
392
594
|
eventProject = _norm;
|
|
393
595
|
}
|
|
@@ -413,20 +615,38 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
413
615
|
// Any event with both org+runId updates the active run map (run:start written directly to file so org:start is first via curl)
|
|
414
616
|
if (event.runId) activeOrgRuns.set(_orgKey, String(event.runId).trim());
|
|
415
617
|
else if (activeOrgRuns.has(_orgKey)) event.runId = activeOrgRuns.get(_orgKey);
|
|
416
|
-
|
|
417
|
-
|
|
618
|
+
else { const _rsId = _getActiveRunId(_orgKey, root); if (_rsId) event.runId = _rsId; }
|
|
619
|
+
if (event.type === 'run:complete' || event.type === 'org:complete' || event.type === 'org:stop') activeOrgRuns.delete(_orgKey);
|
|
620
|
+
// Persist active-run.json so capture-handler.cjs can find the current org/runId without HTTP calls.
|
|
621
|
+
// Use process.cwd() (server's own dir, same as CLAUDE_PROJECT_DIR in the session) — not root (org project dir),
|
|
622
|
+
// because capture-handler.cjs reads from CLAUDE_PROJECT_DIR which is the server's working directory.
|
|
418
623
|
try {
|
|
419
|
-
const _captureDir = path.join(
|
|
624
|
+
const _captureDir = path.join(MONOMIND_HOME, '.monomind', 'capture');
|
|
420
625
|
const _activeRunFile = path.join(_captureDir, 'active-run.json');
|
|
421
|
-
if (event.type === 'run:start' && event.org && event.runId) {
|
|
626
|
+
if ((event.type === 'run:start' || event.type === 'org:start') && event.org && event.runId) {
|
|
422
627
|
fs.mkdirSync(_captureDir, { recursive: true });
|
|
423
628
|
fs.writeFileSync(_activeRunFile, JSON.stringify({ org: String(event.org).trim(), runId: String(event.runId).trim(), ts: Date.now() }));
|
|
424
|
-
} else if ((event.type === 'run:complete' || event.type === 'org:complete') && fs.existsSync(_activeRunFile)) {
|
|
629
|
+
} else if ((event.type === 'run:complete' || event.type === 'org:complete' || event.type === 'org:stop') && fs.existsSync(_activeRunFile)) {
|
|
425
630
|
fs.unlinkSync(_activeRunFile);
|
|
631
|
+
// Phase 1: Clean up ppid-keyed files for this org (Issue 3)
|
|
632
|
+
try {
|
|
633
|
+
const _ppidDir = path.join(_captureDir, 'active-runs');
|
|
634
|
+
const _completedOrg = String(event.org || '').trim();
|
|
635
|
+
if (_completedOrg && fs.existsSync(_ppidDir)) {
|
|
636
|
+
fs.readdirSync(_ppidDir).filter(f => f.endsWith('.json')).forEach(_pf => {
|
|
637
|
+
try {
|
|
638
|
+
const _pData = JSON.parse(fs.readFileSync(path.join(_ppidDir, _pf), 'utf8'));
|
|
639
|
+
if (_pData.org === _completedOrg) fs.unlinkSync(path.join(_ppidDir, _pf));
|
|
640
|
+
} catch (_) {}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
} catch (_e) {}
|
|
426
644
|
}
|
|
427
645
|
} catch(_e) {}
|
|
428
646
|
}
|
|
429
|
-
|
|
647
|
+
// Update durable runstate.json — survives server restarts
|
|
648
|
+
if (event.org) _updateRunState(event, root);
|
|
649
|
+
appendToFile(path.join(dataDir, 'mastermind-events.jsonl'), JSON.stringify(event) + '\n').catch(() => {});
|
|
430
650
|
// Persist to git-safe run file (survives branch switches + shared across worktrees)
|
|
431
651
|
if (event.org && event.runId) {
|
|
432
652
|
try {
|
|
@@ -437,7 +657,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
437
657
|
const _monoDir = _getGitMonomindDir(root) || path.join(root, '.monomind');
|
|
438
658
|
const _runDir = path.join(_monoDir, 'orgs', _orn, 'runs');
|
|
439
659
|
fs.mkdirSync(_runDir, { recursive: true });
|
|
440
|
-
|
|
660
|
+
await appendToFile(path.join(_runDir, `${_rid}.jsonl`), JSON.stringify(event) + '\n');
|
|
661
|
+
_insertRunEvent(event, 'http');
|
|
441
662
|
// agent:usage — persist per-role token/cost data to state.json (accumulated across runs)
|
|
442
663
|
if (event.type === 'agent:usage' && event.role) {
|
|
443
664
|
try {
|
|
@@ -462,11 +683,46 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
462
683
|
// Solution 3: dedicated conversation log — org:comms only, for easy replay
|
|
463
684
|
if (event.type === 'org:comms') {
|
|
464
685
|
const _conv = { ts: event.ts, run_id: _rid, from: event.from, to: event.to, msg: event.msg };
|
|
465
|
-
|
|
686
|
+
await appendToFile(path.join(_runDir, `${_rid}.convs.jsonl`), JSON.stringify(_conv) + '\n');
|
|
466
687
|
// Also write to org-level threads.jsonl so the dashboard Threads tab shows agent conversations
|
|
467
688
|
const _orgThreadsFile = path.join(root, '.monomind', 'orgs', `${_orn}-threads.jsonl`);
|
|
468
689
|
const _thread = { type: 'message', id: `${_rid}-${event.ts}`, run_id: _rid, ts: event.ts, from: event.from, to: event.to, msg: event.msg, subject: `Run ${_rid}` };
|
|
469
|
-
|
|
690
|
+
appendToFile(_orgThreadsFile, JSON.stringify(_thread) + '\n').catch(() => {});
|
|
691
|
+
}
|
|
692
|
+
// Phase 4: Compact completed run to three-tier retention (Issue 7)
|
|
693
|
+
// hot (SQLite JSONL in .monomind) → warm (flat JSONL in archive/) → cold (gzip)
|
|
694
|
+
// We use a lightweight approach: rename completed JSONL to .warm.jsonl, then gzip runs
|
|
695
|
+
// older than 24 hours to .cold.jsonl.gz — no external deps.
|
|
696
|
+
if (event.type === 'run:complete' || event.type === 'org:complete') {
|
|
697
|
+
setImmediate(() => {
|
|
698
|
+
try {
|
|
699
|
+
const _hotFile = path.join(_runDir, `${_rid}.jsonl`);
|
|
700
|
+
const _warmFile = path.join(_runDir, `${_rid}.warm.jsonl`);
|
|
701
|
+
// Promote: hot → warm (just rename — same dir, marks run as done)
|
|
702
|
+
if (fs.existsSync(_hotFile) && !fs.existsSync(_warmFile)) {
|
|
703
|
+
fs.renameSync(_hotFile, _warmFile);
|
|
704
|
+
}
|
|
705
|
+
// Compact warm files older than 24h to cold gzip
|
|
706
|
+
const _24h = 24 * 60 * 60 * 1000;
|
|
707
|
+
fs.readdirSync(_runDir).filter(f => f.endsWith('.warm.jsonl')).forEach(_wf => {
|
|
708
|
+
const _wp = path.join(_runDir, _wf);
|
|
709
|
+
try {
|
|
710
|
+
if (Date.now() - fs.statSync(_wp).mtimeMs < _24h) return;
|
|
711
|
+
const _coldPath = _wp.replace('.warm.jsonl', '.cold.jsonl.gz');
|
|
712
|
+
if (fs.existsSync(_coldPath)) return; // already compacted
|
|
713
|
+
const _warmData = fs.readFileSync(_wp);
|
|
714
|
+
const zlib = require('zlib');
|
|
715
|
+
zlib.gzip(_warmData, (_err, _gz) => {
|
|
716
|
+
if (_err) return;
|
|
717
|
+
try {
|
|
718
|
+
fs.writeFileSync(_coldPath, _gz);
|
|
719
|
+
fs.unlinkSync(_wp); // remove warm after cold written
|
|
720
|
+
} catch (_) {}
|
|
721
|
+
});
|
|
722
|
+
} catch (_) {}
|
|
723
|
+
});
|
|
724
|
+
} catch (_) {}
|
|
725
|
+
});
|
|
470
726
|
}
|
|
471
727
|
}
|
|
472
728
|
} catch (_) {}
|
|
@@ -498,11 +754,11 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
498
754
|
// Format: data/sessions/<sessionId>.jsonl + data/sessions/_index.json
|
|
499
755
|
try {
|
|
500
756
|
const _sid = String(event.session || '').trim();
|
|
501
|
-
if (_sid.length > 0 && _sid.length <= 128 && /^[a-zA-Z0-9_.-]
|
|
757
|
+
if (_sid.length > 0 && _sid.length <= 128 && /^(?!.*\.\.)[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/.test(_sid)) {
|
|
502
758
|
const sessDir = path.join(dataDir, 'sessions');
|
|
503
759
|
fs.mkdirSync(sessDir, { recursive: true });
|
|
504
760
|
// Append event to per-session JSONL (O(1), no read)
|
|
505
|
-
|
|
761
|
+
appendToFile(path.join(sessDir, `${_sid}.jsonl`), JSON.stringify(event) + '\n').catch(() => {});
|
|
506
762
|
// Update lightweight index (id, ts, prompt, status, org, startedAt, endedAt, domains only)
|
|
507
763
|
const indexFile = path.join(sessDir, '_index.json');
|
|
508
764
|
let _idx = [];
|
|
@@ -548,20 +804,36 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
548
804
|
fs.writeFileSync(sessFile, JSON.stringify(sessions.slice(0, 500)));
|
|
549
805
|
} catch (_) {}
|
|
550
806
|
// For org:stop events, write a stop marker the boss agent can detect
|
|
551
|
-
|
|
807
|
+
// For org:start events, remove any existing stop marker so the org shows as running again
|
|
808
|
+
if ((event.type === 'org:stop' || event.type === 'org:start') && event.org) {
|
|
552
809
|
try {
|
|
553
810
|
const orgName = String(event.org).trim();
|
|
554
811
|
// Validate before any filesystem use — reject rather than strip
|
|
555
812
|
if (orgName.length > 0 && orgName.length <= 64 && /^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
556
813
|
const stopDir = path.join(root, '.monomind', 'orgs', '.stops');
|
|
557
|
-
|
|
558
|
-
|
|
814
|
+
if (event.type === 'org:stop') {
|
|
815
|
+
fs.mkdirSync(stopDir, { recursive: true });
|
|
816
|
+
fs.writeFileSync(path.join(stopDir, `${orgName}.stop`), String(Date.now()));
|
|
817
|
+
} else {
|
|
818
|
+
// org:start — remove stop file so the org can appear running
|
|
819
|
+
try { fs.unlinkSync(path.join(stopDir, `${orgName}.stop`)); } catch (_) {}
|
|
820
|
+
}
|
|
559
821
|
}
|
|
560
822
|
} catch (_) {}
|
|
561
823
|
}
|
|
562
824
|
// Broadcast to all mastermind SSE clients
|
|
563
|
-
|
|
564
|
-
|
|
825
|
+
broadcastMm(event);
|
|
826
|
+
// Phase 3: Forward to per-org streaming tail clients
|
|
827
|
+
if (event.org) {
|
|
828
|
+
const _fwdOrg = String(event.org).trim();
|
|
829
|
+
const _fwdClients = runStreamClients.get(_fwdOrg);
|
|
830
|
+
if (_fwdClients && _fwdClients.size > 0) {
|
|
831
|
+
const _fwdLine = `data: ${JSON.stringify(event)}\n\n`;
|
|
832
|
+
for (const _fwdClient of _fwdClients) {
|
|
833
|
+
try { _fwdClient.write(_fwdLine); } catch (_) { _fwdClients.delete(_fwdClient); }
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
565
837
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
566
838
|
res.end('{"ok":true}');
|
|
567
839
|
}
|
|
@@ -3293,11 +3565,11 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
3293
3565
|
}
|
|
3294
3566
|
}, 20_000);
|
|
3295
3567
|
|
|
3296
|
-
|
|
3568
|
+
addSseClient(res);
|
|
3297
3569
|
|
|
3298
3570
|
req.on('close', () => {
|
|
3299
3571
|
clearInterval(keepAlive);
|
|
3300
|
-
|
|
3572
|
+
removeSseClient(res);
|
|
3301
3573
|
});
|
|
3302
3574
|
|
|
3303
3575
|
// Send the initial snapshot immediately
|
|
@@ -3386,44 +3658,36 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
3386
3658
|
if (req.method === 'GET' && url === '/api/orgs') {
|
|
3387
3659
|
try {
|
|
3388
3660
|
const _orgsQs = new URL(req.url, 'http://localhost').searchParams;
|
|
3389
|
-
const
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
// Read events file once, outside the per-org loop
|
|
3395
|
-
let recentLines = [];
|
|
3661
|
+
const _orgsExplicitDir = _orgsQs.get('dir');
|
|
3662
|
+
const _orgsServerRoot = path.resolve(_orgsExplicitDir || projectDir || process.cwd());
|
|
3663
|
+
// Collect project dirs to search: explicit dir + known-projects (like sessions API)
|
|
3664
|
+
const _orgsProjDirs = new Set([_orgsServerRoot]);
|
|
3665
|
+
if (!_orgsExplicitDir) {
|
|
3396
3666
|
try {
|
|
3397
|
-
const
|
|
3398
|
-
if (fs.existsSync(
|
|
3399
|
-
|
|
3400
|
-
const stat = fs.statSync(evFile);
|
|
3401
|
-
const TAIL = 65536;
|
|
3402
|
-
const fd = fs.openSync(evFile, 'r');
|
|
3403
|
-
const buf = Buffer.alloc(Math.min(TAIL, stat.size));
|
|
3404
|
-
try {
|
|
3405
|
-
fs.readSync(fd, buf, 0, buf.length, Math.max(0, stat.size - buf.length));
|
|
3406
|
-
} finally {
|
|
3407
|
-
fs.closeSync(fd);
|
|
3408
|
-
}
|
|
3409
|
-
recentLines = buf.toString('utf8').split('\n').filter(Boolean).reverse();
|
|
3667
|
+
const _knownOrgsFile = path.join(_orgsServerRoot, 'data', 'known-projects.json');
|
|
3668
|
+
if (fs.existsSync(_knownOrgsFile)) {
|
|
3669
|
+
JSON.parse(fs.readFileSync(_knownOrgsFile, 'utf8')).forEach(p => _orgsProjDirs.add(p));
|
|
3410
3670
|
}
|
|
3411
3671
|
} catch(_) {}
|
|
3672
|
+
}
|
|
3673
|
+
const _sidecarSuffixRe = /-(approvals|state|activity|goals|routines|projects|members|issues|workspaces|worktrees|environments|plugins|adapters|bootstrap|threads|budgets|project-workspaces|approval-comments|secrets|join-requests|skills)\.json$/;
|
|
3674
|
+
const _orgsSeen = new Set();
|
|
3675
|
+
let orgs = [];
|
|
3676
|
+
for (const _opd of _orgsProjDirs) {
|
|
3677
|
+
const orgsDir = path.join(_opd, '.monomind', 'orgs');
|
|
3678
|
+
if (!fs.existsSync(orgsDir)) continue;
|
|
3679
|
+
const files = fs.readdirSync(orgsDir).filter(f => f.endsWith('.json') && !_sidecarSuffixRe.test(f));
|
|
3412
3680
|
for (const f of files) {
|
|
3413
3681
|
try {
|
|
3414
3682
|
const cfg = JSON.parse(fs.readFileSync(path.join(orgsDir, f), 'utf8'));
|
|
3415
|
-
let running = false;
|
|
3416
|
-
const lastStart = recentLines.find(l => { try { const e = JSON.parse(l); return e.type === 'org:start' && e.org === cfg.name; } catch(_) { return false; } });
|
|
3417
|
-
const lastStop = recentLines.find(l => { try { const e = JSON.parse(l); return (e.type === 'org:stop' || e.type === 'org:complete') && e.org === cfg.name; } catch(_) { return false; } });
|
|
3418
|
-
if (lastStart) {
|
|
3419
|
-
const startTs = JSON.parse(lastStart).ts || 0;
|
|
3420
|
-
const stopTs = lastStop ? (JSON.parse(lastStop).ts || 0) : 0;
|
|
3421
|
-
running = startTs > stopTs;
|
|
3422
|
-
}
|
|
3423
|
-
// Also check in-memory activeOrgRuns so the list reflects LIVE immediately after launch
|
|
3424
3683
|
const _lOrgName = cfg.name || '';
|
|
3425
|
-
if (!
|
|
3426
|
-
|
|
3684
|
+
if (!_lOrgName || _orgsSeen.has(_lOrgName)) continue;
|
|
3685
|
+
_orgsSeen.add(_lOrgName);
|
|
3686
|
+
const _rs = _readRunState(_lOrgName, _opd);
|
|
3687
|
+
const _ttl = Math.max((_rs?.checkpointInterval || 600000) * 2, 7200000);
|
|
3688
|
+
let running = (_rs?.status === 'running' && (Date.now() - (_rs?.lastEventAt || 0)) < _ttl)
|
|
3689
|
+
|| activeOrgRuns.has(_lOrgName);
|
|
3690
|
+
orgs.push({ name: cfg.name, goal: cfg.goal, roles: Array.isArray(cfg.roles) ? cfg.roles : [], topology: cfg.topology, created_at: cfg.created_at, running, status: cfg.status, projectDir: _opd, lastEventAt: _rs?.lastEventAt || null, loop: cfg.loop ? { poll_interval_minutes: cfg.loop.poll_interval_minutes, last_run: cfg.loop.last_run, next_run: cfg.loop.next_run } : undefined });
|
|
3427
3691
|
} catch(_) {}
|
|
3428
3692
|
}
|
|
3429
3693
|
}
|
|
@@ -3491,7 +3755,9 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
3491
3755
|
const orgName = decodeURIComponent(url.slice('/api/orgs/'.length));
|
|
3492
3756
|
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3493
3757
|
const _orgsOneQs = new URL(req.url, 'http://localhost').searchParams;
|
|
3494
|
-
const
|
|
3758
|
+
const _orgsOneRoot = path.resolve(_orgsOneQs.get('dir') || projectDir || process.cwd());
|
|
3759
|
+
const _orgsOneProjDir = _resolveOrgProjectDir(orgName, _orgsOneRoot) || _orgsOneRoot;
|
|
3760
|
+
const f = path.join(_orgsOneProjDir, '.monomind', 'orgs', `${orgName}.json`);
|
|
3495
3761
|
if (!fs.existsSync(f)) { res.writeHead(404); res.end('{"error":"not found"}'); return; }
|
|
3496
3762
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3497
3763
|
res.end(fs.readFileSync(f, 'utf8'));
|
|
@@ -3505,7 +3771,9 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
3505
3771
|
const orgName = decodeURIComponent(url.slice('/api/org/'.length));
|
|
3506
3772
|
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3507
3773
|
const _orgQs = new URL(req.url, 'http://localhost').searchParams;
|
|
3508
|
-
const
|
|
3774
|
+
const _orgServerRoot = path.resolve(_orgQs.get('dir') || projectDir || process.cwd());
|
|
3775
|
+
// Resolve which project dir actually has this org's config
|
|
3776
|
+
const d = _resolveOrgProjectDir(orgName, _orgServerRoot) || _orgServerRoot;
|
|
3509
3777
|
const orgsDir = path.join(d, '.monomind', 'orgs');
|
|
3510
3778
|
|
|
3511
3779
|
const readJsonSafe = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
|
|
@@ -3530,7 +3798,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
3530
3798
|
try { return fs.statSync(path.join(orgsDir, `${orgName}-state.json`)).mtimeMs; } catch { return 0; }
|
|
3531
3799
|
})();
|
|
3532
3800
|
// Also check org's most recent run file mtime
|
|
3533
|
-
const orgRunsDir = path.join(d, '.monomind', 'orgs', orgName, 'runs');
|
|
3801
|
+
const orgRunsDir = path.join(_getGitMonomindDir(d) || path.join(d, '.monomind'), 'orgs', orgName, 'runs');
|
|
3534
3802
|
const orgLastRunMtime = (() => {
|
|
3535
3803
|
try {
|
|
3536
3804
|
if (!fs.existsSync(orgRunsDir)) return 0;
|
|
@@ -3557,7 +3825,10 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
3557
3825
|
});
|
|
3558
3826
|
} catch { return false; }
|
|
3559
3827
|
})();
|
|
3560
|
-
const
|
|
3828
|
+
const _runstateData = _readRunState(orgName, d);
|
|
3829
|
+
const _runstateTtl = Math.max((_runstateData?.checkpointInterval || 600000) * 2, 7200000);
|
|
3830
|
+
const _runstateAlive = _runstateData?.status === 'running' && (Date.now() - (_runstateData?.lastEventAt || 0)) < _runstateTtl;
|
|
3831
|
+
const running = !fs.existsSync(stopFile) && (_runstateAlive || activeOrgRuns.has(orgName) || _loopRunning);
|
|
3561
3832
|
|
|
3562
3833
|
// Read real tasks from the task store and group by status column
|
|
3563
3834
|
const taskStoreData = readJsonSafe(path.join(d, '.monomind', 'tasks', 'store.json'));
|
|
@@ -3569,7 +3840,10 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
3569
3840
|
};
|
|
3570
3841
|
|
|
3571
3842
|
const result = { config, state, goals: goalsData.goals, routines: routinesData.routines,
|
|
3572
|
-
approvals: approvalsData.approvals, running, tasks
|
|
3843
|
+
approvals: approvalsData.approvals, running, tasks,
|
|
3844
|
+
runId: _runstateData?.runId || null,
|
|
3845
|
+
lastEventAt: _runstateData?.lastEventAt || null,
|
|
3846
|
+
agentStates: _runstateData?.agentStates || {} };
|
|
3573
3847
|
|
|
3574
3848
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3575
3849
|
res.end(JSON.stringify(result));
|
|
@@ -3584,7 +3858,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
3584
3858
|
const orgName = decodeURIComponent(parts[3]);
|
|
3585
3859
|
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('[]'); return; }
|
|
3586
3860
|
const _actQs = new URL(req.url, 'http://localhost').searchParams;
|
|
3587
|
-
const
|
|
3861
|
+
const _actServerRoot = path.resolve(_actQs.get('dir') || projectDir || process.cwd());
|
|
3862
|
+
const d = _resolveOrgProjectDir(orgName, _actServerRoot) || _actServerRoot;
|
|
3588
3863
|
const orgsDir = path.join(d, '.monomind', 'orgs');
|
|
3589
3864
|
const readJ = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
|
|
3590
3865
|
const events = [];
|
|
@@ -3672,7 +3947,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
3672
3947
|
const orgName = decodeURIComponent(parts[3]);
|
|
3673
3948
|
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
|
|
3674
3949
|
const _adaptersQs = new URL(req.url, 'http://localhost').searchParams;
|
|
3675
|
-
const
|
|
3950
|
+
const _adaptersRoot = path.resolve(_adaptersQs.get('dir') || projectDir || process.cwd());
|
|
3951
|
+
const d = _resolveOrgProjectDir(orgName, _adaptersRoot) || _adaptersRoot;
|
|
3676
3952
|
const adaptersFile = path.join(d, '.monomind', 'orgs', `${orgName}-adapters.json`);
|
|
3677
3953
|
if (!fs.existsSync(adaptersFile)) {
|
|
3678
3954
|
// Return defaults derived from org config if available
|
|
@@ -4248,9 +4524,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4248
4524
|
fs.renameSync(tmp, approvalsFile);
|
|
4249
4525
|
// Emit org:approval:resolved event so boss agent unblocks
|
|
4250
4526
|
const event = { type: 'org:approval:resolved', org: orgName, approval_id: approvalId, status, ts: Date.now() };
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
for (const c of mmSseClients) { try { c.write(msg); } catch(_) { mmSseClients.delete(c); } }
|
|
4527
|
+
appendToFile(path.join(path.resolve(_postApprovalsQs.get('dir') || projectDir || process.cwd()), 'data', 'mastermind-events.jsonl'), JSON.stringify(event) + '\n').catch(() => {});
|
|
4528
|
+
broadcastMm(event);
|
|
4254
4529
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
4255
4530
|
res.end(JSON.stringify({ ok: true, status }));
|
|
4256
4531
|
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
@@ -4368,7 +4643,9 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4368
4643
|
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
4369
4644
|
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
4370
4645
|
const _threadsQs = new URL(req.url, 'http://localhost').searchParams;
|
|
4371
|
-
const
|
|
4646
|
+
const _threadsRoot = path.resolve(_threadsQs.get('dir') || projectDir || process.cwd());
|
|
4647
|
+
const _threadsProjDir = _resolveOrgProjectDir(orgName, _threadsRoot) || _threadsRoot;
|
|
4648
|
+
const threadsFile = path.join(_threadsProjDir, '.monomind', 'orgs', `${orgName}-threads.jsonl`);
|
|
4372
4649
|
let threads = [];
|
|
4373
4650
|
try {
|
|
4374
4651
|
const lines = fs.readFileSync(threadsFile, 'utf8').split('\n').filter(l => l.trim());
|
|
@@ -4429,7 +4706,9 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4429
4706
|
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
4430
4707
|
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
4431
4708
|
const _goalsQs = new URL(req.url, 'http://localhost').searchParams;
|
|
4432
|
-
const
|
|
4709
|
+
const _goalsRoot = path.resolve(_goalsQs.get('dir') || projectDir || process.cwd());
|
|
4710
|
+
const _goalsProjDir = _resolveOrgProjectDir(orgName, _goalsRoot) || _goalsRoot;
|
|
4711
|
+
const goalsFile = path.join(_goalsProjDir, '.monomind', 'orgs', `${orgName}-goals.json`);
|
|
4433
4712
|
let data = { goals: [] };
|
|
4434
4713
|
try { data = JSON.parse(fs.readFileSync(goalsFile, 'utf8')); } catch(_) {}
|
|
4435
4714
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
@@ -4445,13 +4724,14 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4445
4724
|
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
4446
4725
|
const _routinesQs = new URL(req.url, 'http://localhost').searchParams;
|
|
4447
4726
|
const _routinesBase = path.resolve(_routinesQs.get('dir') || projectDir || process.cwd());
|
|
4448
|
-
const
|
|
4727
|
+
const _routinesProjDir = _resolveOrgProjectDir(orgName, _routinesBase) || _routinesBase;
|
|
4728
|
+
const routinesFile = path.join(_routinesProjDir, '.monomind', 'orgs', `${orgName}-routines.json`);
|
|
4449
4729
|
let data = { routines: [] };
|
|
4450
4730
|
try { data = JSON.parse(fs.readFileSync(routinesFile, 'utf8')); } catch(_) {}
|
|
4451
4731
|
// Synthesize routines from org config's loop/schedule settings when no explicit routines are defined
|
|
4452
4732
|
if (!data.routines || !data.routines.length) {
|
|
4453
4733
|
try {
|
|
4454
|
-
const orgCfg = JSON.parse(fs.readFileSync(path.join(
|
|
4734
|
+
const orgCfg = JSON.parse(fs.readFileSync(path.join(_routinesProjDir, '.monomind', 'orgs', `${orgName}.json`), 'utf8'));
|
|
4455
4735
|
const loop = orgCfg.loop;
|
|
4456
4736
|
if (loop && (loop.poll_interval_minutes || loop.interval_minutes)) {
|
|
4457
4737
|
const intervalMin = loop.poll_interval_minutes || loop.interval_minutes;
|
|
@@ -4643,9 +4923,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4643
4923
|
try { const lpf = path.join(path.resolve(projectDir || process.cwd()), '.monomind', 'loops', `${orgName}.md`); if (fs.existsSync(lpf)) fs.unlinkSync(lpf); } catch(_) {}
|
|
4644
4924
|
// Emit org:delete event
|
|
4645
4925
|
const deleteEvent = { type: 'org:delete', org: orgName, ts: Date.now() };
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
for (const c of mmSseClients) { try { c.write(msg); } catch(_) { mmSseClients.delete(c); } }
|
|
4926
|
+
appendToFile(path.join(projectDir || process.cwd(), 'data', 'mastermind-events.jsonl'), JSON.stringify(deleteEvent) + '\n').catch(() => {});
|
|
4927
|
+
broadcastMm(deleteEvent);
|
|
4649
4928
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
4650
4929
|
res.end('{"ok":true}');
|
|
4651
4930
|
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
@@ -4662,15 +4941,14 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4662
4941
|
const stopEvent = { type: 'org:stop', org: orgName, ts: Date.now() };
|
|
4663
4942
|
const dataDir = path.join(_stopOrgBase, 'data');
|
|
4664
4943
|
try { fs.mkdirSync(dataDir, { recursive: true }); } catch(_) {}
|
|
4665
|
-
|
|
4944
|
+
appendToFile(path.join(dataDir, 'mastermind-events.jsonl'), JSON.stringify(stopEvent) + '\n').catch(() => {});
|
|
4666
4945
|
// Write stop marker file for boss agent to detect
|
|
4667
4946
|
try {
|
|
4668
4947
|
const stopDir = path.join(_stopOrgBase, '.monomind', 'orgs', '.stops');
|
|
4669
4948
|
fs.mkdirSync(stopDir, { recursive: true });
|
|
4670
4949
|
fs.writeFileSync(path.join(stopDir, `${orgName}.stop`), String(Date.now()));
|
|
4671
4950
|
} catch(_) {}
|
|
4672
|
-
|
|
4673
|
-
for (const c of mmSseClients) { try { c.write(msg); } catch(_) { mmSseClients.delete(c); } }
|
|
4951
|
+
broadcastMm(stopEvent);
|
|
4674
4952
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
4675
4953
|
res.end('{"ok":true}');
|
|
4676
4954
|
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
@@ -4708,35 +4986,63 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4708
4986
|
try {
|
|
4709
4987
|
const _rQs = new URL(req.url, 'http://localhost').searchParams;
|
|
4710
4988
|
const _rOrgName = decodeURIComponent(url.split('/')[3] || '');
|
|
4711
|
-
|
|
4712
|
-
const
|
|
4713
|
-
const
|
|
4989
|
+
if (_rOrgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_rOrgName)) { res.writeHead(400); res.end('{"error":"Invalid org name"}'); return; }
|
|
4990
|
+
const _rExplicitDir = _rQs.get('dir');
|
|
4991
|
+
const _rServerRoot = path.resolve(_rExplicitDir || projectDir || process.cwd());
|
|
4992
|
+
// Search across known projects (same logic as /api/orgs) unless explicit dir given
|
|
4993
|
+
const _rProjDirs = new Set([_rServerRoot]);
|
|
4994
|
+
if (!_rExplicitDir) {
|
|
4995
|
+
try {
|
|
4996
|
+
const _rKnown = JSON.parse(fs.readFileSync(path.join(_rServerRoot, 'data', 'known-projects.json'), 'utf8'));
|
|
4997
|
+
_rKnown.forEach(p => _rProjDirs.add(p));
|
|
4998
|
+
} catch(_) {}
|
|
4999
|
+
}
|
|
5000
|
+
const _rSeenFiles = new Set();
|
|
4714
5001
|
const runs = [];
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
5002
|
+
const _parseRun = (filePath, f) => {
|
|
5003
|
+
try {
|
|
5004
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
5005
|
+
const allLines = raw.split('\n').filter(Boolean);
|
|
5006
|
+
const parse = l => { try { return JSON.parse(l); } catch { return null; } };
|
|
5007
|
+
// Merge .warm.jsonl (promoted pre-complete events) for accurate event count + metadata
|
|
5008
|
+
const warmFile = filePath.replace(/\.jsonl$/, '.warm.jsonl');
|
|
5009
|
+
let warmLines = [];
|
|
5010
|
+
let warmEvents = [];
|
|
5011
|
+
try { if (fs.existsSync(warmFile)) { warmLines = fs.readFileSync(warmFile, 'utf8').split('\n').filter(Boolean); warmEvents = warmLines.map(parse).filter(Boolean); } } catch(_) {}
|
|
5012
|
+
const combinedLines = [...warmLines, ...allLines];
|
|
5013
|
+
const eventCount = combinedLines.length;
|
|
5014
|
+
const headEvents = (warmEvents.length ? warmEvents : allLines.map(parse).filter(Boolean)).slice(0, 10);
|
|
5015
|
+
const tailEvents = (allLines.map(parse).filter(Boolean).slice(-5).length ? allLines.map(parse).filter(Boolean).slice(-5) : warmEvents.slice(-5));
|
|
5016
|
+
const first = headEvents.find(e => e.type === 'run:start') || headEvents[0];
|
|
5017
|
+
const last = [...(warmEvents.slice(-5)), ...(allLines.map(parse).filter(Boolean).slice(-3))].slice().reverse().find(e => e.type === 'run:complete' || e.type === 'org:complete');
|
|
5018
|
+
const cycles = combinedLines.filter(l => l.includes('"org:checkpoint"')).length;
|
|
5019
|
+
const lastEvent = allLines.map(parse).filter(Boolean).slice(-1)[0] || warmEvents.slice(-1)[0];
|
|
5020
|
+
const ageMs = lastEvent?.ts ? Date.now() - lastEvent.ts : Infinity;
|
|
5021
|
+
const isStale = !last && ageMs > 30 * 60 * 1000;
|
|
5022
|
+
const firstBossComms = headEvents.find(e => e.type === 'org:comms' && (e.from === 'boss' || e.role === 'boss') && e.msg);
|
|
5023
|
+
const derivedGoal = first?.goal || firstBossComms?.msg?.slice(0, 80) || '';
|
|
5024
|
+
return { runId: f.replace('.jsonl', ''), startedAt: first?.ts || 0, endedAt: last?.ts || 0,
|
|
5025
|
+
status: last ? 'complete' : isStale ? 'stale' : 'running',
|
|
5026
|
+
eventCount, cycleCount: cycles, goal: derivedGoal, bossRole: first?.bossRole || '' };
|
|
5027
|
+
} catch(_) { return null; }
|
|
5028
|
+
};
|
|
5029
|
+
for (const _rpd of _rProjDirs) {
|
|
5030
|
+
// Check both .monomind and .git/monomind locations
|
|
5031
|
+
const _rMonoDir = _getGitMonomindDir(_rpd) || path.join(_rpd, '.monomind');
|
|
5032
|
+
const _rSearchDirs = [path.join(_rMonoDir, 'orgs', _rOrgName, 'runs')];
|
|
5033
|
+
if (_rMonoDir !== path.join(_rpd, '.monomind')) _rSearchDirs.push(path.join(_rpd, '.monomind', 'orgs', _rOrgName, 'runs'));
|
|
5034
|
+
for (const _rDir of _rSearchDirs) {
|
|
5035
|
+
if (!fs.existsSync(_rDir)) continue;
|
|
5036
|
+
const files = fs.readdirSync(_rDir).filter(f => f.endsWith('.jsonl') && !f.endsWith('.convs.jsonl') && !f.endsWith('.warm.jsonl') && !f.endsWith('.cold.jsonl')).sort().reverse();
|
|
5037
|
+
for (const f of files.slice(0, 50)) {
|
|
5038
|
+
if (_rSeenFiles.has(f)) continue;
|
|
5039
|
+
_rSeenFiles.add(f);
|
|
5040
|
+
const r = _parseRun(path.join(_rDir, f), f);
|
|
5041
|
+
if (r) runs.push(r);
|
|
5042
|
+
}
|
|
4738
5043
|
}
|
|
4739
5044
|
}
|
|
5045
|
+
runs.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
|
|
4740
5046
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
4741
5047
|
res.end(JSON.stringify(runs));
|
|
4742
5048
|
} catch (_) { res.writeHead(500); res.end('[]'); }
|
|
@@ -4750,18 +5056,72 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4750
5056
|
const _rvParts = url.replace(/\?.*$/, '').split('/');
|
|
4751
5057
|
const _rvOrgName = decodeURIComponent(_rvParts[3] || '');
|
|
4752
5058
|
const _rvRunId = decodeURIComponent(_rvParts[5] || '');
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
const
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
5059
|
+
if (_rvOrgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_rvOrgName) ||
|
|
5060
|
+
_rvRunId.length > 80 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_rvRunId)) { res.writeHead(400); res.end('{"error":"Invalid org or run id"}'); return; }
|
|
5061
|
+
const _rvExplicitDir = _rvQs.get('dir');
|
|
5062
|
+
const _rvServerRoot = path.resolve(_rvExplicitDir || projectDir || process.cwd());
|
|
5063
|
+
// Search across known projects
|
|
5064
|
+
const _rvProjDirs = new Set([_rvServerRoot]);
|
|
5065
|
+
if (!_rvExplicitDir) {
|
|
5066
|
+
try {
|
|
5067
|
+
JSON.parse(fs.readFileSync(path.join(_rvServerRoot, 'data', 'known-projects.json'), 'utf8')).forEach(p => _rvProjDirs.add(p));
|
|
5068
|
+
} catch(_) {}
|
|
5069
|
+
}
|
|
5070
|
+
let _rvFile = null;
|
|
5071
|
+
for (const _rvpd of _rvProjDirs) {
|
|
5072
|
+
const _rvMonoDir = _getGitMonomindDir(_rvpd) || path.join(_rvpd, '.monomind');
|
|
5073
|
+
const _candidates = [path.join(_rvMonoDir, 'orgs', _rvOrgName, 'runs', `${_rvRunId}.jsonl`)];
|
|
5074
|
+
if (_rvMonoDir !== path.join(_rvpd, '.monomind')) _candidates.push(path.join(_rvpd, '.monomind', 'orgs', _rvOrgName, 'runs', `${_rvRunId}.jsonl`));
|
|
5075
|
+
for (const c of _candidates) { if (fs.existsSync(c)) { _rvFile = c; break; } }
|
|
5076
|
+
if (_rvFile) break;
|
|
5077
|
+
}
|
|
5078
|
+
if (!_rvFile) { res.writeHead(404); res.end('{"error":"run not found"}'); return; }
|
|
5079
|
+
const _parseLines = p => { try { return fs.readFileSync(p, 'utf8').split('\n').filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); } catch { return []; } };
|
|
5080
|
+
const events = _parseLines(_rvFile);
|
|
5081
|
+
// Merge .warm.jsonl (pre-run:complete events, including org:comms) if it exists.
|
|
5082
|
+
// When run:complete fires, the hot .jsonl is renamed to .warm.jsonl so all pre-complete
|
|
5083
|
+
// events live there. The current .jsonl then only holds post-complete events (e.g. org:stop).
|
|
5084
|
+
const _rvWarmFile = _rvFile.replace(/\.jsonl$/, '.warm.jsonl');
|
|
5085
|
+
if (fs.existsSync(_rvWarmFile)) {
|
|
5086
|
+
events.push(..._parseLines(_rvWarmFile));
|
|
5087
|
+
}
|
|
5088
|
+
// For in-progress runs (no .warm.jsonl), org:comms also go to .convs.jsonl (stripped form).
|
|
5089
|
+
// They're already in .jsonl as full events, so .convs.jsonl would duplicate — skip it.
|
|
5090
|
+
events.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
4759
5091
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
4760
5092
|
res.end(JSON.stringify(events));
|
|
4761
5093
|
} catch (_) { res.writeHead(500); res.end('[]'); }
|
|
4762
5094
|
return;
|
|
4763
5095
|
}
|
|
4764
5096
|
|
|
5097
|
+
// GET /api/org/:name/artifact — serve file content for chat "View" button
|
|
5098
|
+
if (req.method === 'GET' && /^\/api\/org\/[^/]+\/artifact/.test(url)) {
|
|
5099
|
+
try {
|
|
5100
|
+
const _artQp = new URL('http://x' + req.url).searchParams;
|
|
5101
|
+
const _rawPath = _artQp.get('path');
|
|
5102
|
+
if (!_rawPath) { res.writeHead(400); res.end(JSON.stringify({ error: 'path required' })); return; }
|
|
5103
|
+
const _filePath = path.resolve(decodeURIComponent(_rawPath));
|
|
5104
|
+
// Path traversal guard: only allow reads within known project dirs
|
|
5105
|
+
const _allowed = _getAllowedArtifactDirs(projectDir || process.cwd());
|
|
5106
|
+
const _safe = _allowed.some(d => _filePath.startsWith(d + path.sep) || _filePath === d);
|
|
5107
|
+
if (!_safe) { res.writeHead(403); res.end(JSON.stringify({ error: 'path not allowed' })); return; }
|
|
5108
|
+
if (!fs.existsSync(_filePath)) { res.writeHead(404); res.end(JSON.stringify({ error: 'file not found' })); return; }
|
|
5109
|
+
const _mime = _detectMimeType(_filePath);
|
|
5110
|
+
const _size = fs.statSync(_filePath).size;
|
|
5111
|
+
// Reject files >2MB to avoid blocking the event loop
|
|
5112
|
+
if (_size > 2 * 1024 * 1024) { res.writeHead(413); res.end(JSON.stringify({ error: 'file too large', size: _size })); return; }
|
|
5113
|
+
if (!_mime.startsWith('text/') && _mime !== 'application/json') {
|
|
5114
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
5115
|
+
res.end(JSON.stringify({ binary: true, mimeType: _mime, size: _size }));
|
|
5116
|
+
return;
|
|
5117
|
+
}
|
|
5118
|
+
const _content = fs.readFileSync(_filePath, 'utf8');
|
|
5119
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
5120
|
+
res.end(JSON.stringify({ content: _content, mimeType: _mime, size: _size }));
|
|
5121
|
+
} catch (_e) { res.writeHead(500); res.end(JSON.stringify({ error: 'read failed' })); }
|
|
5122
|
+
return;
|
|
5123
|
+
}
|
|
5124
|
+
|
|
4765
5125
|
// ------------------------------------------------- Mastermind event system
|
|
4766
5126
|
// POST /api/mastermind/event — ingest event from mastermind skill
|
|
4767
5127
|
if (req.method === 'POST' && url === '/api/mastermind/event') {
|
|
@@ -4777,7 +5137,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4777
5137
|
'Access-Control-Allow-Origin': '*',
|
|
4778
5138
|
});
|
|
4779
5139
|
res.write(': connected\n\n');
|
|
4780
|
-
|
|
5140
|
+
addMmClient(res);
|
|
4781
5141
|
// Replay last 50 events from disk (use ?project= param if provided)
|
|
4782
5142
|
try {
|
|
4783
5143
|
const _sseQp = new URL('http://x' + req.url).searchParams;
|
|
@@ -4787,8 +5147,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4787
5147
|
const lines = fs.readFileSync(evFile, 'utf8').trim().split('\n').filter(Boolean).slice(-50);
|
|
4788
5148
|
for (const l of lines) res.write(`data: ${l}\n\n`);
|
|
4789
5149
|
} catch (_) {}
|
|
4790
|
-
const ka = setInterval(() => { try { res.write(': ping\n\n'); } catch (_) { clearInterval(ka);
|
|
4791
|
-
req.on('close', () => {
|
|
5150
|
+
const ka = setInterval(() => { try { res.write(': ping\n\n'); } catch (_) { clearInterval(ka); removeMmClient(res); } }, 20000);
|
|
5151
|
+
req.on('close', () => { removeMmClient(res); clearInterval(ka); });
|
|
4792
5152
|
return;
|
|
4793
5153
|
}
|
|
4794
5154
|
|
|
@@ -4817,7 +5177,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4817
5177
|
const top = idx.slice(0, limitParam);
|
|
4818
5178
|
for (const entry of top) {
|
|
4819
5179
|
const _sid = String(entry.id || '').trim();
|
|
4820
|
-
if (!_sid || !/^[a-zA-Z0-9_.-]
|
|
5180
|
+
if (!_sid || !/^(?!.*\.\.)[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/.test(_sid)) continue;
|
|
4821
5181
|
let events = [];
|
|
4822
5182
|
try {
|
|
4823
5183
|
const jl = fs.readFileSync(path.join(sessDir, `${_sid}.jsonl`), 'utf8');
|
|
@@ -4965,7 +5325,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4965
5325
|
ts: Date.now(),
|
|
4966
5326
|
uptime: process.uptime(),
|
|
4967
5327
|
dir: root,
|
|
4968
|
-
sseClients:
|
|
5328
|
+
sseClients: getMmClientCount(),
|
|
4969
5329
|
activeOrgs: Object.keys(orgRuns).length,
|
|
4970
5330
|
orgRuns,
|
|
4971
5331
|
recentEvents,
|
|
@@ -5035,12 +5395,387 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
5035
5395
|
return;
|
|
5036
5396
|
}
|
|
5037
5397
|
|
|
5398
|
+
// ----------------------------------------------- GET /api/monoagent/platforms
|
|
5399
|
+
// Returns all supported platforms from `monoes connect list --all --json`
|
|
5400
|
+
if (req.method === 'GET' && url === '/api/monoagent/platforms') {
|
|
5401
|
+
try {
|
|
5402
|
+
const { execFile } = await import('child_process');
|
|
5403
|
+
const out = await new Promise((resolve, reject) => {
|
|
5404
|
+
execFile('monoes', ['connect', 'list', '--all', '--json'], { timeout: 8000 }, (err, stdout) => {
|
|
5405
|
+
if (err) reject(err); else resolve(stdout);
|
|
5406
|
+
});
|
|
5407
|
+
});
|
|
5408
|
+
// Parse + re-serialize to ensure only valid JSON reaches the client
|
|
5409
|
+
// (monoes may emit warning lines before the JSON array)
|
|
5410
|
+
let parsed; try { parsed = JSON.parse(out); } catch (_) { parsed = []; }
|
|
5411
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5412
|
+
res.end(JSON.stringify(Array.isArray(parsed) ? parsed : []));
|
|
5413
|
+
} catch (e) { res.writeHead(200); res.end('[]'); }
|
|
5414
|
+
return;
|
|
5415
|
+
}
|
|
5416
|
+
|
|
5417
|
+
// ----------------------------------------------- GET /api/monoagent/connections
|
|
5418
|
+
// Returns active API/OAuth connections + browser sessions merged
|
|
5419
|
+
if (req.method === 'GET' && url === '/api/monoagent/connections') {
|
|
5420
|
+
try {
|
|
5421
|
+
const { execFile } = await import('child_process');
|
|
5422
|
+
const [connsOut, sessOut] = await Promise.all([
|
|
5423
|
+
new Promise((resolve) => {
|
|
5424
|
+
execFile('monoes', ['connect', 'list', '--json'], { timeout: 8000 }, (err, stdout) => resolve(err ? '[]' : stdout));
|
|
5425
|
+
}),
|
|
5426
|
+
new Promise((resolve) => {
|
|
5427
|
+
execFile('monoes', ['--json', 'login', 'status'], { timeout: 8000 }, (err, stdout) => resolve(err ? '[]' : stdout));
|
|
5428
|
+
}),
|
|
5429
|
+
]);
|
|
5430
|
+
let connections = []; try { connections = JSON.parse(connsOut); } catch (_) {}
|
|
5431
|
+
let sessions = []; try { sessions = JSON.parse(sessOut); } catch (_) {}
|
|
5432
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5433
|
+
res.end(JSON.stringify({ connections, sessions }));
|
|
5434
|
+
} catch (e) { res.writeHead(200); res.end(JSON.stringify({ connections: [], sessions: [] })); }
|
|
5435
|
+
return;
|
|
5436
|
+
}
|
|
5437
|
+
|
|
5438
|
+
// ----------------------------------------------- POST /api/monoagent/login
|
|
5439
|
+
// Launches browser login for social platforms via `monoes login <platform>`
|
|
5440
|
+
if (req.method === 'POST' && url === '/api/monoagent/login') {
|
|
5441
|
+
try {
|
|
5442
|
+
let body = '';
|
|
5443
|
+
await new Promise((resolve, reject) => { req.on('data', d => { body += d; if (body.length > 65536) { req.destroy(); reject(new Error('body too large')); } }); req.on('end', resolve); req.on('error', reject); });
|
|
5444
|
+
const { id } = JSON.parse(body);
|
|
5445
|
+
if (!id || !/^[a-z][a-z0-9_-]*$/.test(id)) { res.writeHead(400); res.end(JSON.stringify({ error: 'invalid id' })); return; }
|
|
5446
|
+
const { spawn } = await import('child_process');
|
|
5447
|
+
const child = spawn('monoes', ['login', id, '--timeout', '10m'], { detached: true, stdio: 'ignore' });
|
|
5448
|
+
// Defer response until spawn confirms or errors — prevents race where error fires after res.end()
|
|
5449
|
+
child.once('spawn', () => {
|
|
5450
|
+
child.unref();
|
|
5451
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5452
|
+
res.end(JSON.stringify({ ok: true }));
|
|
5453
|
+
});
|
|
5454
|
+
child.once('error', (err) => {
|
|
5455
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
5456
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
5457
|
+
});
|
|
5458
|
+
} catch (e) { res.writeHead(500); res.end(JSON.stringify({ error: e.message })); }
|
|
5459
|
+
return;
|
|
5460
|
+
}
|
|
5461
|
+
|
|
5462
|
+
// ----------------------------------------------- POST /api/monoagent/connect
|
|
5463
|
+
if (req.method === 'POST' && url === '/api/monoagent/connect') {
|
|
5464
|
+
try {
|
|
5465
|
+
let body = '';
|
|
5466
|
+
await new Promise((resolve, reject) => { req.on('data', d => { body += d; if (body.length > 65536) { req.destroy(); reject(new Error('body too large')); } }); req.on('end', resolve); req.on('error', reject); });
|
|
5467
|
+
const { id, method, fields } = JSON.parse(body);
|
|
5468
|
+
if (!id || !/^[a-z][a-z0-9_-]*$/.test(id)) { res.writeHead(400); res.end(JSON.stringify({ error: 'invalid id' })); return; }
|
|
5469
|
+
if (method && !/^[a-z][a-z0-9_-]*$/.test(method)) { res.writeHead(400); res.end(JSON.stringify({ error: 'invalid method' })); return; }
|
|
5470
|
+
const { execFile } = await import('child_process');
|
|
5471
|
+
const args = ['connect', id];
|
|
5472
|
+
if (method) args.push('--method', method);
|
|
5473
|
+
if (fields && typeof fields === 'object') {
|
|
5474
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
5475
|
+
// Only allow simple word keys — prevents --flag injection
|
|
5476
|
+
if (!/^[a-z][a-z0-9_]*$/.test(k)) continue;
|
|
5477
|
+
args.push(`--${k}`, String(v).slice(0, 2048));
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
await new Promise((resolve, reject) => {
|
|
5481
|
+
execFile('monoes', args, { timeout: 30000 }, (err, stdout, stderr) => {
|
|
5482
|
+
const ok = !err;
|
|
5483
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5484
|
+
res.end(JSON.stringify({ ok, stdout: (stdout || '').trim(), stderr: (stderr || '').trim() }));
|
|
5485
|
+
resolve();
|
|
5486
|
+
});
|
|
5487
|
+
});
|
|
5488
|
+
} catch (e) { res.writeHead(500); res.end(JSON.stringify({ error: e.message })); }
|
|
5489
|
+
return;
|
|
5490
|
+
}
|
|
5491
|
+
|
|
5492
|
+
// ----------------------------------------------- POST /api/monoagent/test
|
|
5493
|
+
if (req.method === 'POST' && url === '/api/monoagent/test') {
|
|
5494
|
+
try {
|
|
5495
|
+
let body = '';
|
|
5496
|
+
await new Promise((resolve, reject) => { req.on('data', d => { body += d; if (body.length > 65536) { req.destroy(); reject(new Error('body too large')); } }); req.on('end', resolve); req.on('error', reject); });
|
|
5497
|
+
const { id } = JSON.parse(body);
|
|
5498
|
+
if (!id || !/^[a-z0-9][a-z0-9_:-]*$/.test(id)) { res.writeHead(400); res.end(JSON.stringify({ error: 'id required' })); return; }
|
|
5499
|
+
const { execFile } = await import('child_process');
|
|
5500
|
+
await new Promise((resolve, reject) => {
|
|
5501
|
+
execFile('monoes', ['connect', 'test', id], { timeout: 15000 }, (err, stdout, stderr) => {
|
|
5502
|
+
if (err) reject(new Error(stderr || err.message)); else resolve(stdout);
|
|
5503
|
+
});
|
|
5504
|
+
});
|
|
5505
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5506
|
+
res.end(JSON.stringify({ ok: true }));
|
|
5507
|
+
} catch (e) { res.writeHead(200); res.end(JSON.stringify({ ok: false, error: e.message })); }
|
|
5508
|
+
return;
|
|
5509
|
+
}
|
|
5510
|
+
|
|
5511
|
+
// ----------------------------------------------- POST /api/monoagent/disconnect
|
|
5512
|
+
if (req.method === 'POST' && url === '/api/monoagent/disconnect') {
|
|
5513
|
+
try {
|
|
5514
|
+
let body = '';
|
|
5515
|
+
await new Promise((resolve, reject) => { req.on('data', d => { body += d; if (body.length > 65536) { req.destroy(); reject(new Error('body too large')); } }); req.on('end', resolve); req.on('error', reject); });
|
|
5516
|
+
const { id, type } = JSON.parse(body);
|
|
5517
|
+
if (!id || !/^[a-z0-9][a-z0-9_:-]*$/.test(id)) { res.writeHead(400); res.end(JSON.stringify({ error: 'id required' })); return; }
|
|
5518
|
+
if (type !== 'session' && type !== 'connection') { res.writeHead(400); res.end(JSON.stringify({ error: 'invalid type' })); return; }
|
|
5519
|
+
const { execFile } = await import('child_process');
|
|
5520
|
+
const cmd = type === 'session' ? ['logout', id] : ['connect', 'remove', id];
|
|
5521
|
+
await new Promise((resolve, reject) => {
|
|
5522
|
+
execFile('monoes', cmd, { timeout: 10000 }, (err, _stdout, stderr) => {
|
|
5523
|
+
if (err) reject(new Error(stderr || err.message)); else resolve();
|
|
5524
|
+
});
|
|
5525
|
+
});
|
|
5526
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5527
|
+
res.end(JSON.stringify({ ok: true }));
|
|
5528
|
+
} catch (e) { res.writeHead(200); res.end(JSON.stringify({ ok: false, error: e.message })); }
|
|
5529
|
+
return;
|
|
5530
|
+
}
|
|
5531
|
+
|
|
5532
|
+
// ------------------------------------------------- POST /api/playbooks
|
|
5533
|
+
// Save a playbook definition to .monomind/playbooks/<id>.json
|
|
5534
|
+
if (req.method === 'POST' && url === '/api/playbooks') {
|
|
5535
|
+
try {
|
|
5536
|
+
let body = '';
|
|
5537
|
+
await new Promise((resolve, reject) => {
|
|
5538
|
+
req.on('data', d => { body += d; });
|
|
5539
|
+
req.on('end', resolve);
|
|
5540
|
+
req.on('error', reject);
|
|
5541
|
+
});
|
|
5542
|
+
const pb = JSON.parse(body);
|
|
5543
|
+
if (!pb.id || !pb.name) {
|
|
5544
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
5545
|
+
res.end(JSON.stringify({ error: 'id and name are required' }));
|
|
5546
|
+
return;
|
|
5547
|
+
}
|
|
5548
|
+
const dir = projectDir || process.cwd();
|
|
5549
|
+
const playbookDir = path.join(dir, '.monomind', 'playbooks');
|
|
5550
|
+
fs.mkdirSync(playbookDir, { recursive: true });
|
|
5551
|
+
const filePath = path.join(playbookDir, pb.id + '.json');
|
|
5552
|
+
fs.writeFileSync(filePath, JSON.stringify(pb, null, 2));
|
|
5553
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5554
|
+
res.end(JSON.stringify({ ok: true, id: pb.id, file: pb.id + '.json' }));
|
|
5555
|
+
} catch (e) { res.writeHead(500); res.end(JSON.stringify({ error: e.message })); }
|
|
5556
|
+
return;
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5559
|
+
// ------------------------------------------------- GET /api/workflow-defs
|
|
5560
|
+
if (req.method === 'GET' && url === '/api/workflow-defs') {
|
|
5561
|
+
try {
|
|
5562
|
+
const qp = new URL(req.url, 'http://x').searchParams;
|
|
5563
|
+
const dir = qp.get('dir') || projectDir || process.cwd();
|
|
5564
|
+
const playbookDir = path.join(dir, '.monomind', 'playbooks');
|
|
5565
|
+
const result = [];
|
|
5566
|
+
if (fs.existsSync(playbookDir)) {
|
|
5567
|
+
const files = fs.readdirSync(playbookDir).filter(f => f.endsWith('.json'));
|
|
5568
|
+
for (const file of files) {
|
|
5569
|
+
try {
|
|
5570
|
+
const fpath = path.join(playbookDir, file);
|
|
5571
|
+
const stat = fs.statSync(fpath);
|
|
5572
|
+
const def = JSON.parse(fs.readFileSync(fpath, 'utf8'));
|
|
5573
|
+
const params = (def.params || []).map(p => typeof p === 'string' ? p : (p.name || p.key || ''));
|
|
5574
|
+
result.push({
|
|
5575
|
+
id: def.id || file.replace('.json', ''),
|
|
5576
|
+
name: def.name || file.replace('.json', ''),
|
|
5577
|
+
description: def.description || null,
|
|
5578
|
+
file,
|
|
5579
|
+
nodeCount: Array.isArray(def.nodes) ? def.nodes.length : 0,
|
|
5580
|
+
params,
|
|
5581
|
+
modifiedAt: stat.mtimeMs,
|
|
5582
|
+
});
|
|
5583
|
+
} catch (_) {}
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5587
|
+
res.end(JSON.stringify(result));
|
|
5588
|
+
} catch (e) { res.writeHead(500); res.end(JSON.stringify({ error: e.message })); }
|
|
5589
|
+
return;
|
|
5590
|
+
}
|
|
5591
|
+
|
|
5592
|
+
// ------------------------------------------------- GET /api/workflow-runs
|
|
5593
|
+
if (req.method === 'GET' && url === '/api/workflow-runs') {
|
|
5594
|
+
// Reads from ~/.monomind/browse-runs.json written by the monobrowse dashboard server.
|
|
5595
|
+
try {
|
|
5596
|
+
const runsFile = path.join(os.homedir(), '.monomind', 'browse-runs.json');
|
|
5597
|
+
if (fs.existsSync(runsFile)) {
|
|
5598
|
+
const raw = fs.readFileSync(runsFile, 'utf-8');
|
|
5599
|
+
const runs = JSON.parse(raw);
|
|
5600
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5601
|
+
res.end(JSON.stringify(Array.isArray(runs) ? runs : []));
|
|
5602
|
+
} else {
|
|
5603
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5604
|
+
res.end('[]');
|
|
5605
|
+
}
|
|
5606
|
+
} catch {
|
|
5607
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5608
|
+
res.end('[]');
|
|
5609
|
+
}
|
|
5610
|
+
return;
|
|
5611
|
+
}
|
|
5612
|
+
|
|
5613
|
+
// ---- POST /api/orgs/:name/mark-complete — manual STALE recovery ----
|
|
5614
|
+
if (req.method === 'POST' && /^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/mark-complete$/i.test(url)) {
|
|
5615
|
+
const _mcOrgName = decodeURIComponent(url.split('/')[3]);
|
|
5616
|
+
if (_mcOrgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_mcOrgName)) {
|
|
5617
|
+
res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid org name' })); return;
|
|
5618
|
+
}
|
|
5619
|
+
const _mcRoot = projectDir || process.cwd();
|
|
5620
|
+
const _mcMonoDir = _getGitMonomindDir(_mcRoot) || path.join(_mcRoot, '.monomind');
|
|
5621
|
+
const _mcRunId = activeOrgRuns.get(_mcOrgName) || _getActiveRunId(_mcOrgName, _mcRoot);
|
|
5622
|
+
if (!_mcRunId) {
|
|
5623
|
+
res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'No active run for org: ' + _mcOrgName })); return;
|
|
5624
|
+
}
|
|
5625
|
+
const _mcEvent = { type: 'run:complete', org: _mcOrgName, runId: _mcRunId, ts: Date.now(), reason: 'manual' };
|
|
5626
|
+
try {
|
|
5627
|
+
const _mcRunFile = path.join(_mcMonoDir, 'orgs', _mcOrgName, 'runs', `${_mcRunId}.jsonl`);
|
|
5628
|
+
if (fs.existsSync(_mcRunFile)) await appendToFile(_mcRunFile, JSON.stringify(_mcEvent) + '\n');
|
|
5629
|
+
activeOrgRuns.delete(_mcOrgName);
|
|
5630
|
+
// Clean up ppid-keyed active-run files for this org
|
|
5631
|
+
const _mcCapDir = path.join(MONOMIND_HOME, '.monomind', 'capture');
|
|
5632
|
+
try {
|
|
5633
|
+
const _mcPpidDir = path.join(_mcCapDir, 'active-runs');
|
|
5634
|
+
if (fs.existsSync(_mcPpidDir)) {
|
|
5635
|
+
fs.readdirSync(_mcPpidDir).filter(f => f.endsWith('.json')).forEach(_pf => {
|
|
5636
|
+
try { const _pd = JSON.parse(fs.readFileSync(path.join(_mcPpidDir, _pf), 'utf8')); if (_pd.org === _mcOrgName) fs.unlinkSync(path.join(_mcPpidDir, _pf)); } catch (_) {}
|
|
5637
|
+
});
|
|
5638
|
+
}
|
|
5639
|
+
const _mcActiveFile = path.join(_mcCapDir, 'active-run.json');
|
|
5640
|
+
if (fs.existsSync(_mcActiveFile)) {
|
|
5641
|
+
try { const _a = JSON.parse(fs.readFileSync(_mcActiveFile, 'utf8')); if (_a.org === _mcOrgName) fs.unlinkSync(_mcActiveFile); } catch (_) {}
|
|
5642
|
+
}
|
|
5643
|
+
} catch (_) {}
|
|
5644
|
+
_updateRunState(_mcEvent, _mcRoot);
|
|
5645
|
+
broadcastMm(_mcEvent);
|
|
5646
|
+
const _mcFwdClients = runStreamClients.get(_mcOrgName);
|
|
5647
|
+
if (_mcFwdClients && _mcFwdClients.size > 0) {
|
|
5648
|
+
const _mcLine = `data: ${JSON.stringify(_mcEvent)}\n\n`;
|
|
5649
|
+
for (const _cl of _mcFwdClients) { try { _cl.write(_mcLine); } catch (_) { _mcFwdClients.delete(_cl); } }
|
|
5650
|
+
}
|
|
5651
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
5652
|
+
res.end(JSON.stringify({ ok: true, runId: _mcRunId }));
|
|
5653
|
+
} catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message })); }
|
|
5654
|
+
return;
|
|
5655
|
+
}
|
|
5656
|
+
|
|
5657
|
+
// ---- GET /api/orgs/:name/runs/current/stream — Phase 3 streaming tail ----
|
|
5658
|
+
if (req.method === 'GET' && /^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/runs\/current\/stream$/i.test(url)) {
|
|
5659
|
+
const _stOrgName = decodeURIComponent(url.split('/')[3]);
|
|
5660
|
+
if (_stOrgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_stOrgName)) {
|
|
5661
|
+
res.writeHead(400); res.end('Invalid org name'); return;
|
|
5662
|
+
}
|
|
5663
|
+
const _stQs = new URL(req.url, 'http://localhost').searchParams;
|
|
5664
|
+
const _stSince = Math.max(0, parseInt(_stQs.get('since') || '0', 10) || 0);
|
|
5665
|
+
res.writeHead(200, {
|
|
5666
|
+
'Content-Type': 'text/event-stream',
|
|
5667
|
+
'Cache-Control': 'no-cache',
|
|
5668
|
+
'Connection': 'keep-alive',
|
|
5669
|
+
'Access-Control-Allow-Origin': '*',
|
|
5670
|
+
'X-Accel-Buffering': 'no',
|
|
5671
|
+
});
|
|
5672
|
+
res.write(': connected\n\n');
|
|
5673
|
+
// Register client for live events
|
|
5674
|
+
if (!runStreamClients.has(_stOrgName)) runStreamClients.set(_stOrgName, new Set());
|
|
5675
|
+
runStreamClients.get(_stOrgName).add(res);
|
|
5676
|
+
// Replay events since `since` (SQLite row id cursor; falls back to JSONL line offset)
|
|
5677
|
+
try {
|
|
5678
|
+
if (_runDb) {
|
|
5679
|
+
// SQLite path: cursor is last row id seen (client sends 0 on first connect)
|
|
5680
|
+
const _stStmt = _runDb.prepare(
|
|
5681
|
+
'SELECT id, raw FROM run_events WHERE org=? AND id > ? ORDER BY id LIMIT 2000'
|
|
5682
|
+
);
|
|
5683
|
+
_stStmt.bind([_stOrgName, _stSince]);
|
|
5684
|
+
let _stLastId = _stSince;
|
|
5685
|
+
while (_stStmt.step()) {
|
|
5686
|
+
const _stRow = _stStmt.getAsObject();
|
|
5687
|
+
try { res.write(`data: ${_stRow.raw}\n\n`); _stLastId = _stRow.id; } catch (_) { break; }
|
|
5688
|
+
}
|
|
5689
|
+
_stStmt.free();
|
|
5690
|
+
res.write(`data: ${JSON.stringify({ type: 'stream:replay-done', count: _stLastId })}\n\n`);
|
|
5691
|
+
} else {
|
|
5692
|
+
// JSONL fallback: since = 0-based line offset
|
|
5693
|
+
const _stRoot = projectDir || process.cwd();
|
|
5694
|
+
const _stRunId = activeOrgRuns.get(_stOrgName) || _getActiveRunId(_stOrgName, _stRoot);
|
|
5695
|
+
if (_stRunId) {
|
|
5696
|
+
const _stMono = _getGitMonomindDir(_stRoot) || path.join(_stRoot, '.monomind');
|
|
5697
|
+
const _stRunFile = path.join(_stMono, 'orgs', _stOrgName, 'runs', `${_stRunId}.jsonl`);
|
|
5698
|
+
if (fs.existsSync(_stRunFile)) {
|
|
5699
|
+
const _stLines = fs.readFileSync(_stRunFile, 'utf8').trim().split('\n').filter(Boolean);
|
|
5700
|
+
for (let _i = _stSince; _i < _stLines.length; _i++) {
|
|
5701
|
+
try { res.write(`data: ${_stLines[_i]}\n\n`); } catch (_) { break; }
|
|
5702
|
+
}
|
|
5703
|
+
res.write(`data: ${JSON.stringify({ type: 'stream:replay-done', count: _stLines.length })}\n\n`);
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
} catch (_) {}
|
|
5708
|
+
const _stKa = setInterval(() => { try { res.write(': ping\n\n'); } catch (_) { clearInterval(_stKa); } }, 20000);
|
|
5709
|
+
req.on('close', () => {
|
|
5710
|
+
clearInterval(_stKa);
|
|
5711
|
+
const _stClients = runStreamClients.get(_stOrgName);
|
|
5712
|
+
if (_stClients) { _stClients.delete(res); if (_stClients.size === 0) runStreamClients.delete(_stOrgName); }
|
|
5713
|
+
});
|
|
5714
|
+
return;
|
|
5715
|
+
}
|
|
5716
|
+
|
|
5038
5717
|
// ------------------------------------------------------------------ 404
|
|
5039
5718
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
5040
5719
|
res.end('Not found');
|
|
5041
5720
|
});
|
|
5042
5721
|
|
|
5043
|
-
//
|
|
5722
|
+
// ── Gap-fill ordering (ADR Issue 7): rebuild activeOrgRuns BEFORE the server
|
|
5723
|
+
// starts accepting connections so the first incoming event already has runId context.
|
|
5724
|
+
// Uses SQLite when available; falls back to JSONL scan.
|
|
5725
|
+
await _initRunDb(MONOMIND_HOME);
|
|
5726
|
+
try {
|
|
5727
|
+
if (_runDb) {
|
|
5728
|
+
// SQLite gap-fill: for each org, find latest run_id and check if it has run:complete
|
|
5729
|
+
const _gfOrgsStmt = _runDb.prepare('SELECT DISTINCT org FROM run_events');
|
|
5730
|
+
while (_gfOrgsStmt.step()) {
|
|
5731
|
+
const _gfOrg = _gfOrgsStmt.getAsObject().org;
|
|
5732
|
+
if (!_gfOrg || !/^[a-z0-9][a-z0-9_-]*$/i.test(_gfOrg)) continue;
|
|
5733
|
+
// Resolve the latest run_id for this org, then check if it has a terminal event
|
|
5734
|
+
const _gfLatRunStmt = _runDb.prepare(
|
|
5735
|
+
"SELECT run_id FROM run_events WHERE org=? ORDER BY id DESC LIMIT 1"
|
|
5736
|
+
);
|
|
5737
|
+
_gfLatRunStmt.bind([_gfOrg]);
|
|
5738
|
+
let _gfLatestRun = null;
|
|
5739
|
+
if (_gfLatRunStmt.step()) _gfLatestRun = _gfLatRunStmt.getAsObject().run_id;
|
|
5740
|
+
_gfLatRunStmt.free();
|
|
5741
|
+
let _gfDone = false;
|
|
5742
|
+
if (_gfLatestRun) {
|
|
5743
|
+
const _gfRunStmt = _runDb.prepare(
|
|
5744
|
+
"SELECT type FROM run_events WHERE org=? AND run_id=? AND type IN ('run:complete','org:complete','org:stop') LIMIT 1"
|
|
5745
|
+
);
|
|
5746
|
+
_gfRunStmt.bind([_gfOrg, _gfLatestRun]);
|
|
5747
|
+
if (_gfRunStmt.step()) _gfDone = true;
|
|
5748
|
+
_gfRunStmt.free();
|
|
5749
|
+
}
|
|
5750
|
+
if (_gfLatestRun && !_gfDone) activeOrgRuns.set(_gfOrg, _gfLatestRun);
|
|
5751
|
+
}
|
|
5752
|
+
_gfOrgsStmt.free();
|
|
5753
|
+
} else {
|
|
5754
|
+
// JSONL fallback
|
|
5755
|
+
const _gfOrgsDir = path.join(MONOMIND_HOME, '.monomind', 'orgs');
|
|
5756
|
+
if (fs.existsSync(_gfOrgsDir)) {
|
|
5757
|
+
for (const _gfOrg of fs.readdirSync(_gfOrgsDir)) {
|
|
5758
|
+
if (!_gfOrg || _gfOrg.startsWith('.') || !/^[a-z0-9][a-z0-9_-]*$/i.test(_gfOrg)) continue;
|
|
5759
|
+
const _gfRunsDir = path.join(_gfOrgsDir, _gfOrg, 'runs');
|
|
5760
|
+
if (!fs.existsSync(_gfRunsDir)) continue;
|
|
5761
|
+
const _gfFiles = fs.readdirSync(_gfRunsDir)
|
|
5762
|
+
.filter(f => f.endsWith('.jsonl') && !f.endsWith('.convs.jsonl'))
|
|
5763
|
+
.sort().reverse();
|
|
5764
|
+
for (const _gfF of _gfFiles.slice(0, 5)) {
|
|
5765
|
+
try {
|
|
5766
|
+
const _gfId = _gfF.replace('.jsonl', '');
|
|
5767
|
+
const _gfContent = fs.readFileSync(path.join(_gfRunsDir, _gfF), 'utf8');
|
|
5768
|
+
const _gfLast = _gfContent.trim().split('\n').filter(Boolean).slice(-10);
|
|
5769
|
+
const _gfDone = _gfLast.some(l => { try { const e = JSON.parse(l); return e.type === 'run:complete' || e.type === 'org:complete'; } catch { return false; } });
|
|
5770
|
+
if (!_gfDone) { activeOrgRuns.set(_gfOrg, _gfId); break; }
|
|
5771
|
+
} catch (_) {}
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5775
|
+
}
|
|
5776
|
+
} catch (_) {}
|
|
5777
|
+
|
|
5778
|
+
// Bind to available port (after activeOrgRuns is populated — no race window)
|
|
5044
5779
|
const boundPort = await bindServer(server, port);
|
|
5045
5780
|
const url = `http://localhost:${boundPort}`;
|
|
5046
5781
|
|
|
@@ -5059,7 +5794,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
5059
5794
|
const _migIndex = [];
|
|
5060
5795
|
for (const sess of (_migOld || [])) {
|
|
5061
5796
|
const _msid = String(sess.id || '').trim();
|
|
5062
|
-
if (!_msid || !/^[a-zA-Z0-9_.-]
|
|
5797
|
+
if (!_msid || !/^(?!.*\.\.)[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/.test(_msid)) continue;
|
|
5063
5798
|
// Write per-session JSONL
|
|
5064
5799
|
const _mEvts = (sess.events || []);
|
|
5065
5800
|
const _mLines = _mEvts.map(e => JSON.stringify(e)).join('\n');
|
|
@@ -5075,32 +5810,6 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
5075
5810
|
}
|
|
5076
5811
|
} catch (_) {}
|
|
5077
5812
|
|
|
5078
|
-
// Rebuild activeOrgRuns from disk so event enrichment (runId injection) still works
|
|
5079
|
-
// after a server restart. Without this, org events emitted mid-run that lack runId
|
|
5080
|
-
// are broadcast without it and _odtHandleLiveEvent drops them.
|
|
5081
|
-
try {
|
|
5082
|
-
const _rbOrgsDir = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
|
|
5083
|
-
if (fs.existsSync(_rbOrgsDir)) {
|
|
5084
|
-
for (const _rbOrg of fs.readdirSync(_rbOrgsDir)) {
|
|
5085
|
-
if (!_rbOrg || _rbOrg.startsWith('.') || !/^[a-z0-9][a-z0-9_-]*$/i.test(_rbOrg)) continue;
|
|
5086
|
-
const _rbRunsDir = path.join(_rbOrgsDir, _rbOrg, 'runs');
|
|
5087
|
-
if (!fs.existsSync(_rbRunsDir)) continue;
|
|
5088
|
-
const _rbFiles = fs.readdirSync(_rbRunsDir)
|
|
5089
|
-
.filter(f => f.endsWith('.jsonl') && !f.endsWith('.convs.jsonl'))
|
|
5090
|
-
.sort().reverse();
|
|
5091
|
-
for (const _rbF of _rbFiles.slice(0, 5)) {
|
|
5092
|
-
try {
|
|
5093
|
-
const _rbId = _rbF.replace('.jsonl', '');
|
|
5094
|
-
const _rbContent = fs.readFileSync(path.join(_rbRunsDir, _rbF), 'utf8');
|
|
5095
|
-
const _rbLast = _rbContent.trim().split('\n').filter(Boolean).slice(-10);
|
|
5096
|
-
const _rbDone = _rbLast.some(l => { try { const e = JSON.parse(l); return e.type === 'run:complete' || e.type === 'org:complete'; } catch { return false; } });
|
|
5097
|
-
if (!_rbDone) { activeOrgRuns.set(_rbOrg, _rbId); break; }
|
|
5098
|
-
} catch (_) {}
|
|
5099
|
-
}
|
|
5100
|
-
}
|
|
5101
|
-
}
|
|
5102
|
-
} catch (_) {}
|
|
5103
|
-
|
|
5104
5813
|
// ---------------------------------------------------------------- Watchers
|
|
5105
5814
|
let debounceTimer = null;
|
|
5106
5815
|
let pendingSections = new Set();
|
|
@@ -5129,6 +5838,128 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
5129
5838
|
}
|
|
5130
5839
|
}
|
|
5131
5840
|
|
|
5841
|
+
// ── Phase 1: fs.watch orgs dir — pick up run events written directly to JSONL files
|
|
5842
|
+
// without going through the HTTP endpoint (e.g. when runorg.md bash writes run:start directly).
|
|
5843
|
+
// Also forwards new bytes to per-org SSE clients (runStreamClients) so the chat tab
|
|
5844
|
+
// receives bash-written lifecycle events in real-time (Phase 3 gap-fill).
|
|
5845
|
+
const _orgsFileSizes = new Map(); // absPath → last known byte offset
|
|
5846
|
+
function _readNewOrgLines(absPath, orgName, runId) {
|
|
5847
|
+
try {
|
|
5848
|
+
const stat = fs.statSync(absPath);
|
|
5849
|
+
const prevSize = _orgsFileSizes.get(absPath) || 0;
|
|
5850
|
+
if (stat.size <= prevSize) return; // nothing new
|
|
5851
|
+
_orgsFileSizes.set(absPath, stat.size);
|
|
5852
|
+
// Read only the new bytes to avoid re-processing existing lines
|
|
5853
|
+
const fd = fs.openSync(absPath, 'r');
|
|
5854
|
+
const newLen = stat.size - prevSize;
|
|
5855
|
+
const buf = Buffer.alloc(newLen);
|
|
5856
|
+
fs.readSync(fd, buf, 0, newLen, prevSize);
|
|
5857
|
+
fs.closeSync(fd);
|
|
5858
|
+
const newText = buf.toString('utf8');
|
|
5859
|
+
const newLines = newText.split('\n').filter(Boolean);
|
|
5860
|
+
const clients = runStreamClients.get(orgName);
|
|
5861
|
+
for (const _rawLine of newLines) {
|
|
5862
|
+
let ev;
|
|
5863
|
+
try { ev = JSON.parse(_rawLine); } catch { continue; }
|
|
5864
|
+
if (!ev || !ev.type) continue;
|
|
5865
|
+
// Index in SQLite (watcher path — bash-written lifecycle events)
|
|
5866
|
+
if (!ev.org) ev.org = orgName;
|
|
5867
|
+
if (!ev.runId) ev.runId = runId;
|
|
5868
|
+
_insertRunEvent(ev, 'watcher');
|
|
5869
|
+
// Update activeOrgRuns based on file-watcher evidence
|
|
5870
|
+
if ((ev.type === 'run:start' || ev.type === 'org:start') && ev.runId) {
|
|
5871
|
+
activeOrgRuns.set(orgName, String(ev.runId).trim());
|
|
5872
|
+
} else if (ev.type === 'run:complete' || ev.type === 'org:complete' || ev.type === 'org:stop') {
|
|
5873
|
+
activeOrgRuns.delete(orgName);
|
|
5874
|
+
}
|
|
5875
|
+
// Forward to per-org SSE clients so the chat tab gets live bash-written events
|
|
5876
|
+
if (clients && clients.size > 0) {
|
|
5877
|
+
const _sseData = `data: ${_rawLine}\n\n`;
|
|
5878
|
+
for (const _cl of clients) { try { _cl.write(_sseData); } catch (_) { clients.delete(_cl); } }
|
|
5879
|
+
}
|
|
5880
|
+
// Also broadcast to mastermind-stream for the org activity strip
|
|
5881
|
+
if (ev.org && ev.org === orgName) broadcastMm({ ...ev, _fromWatcher: true });
|
|
5882
|
+
}
|
|
5883
|
+
} catch (_) {}
|
|
5884
|
+
}
|
|
5885
|
+
|
|
5886
|
+
function watchOrgsDir() {
|
|
5887
|
+
const _orgsDir = path.join(MONOMIND_HOME, '.monomind', 'orgs');
|
|
5888
|
+
if (!fs.existsSync(_orgsDir)) {
|
|
5889
|
+
// Orgs dir may not exist yet; watch parent and re-try when it appears
|
|
5890
|
+
const _parentDir = path.join(MONOMIND_HOME, '.monomind');
|
|
5891
|
+
if (fs.existsSync(_parentDir)) {
|
|
5892
|
+
try {
|
|
5893
|
+
fs.watch(_parentDir, (_evType, _fname) => {
|
|
5894
|
+
if (_fname === 'orgs' && fs.existsSync(_orgsDir)) watchOrgsDir();
|
|
5895
|
+
});
|
|
5896
|
+
} catch (_) {}
|
|
5897
|
+
}
|
|
5898
|
+
return;
|
|
5899
|
+
}
|
|
5900
|
+
// Seed initial file sizes so the watcher only forwards NEW bytes after startup
|
|
5901
|
+
try {
|
|
5902
|
+
for (const _org of fs.readdirSync(_orgsDir)) {
|
|
5903
|
+
const _runsDir = path.join(_orgsDir, _org, 'runs');
|
|
5904
|
+
if (!fs.existsSync(_runsDir)) continue;
|
|
5905
|
+
for (const _f of fs.readdirSync(_runsDir).filter(f => f.endsWith('.jsonl') && !f.endsWith('.warm.jsonl') && !f.endsWith('.convs.jsonl'))) {
|
|
5906
|
+
try { _orgsFileSizes.set(path.join(_runsDir, _f), fs.statSync(path.join(_runsDir, _f)).size); } catch (_) {}
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
5909
|
+
} catch (_) {}
|
|
5910
|
+
// Use chokidar when available (Linux requires it — fs.watch { recursive } is macOS/Windows only).
|
|
5911
|
+
// Falls back to fs.watch for environments where chokidar is absent.
|
|
5912
|
+
let _watcherStarted = false;
|
|
5913
|
+
try {
|
|
5914
|
+
const chokidar = _require('chokidar');
|
|
5915
|
+
const _chokidarWatcher = chokidar.watch(_orgsDir, {
|
|
5916
|
+
persistent: false,
|
|
5917
|
+
ignoreInitial: true,
|
|
5918
|
+
depth: 3,
|
|
5919
|
+
ignored: (p) => {
|
|
5920
|
+
const b = path.basename(p);
|
|
5921
|
+
return b.endsWith('.warm.jsonl') || b.endsWith('.convs.jsonl') || b.startsWith('.');
|
|
5922
|
+
},
|
|
5923
|
+
awaitWriteFinish: false,
|
|
5924
|
+
});
|
|
5925
|
+
const _handleChokidarPath = (absPath) => {
|
|
5926
|
+
if (!absPath.endsWith('.jsonl')) return;
|
|
5927
|
+
const rel = path.relative(_orgsDir, absPath).replace(/\\/g, '/');
|
|
5928
|
+
const parts = rel.split('/');
|
|
5929
|
+
if (parts.length >= 3 && parts[1] === 'runs') {
|
|
5930
|
+
const _wOrgName = parts[0];
|
|
5931
|
+
const _wRunId = parts[2].replace('.jsonl', '');
|
|
5932
|
+
if (_wOrgName && _wRunId && /^[a-z0-9][a-z0-9_-]*$/i.test(_wOrgName) && /^[a-z0-9][a-z0-9_-]*$/i.test(_wRunId)) {
|
|
5933
|
+
_readNewOrgLines(absPath, _wOrgName, _wRunId);
|
|
5934
|
+
}
|
|
5935
|
+
}
|
|
5936
|
+
};
|
|
5937
|
+
_chokidarWatcher.on('add', _handleChokidarPath);
|
|
5938
|
+
_chokidarWatcher.on('change', _handleChokidarPath);
|
|
5939
|
+
activeWatchers.push({ close: () => _chokidarWatcher.close() });
|
|
5940
|
+
_watcherStarted = true;
|
|
5941
|
+
} catch (_chokidarErr) { /* chokidar unavailable — fall through to fs.watch */ }
|
|
5942
|
+
if (!_watcherStarted) {
|
|
5943
|
+
try {
|
|
5944
|
+
const _orgsWatcher = fs.watch(_orgsDir, { recursive: true, persistent: false }, (_evType, _fname) => {
|
|
5945
|
+
if (!_fname || !_fname.endsWith('.jsonl') || _fname.endsWith('.warm.jsonl') || _fname.endsWith('.convs.jsonl')) return;
|
|
5946
|
+
const _parts = _fname.replace(/\\/g, '/').split('/');
|
|
5947
|
+
if (_parts.length >= 3 && _parts[1] === 'runs') {
|
|
5948
|
+
const _wOrgName = _parts[0];
|
|
5949
|
+
const _wRunId = _parts[2].replace('.jsonl', '');
|
|
5950
|
+
if (_wOrgName && _wRunId && /^[a-z0-9][a-z0-9_-]*$/i.test(_wOrgName) && /^[a-z0-9][a-z0-9_-]*$/i.test(_wRunId)) {
|
|
5951
|
+
_readNewOrgLines(path.join(_orgsDir, _fname.replace(/\\/g, '/')), _wOrgName, _wRunId);
|
|
5952
|
+
}
|
|
5953
|
+
}
|
|
5954
|
+
});
|
|
5955
|
+
activeWatchers.push(_orgsWatcher);
|
|
5956
|
+
} catch (_wErr) {
|
|
5957
|
+
console.warn('[monomind] watchOrgsDir: both chokidar and fs.watch failed — bash-written lifecycle events will not reach SSE clients. HTTP-posted events still work via spool DLQ.');
|
|
5958
|
+
}
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
watchOrgsDir();
|
|
5962
|
+
|
|
5132
5963
|
// Watch .claude/sessions/ if present
|
|
5133
5964
|
const claudeSessionsDir = path.join(projectDir || process.cwd(), '.claude', 'sessions');
|
|
5134
5965
|
if (fs.existsSync(claudeSessionsDir)) {
|
|
@@ -5140,6 +5971,134 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
5140
5971
|
}
|
|
5141
5972
|
}
|
|
5142
5973
|
|
|
5974
|
+
// ── Phase 2: Spool polling — replay undelivered events from capture-handler (Issue 5) ──
|
|
5975
|
+
// capture-handler writes events to spool/ before HTTP POST. If the POST fails (server
|
|
5976
|
+
// down, timeout), the file stays. We poll every 5s and replay them.
|
|
5977
|
+
const _spoolBaseDir = path.join(MONOMIND_HOME, '.monomind', 'capture', 'spool');
|
|
5978
|
+
const _spoolTimer = setInterval(() => {
|
|
5979
|
+
if (!fs.existsSync(_spoolBaseDir)) return;
|
|
5980
|
+
try {
|
|
5981
|
+
const _spoolFiles = fs.readdirSync(_spoolBaseDir)
|
|
5982
|
+
.filter(f => f.endsWith('.json') && !f.startsWith('.'))
|
|
5983
|
+
.sort() // chronological (timestamp prefix)
|
|
5984
|
+
.slice(0, 20); // max 20 per cycle to avoid flooding
|
|
5985
|
+
for (const _sf of _spoolFiles) {
|
|
5986
|
+
const _sfPath = path.join(_spoolBaseDir, _sf);
|
|
5987
|
+
try {
|
|
5988
|
+
const _spoolEvent = JSON.parse(fs.readFileSync(_sfPath, 'utf8'));
|
|
5989
|
+
const _spoolBody = JSON.stringify(_spoolEvent);
|
|
5990
|
+
const _spoolReq = http.request({
|
|
5991
|
+
hostname: 'localhost', port: boundPort,
|
|
5992
|
+
path: '/api/mastermind/event', method: 'POST',
|
|
5993
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(_spoolBody) },
|
|
5994
|
+
}, (_spoolRes) => {
|
|
5995
|
+
// Delete only after confirmed delivery; leave file on failure for next poll cycle
|
|
5996
|
+
if (_spoolRes.statusCode >= 200 && _spoolRes.statusCode < 300) {
|
|
5997
|
+
try { fs.unlinkSync(_sfPath); } catch (_) {}
|
|
5998
|
+
}
|
|
5999
|
+
_spoolRes.resume();
|
|
6000
|
+
});
|
|
6001
|
+
_spoolReq.on('error', () => {});
|
|
6002
|
+
_spoolReq.setTimeout(2000, () => { _spoolReq.destroy(); });
|
|
6003
|
+
_spoolReq.write(_spoolBody);
|
|
6004
|
+
_spoolReq.end();
|
|
6005
|
+
} catch (_e) {}
|
|
6006
|
+
}
|
|
6007
|
+
} catch (_) {}
|
|
6008
|
+
}, 5000);
|
|
6009
|
+
// Clean up spool files older than 8 hours on startup (stale captures from crashed sessions)
|
|
6010
|
+
try {
|
|
6011
|
+
if (fs.existsSync(_spoolBaseDir)) {
|
|
6012
|
+
const _staleMs = 8 * 60 * 60 * 1000;
|
|
6013
|
+
fs.readdirSync(_spoolBaseDir).filter(f => f.endsWith('.json')).forEach(_staleF => {
|
|
6014
|
+
const _staleP = path.join(_spoolBaseDir, _staleF);
|
|
6015
|
+
try {
|
|
6016
|
+
if (Date.now() - fs.statSync(_staleP).mtimeMs > _staleMs) fs.unlinkSync(_staleP);
|
|
6017
|
+
} catch (_) {}
|
|
6018
|
+
});
|
|
6019
|
+
}
|
|
6020
|
+
} catch (_) {}
|
|
6021
|
+
|
|
6022
|
+
// ── Phase 3: Read-batch polling — aggregate file-read events from capture-handler (Issue 9) ──
|
|
6023
|
+
// capture-handler writes Read tool calls to capture/read-batch-{ppid}-{pid}.json (per-subagent, no sharing).
|
|
6024
|
+
// Server polls every 3s, aggregates all matching files per session, emits agent:read:batch, removes files.
|
|
6025
|
+
const _rbDir = path.join(MONOMIND_HOME, '.monomind', 'capture');
|
|
6026
|
+
const _rbTimer = setInterval(() => {
|
|
6027
|
+
if (!fs.existsSync(_rbDir)) return;
|
|
6028
|
+
try {
|
|
6029
|
+
fs.readdirSync(_rbDir)
|
|
6030
|
+
.filter(f => f.startsWith('read-batch-') && f.endsWith('.json'))
|
|
6031
|
+
.forEach(_rbf => {
|
|
6032
|
+
const _rbPath = path.join(_rbDir, _rbf);
|
|
6033
|
+
try {
|
|
6034
|
+
const _rbData = JSON.parse(fs.readFileSync(_rbPath, 'utf8'));
|
|
6035
|
+
fs.unlinkSync(_rbPath);
|
|
6036
|
+
if (!Array.isArray(_rbData) || _rbData.length === 0) return;
|
|
6037
|
+
const _rbOrg = String(_rbData[0].org || '').trim();
|
|
6038
|
+
const _rbRunId = String(_rbData[0].runId || '').trim();
|
|
6039
|
+
const _rbEvent = {
|
|
6040
|
+
type: 'agent:read:batch',
|
|
6041
|
+
org: _rbOrg,
|
|
6042
|
+
runId: _rbRunId,
|
|
6043
|
+
paths: _rbData.map(e => String(e.path || '').slice(0, 256)),
|
|
6044
|
+
count: _rbData.length,
|
|
6045
|
+
ts: Date.now(),
|
|
6046
|
+
};
|
|
6047
|
+
const _rbBody = JSON.stringify(_rbEvent);
|
|
6048
|
+
const _rbReq = http.request({
|
|
6049
|
+
hostname: 'localhost', port: boundPort,
|
|
6050
|
+
path: '/api/mastermind/event', method: 'POST',
|
|
6051
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(_rbBody) },
|
|
6052
|
+
}, () => {});
|
|
6053
|
+
_rbReq.on('error', () => {});
|
|
6054
|
+
_rbReq.setTimeout(2000, () => { _rbReq.destroy(); });
|
|
6055
|
+
_rbReq.write(_rbBody);
|
|
6056
|
+
_rbReq.end();
|
|
6057
|
+
} catch (_e) {}
|
|
6058
|
+
});
|
|
6059
|
+
} catch (_) {}
|
|
6060
|
+
}, 3000);
|
|
6061
|
+
|
|
6062
|
+
// ── Phase 4: Daemon heartbeat — ps -p {ppid} liveness check (Issue 8) ──
|
|
6063
|
+
// Periodically checks if the Claude Code session (tracked via ppid-keyed files) is still alive.
|
|
6064
|
+
// If the parent process is gone, auto-emits org:stop to close stale LIVE orgs in the dashboard.
|
|
6065
|
+
const _ppidCheckDir = path.join(MONOMIND_HOME, '.monomind', 'capture', 'active-runs');
|
|
6066
|
+
const _heartbeatTimer = setInterval(() => {
|
|
6067
|
+
if (!fs.existsSync(_ppidCheckDir)) return;
|
|
6068
|
+
try {
|
|
6069
|
+
fs.readdirSync(_ppidCheckDir).filter(f => f.endsWith('.json')).forEach(_ppf => {
|
|
6070
|
+
const _ppPath = path.join(_ppidCheckDir, _ppf);
|
|
6071
|
+
try {
|
|
6072
|
+
const _ppData = JSON.parse(fs.readFileSync(_ppPath, 'utf8'));
|
|
6073
|
+
const _ppid = parseInt(_ppf.replace('.json', ''), 10);
|
|
6074
|
+
if (!_ppid || isNaN(_ppid)) return;
|
|
6075
|
+
// Check if the ppid process is still alive (signal 0 = probe, no kill)
|
|
6076
|
+
try {
|
|
6077
|
+
process.kill(_ppid, 0);
|
|
6078
|
+
// Process alive — no action
|
|
6079
|
+
} catch (_psErr) {
|
|
6080
|
+
// Process gone — emit org:stop and remove the ppid file
|
|
6081
|
+
fs.unlinkSync(_ppPath);
|
|
6082
|
+
const _staleOrg = String(_ppData.org || '').trim();
|
|
6083
|
+
const _staleRun = String(_ppData.runId || '').trim();
|
|
6084
|
+
if (_staleOrg && activeOrgRuns.has(_staleOrg)) {
|
|
6085
|
+
const _stopBody = JSON.stringify({ type: 'org:stop', org: _staleOrg, runId: _staleRun, reason: 'ppid-dead', ts: Date.now() });
|
|
6086
|
+
const _stopReq = http.request({
|
|
6087
|
+
hostname: 'localhost', port: boundPort,
|
|
6088
|
+
path: '/api/mastermind/event', method: 'POST',
|
|
6089
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(_stopBody) },
|
|
6090
|
+
}, () => {});
|
|
6091
|
+
_stopReq.on('error', () => {});
|
|
6092
|
+
_stopReq.setTimeout(2000, () => { _stopReq.destroy(); });
|
|
6093
|
+
_stopReq.write(_stopBody);
|
|
6094
|
+
_stopReq.end();
|
|
6095
|
+
}
|
|
6096
|
+
}
|
|
6097
|
+
} catch (_) {}
|
|
6098
|
+
});
|
|
6099
|
+
} catch (_) {}
|
|
6100
|
+
}, 60000); // every 60s — intentionally infrequent, just a safety net
|
|
6101
|
+
|
|
5143
6102
|
// Update module-level state
|
|
5144
6103
|
running = true;
|
|
5145
6104
|
currentPort = boundPort;
|
|
@@ -5148,6 +6107,14 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
5148
6107
|
|
|
5149
6108
|
// --------------------------------------------------------- Graceful shutdown
|
|
5150
6109
|
function shutdown() {
|
|
6110
|
+
clearInterval(_spoolTimer);
|
|
6111
|
+
clearInterval(_rbTimer);
|
|
6112
|
+
clearInterval(_heartbeatTimer);
|
|
6113
|
+
// Flush SQLite run-event index to disk before exit (bypasses 1000ms debounce timer)
|
|
6114
|
+
clearTimeout(_runDbPersistTimer);
|
|
6115
|
+
if (_runDb && _runDbPath) {
|
|
6116
|
+
try { fs.writeFileSync(_runDbPath, Buffer.from(_runDb.export())); } catch (_) {}
|
|
6117
|
+
}
|
|
5151
6118
|
for (const w of activeWatchers) {
|
|
5152
6119
|
try {
|
|
5153
6120
|
w.close();
|
|
@@ -5158,20 +6125,16 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
5158
6125
|
activeWatchers.length = 0;
|
|
5159
6126
|
|
|
5160
6127
|
// Close all SSE connections
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
running = false;
|
|
5172
|
-
currentPort = null;
|
|
5173
|
-
currentUrl = null;
|
|
5174
|
-
activeServer = null;
|
|
6128
|
+
closeSseClients();
|
|
6129
|
+
|
|
6130
|
+
// Drain in-flight JSONL appends before closing (prevents truncated writes on fast SIGTERM)
|
|
6131
|
+
Promise.all([..._writeQueue.values()]).catch(() => {}).finally(() => {
|
|
6132
|
+
server.close(() => {
|
|
6133
|
+
running = false;
|
|
6134
|
+
currentPort = null;
|
|
6135
|
+
currentUrl = null;
|
|
6136
|
+
activeServer = null;
|
|
6137
|
+
});
|
|
5175
6138
|
});
|
|
5176
6139
|
}
|
|
5177
6140
|
|
|
@@ -5196,7 +6159,7 @@ export function getServerStatus() {
|
|
|
5196
6159
|
running,
|
|
5197
6160
|
port: currentPort,
|
|
5198
6161
|
url: currentUrl,
|
|
5199
|
-
clientCount:
|
|
6162
|
+
clientCount: getSseClientCount(),
|
|
5200
6163
|
};
|
|
5201
6164
|
}
|
|
5202
6165
|
|