@wrongstack/core 0.285.0 → 0.287.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/dist/autophase/phase-orchestrator.d.ts.map +1 -1
- package/dist/boot.d.ts.map +1 -1
- package/dist/coordination/agent-monitor.d.ts +29 -0
- package/dist/coordination/agent-monitor.d.ts.map +1 -1
- package/dist/coordination/autonomous-brain.d.ts +0 -2
- package/dist/coordination/autonomous-brain.d.ts.map +1 -1
- package/dist/coordination/autonomous-coordinator.d.ts +4 -0
- package/dist/coordination/autonomous-coordinator.d.ts.map +1 -1
- package/dist/coordination/brain-ledger.d.ts +142 -0
- package/dist/coordination/brain-ledger.d.ts.map +1 -0
- package/dist/coordination/brain-monitor.d.ts +46 -2
- package/dist/coordination/brain-monitor.d.ts.map +1 -1
- package/dist/coordination/brain.d.ts +43 -0
- package/dist/coordination/brain.d.ts.map +1 -1
- package/dist/coordination/delegate-tool.d.ts +8 -14
- package/dist/coordination/delegate-tool.d.ts.map +1 -1
- package/dist/coordination/director/director-btw-notes.d.ts +20 -0
- package/dist/coordination/director/director-btw-notes.d.ts.map +1 -0
- package/dist/coordination/director/director-collab.d.ts +40 -0
- package/dist/coordination/director/director-collab.d.ts.map +1 -0
- package/dist/coordination/director.d.ts +28 -9
- package/dist/coordination/director.d.ts.map +1 -1
- package/dist/coordination/fleet-manager.d.ts +21 -0
- package/dist/coordination/fleet-manager.d.ts.map +1 -1
- package/dist/coordination/fleet.d.ts +6 -7
- package/dist/coordination/fleet.d.ts.map +1 -1
- package/dist/coordination/global-mailbox.d.ts +96 -21
- package/dist/coordination/global-mailbox.d.ts.map +1 -1
- package/dist/coordination/icoordinator.d.ts +1 -3
- package/dist/coordination/icoordinator.d.ts.map +1 -1
- package/dist/coordination/index.d.ts +2 -1
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +2176 -806
- package/dist/coordination/index.js.map +4 -4
- package/dist/coordination/knowledge-graph.d.ts +13 -1
- package/dist/coordination/knowledge-graph.d.ts.map +1 -1
- package/dist/coordination/mail-tools.d.ts.map +1 -1
- package/dist/coordination/mailbox-actions.d.ts +12 -13
- package/dist/coordination/mailbox-actions.d.ts.map +1 -1
- package/dist/coordination/mailbox-constants.d.ts +64 -0
- package/dist/coordination/mailbox-constants.d.ts.map +1 -0
- package/dist/coordination/mailbox-events.d.ts +32 -0
- package/dist/coordination/mailbox-events.d.ts.map +1 -0
- package/dist/coordination/mailbox-message-codec.d.ts +28 -0
- package/dist/coordination/mailbox-message-codec.d.ts.map +1 -0
- package/dist/coordination/mailbox-types.d.ts +130 -5
- package/dist/coordination/mailbox-types.d.ts.map +1 -1
- package/dist/coordination/mailbox.d.ts +21 -2
- package/dist/coordination/mailbox.d.ts.map +1 -1
- package/dist/coordination/package-author-tracker.d.ts.map +1 -1
- package/dist/coordination/subagent-result-tool.d.ts.map +1 -1
- package/dist/core/agent-loop.d.ts.map +1 -1
- package/dist/core/agent-response.d.ts +10 -1
- package/dist/core/agent-response.d.ts.map +1 -1
- package/dist/core/agent-tools.d.ts.map +1 -1
- package/dist/core/agent.d.ts +8 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/context.d.ts +25 -0
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/conversation-state.d.ts +6 -0
- package/dist/core/conversation-state.d.ts.map +1 -1
- package/dist/core/fallback-model.d.ts.map +1 -1
- package/dist/core/mailbox-loop.d.ts +12 -5
- package/dist/core/mailbox-loop.d.ts.map +1 -1
- package/dist/core/system-prompt-builder.d.ts +14 -6
- package/dist/core/system-prompt-builder.d.ts.map +1 -1
- package/dist/defaults/index.d.ts +0 -1
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +1690 -1998
- package/dist/defaults/index.js.map +4 -4
- package/dist/execution/auto-compaction-middleware.d.ts +8 -1
- package/dist/execution/auto-compaction-middleware.d.ts.map +1 -1
- package/dist/execution/autonomous-runner.d.ts +7 -1
- package/dist/execution/autonomous-runner.d.ts.map +1 -1
- package/dist/execution/autonomy-brain.d.ts +72 -11
- package/dist/execution/autonomy-brain.d.ts.map +1 -1
- package/dist/execution/brain-chain.d.ts +53 -0
- package/dist/execution/brain-chain.d.ts.map +1 -0
- package/dist/execution/brain-evaluation.d.ts +66 -0
- package/dist/execution/brain-evaluation.d.ts.map +1 -0
- package/dist/execution/brain-runtime.d.ts +171 -0
- package/dist/execution/brain-runtime.d.ts.map +1 -0
- package/dist/execution/compaction-core.d.ts.map +1 -1
- package/dist/execution/council-brain.d.ts +79 -0
- package/dist/execution/council-brain.d.ts.map +1 -0
- package/dist/execution/council-orchestrator.d.ts +32 -0
- package/dist/execution/council-orchestrator.d.ts.map +1 -0
- package/dist/execution/council-personas.d.ts +19 -0
- package/dist/execution/council-personas.d.ts.map +1 -0
- package/dist/execution/council-profiles.d.ts +34 -0
- package/dist/execution/council-profiles.d.ts.map +1 -0
- package/dist/execution/council-prompts.d.ts +21 -0
- package/dist/execution/council-prompts.d.ts.map +1 -0
- package/dist/execution/council-resolution.d.ts +57 -0
- package/dist/execution/council-resolution.d.ts.map +1 -0
- package/dist/execution/design-kit-loader.d.ts.map +1 -1
- package/dist/execution/enhance-recovery.d.ts +35 -0
- package/dist/execution/enhance-recovery.d.ts.map +1 -0
- package/dist/execution/error-handler.d.ts.map +1 -1
- package/dist/execution/eternal-autonomy.d.ts +12 -0
- package/dist/execution/eternal-autonomy.d.ts.map +1 -1
- package/dist/execution/index.d.ts +11 -1
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +3076 -526
- package/dist/execution/index.js.map +4 -4
- package/dist/execution/intelligent-compactor.d.ts +18 -7
- package/dist/execution/intelligent-compactor.d.ts.map +1 -1
- package/dist/execution/one-shot-llm.d.ts +52 -0
- package/dist/execution/one-shot-llm.d.ts.map +1 -0
- package/dist/execution/parallel-eternal-engine.d.ts +12 -0
- package/dist/execution/parallel-eternal-engine.d.ts.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +23 -4
- package/dist/execution/prompt-enhancer.d.ts.map +1 -1
- package/dist/execution/prompt-enhancer.js +24 -6
- package/dist/execution/prompt-enhancer.js.map +2 -2
- package/dist/execution/selective-compactor.d.ts +4 -4
- package/dist/execution/strategy-compactor.d.ts +7 -0
- package/dist/execution/strategy-compactor.d.ts.map +1 -1
- package/dist/execution/tool-executor.d.ts +19 -5
- package/dist/execution/tool-executor.d.ts.map +1 -1
- package/dist/hooks/registry.d.ts.map +1 -1
- package/dist/hooks/runner.d.ts.map +1 -1
- package/dist/hooks/shell-executor.d.ts.map +1 -1
- package/dist/hq/alerts.d.ts +5 -0
- package/dist/hq/alerts.d.ts.map +1 -1
- package/dist/hq/auth-store.d.ts +38 -10
- package/dist/hq/auth-store.d.ts.map +1 -1
- package/dist/hq/commands.d.ts +8 -4
- package/dist/hq/commands.d.ts.map +1 -1
- package/dist/hq/cost-bridge.d.ts +5 -3
- package/dist/hq/cost-bridge.d.ts.map +1 -1
- package/dist/hq/factory.d.ts +3 -0
- package/dist/hq/factory.d.ts.map +1 -1
- package/dist/hq/fleet-bridge.d.ts.map +1 -1
- package/dist/hq/index.d.ts +0 -1
- package/dist/hq/index.d.ts.map +1 -1
- package/dist/hq/index.js +1016 -283
- package/dist/hq/index.js.map +4 -4
- package/dist/hq/persistence.d.ts +42 -0
- package/dist/hq/persistence.d.ts.map +1 -1
- package/dist/hq/protocol/brain.d.ts +24 -0
- package/dist/hq/protocol/brain.d.ts.map +1 -0
- package/dist/hq/protocol/browser.d.ts +12 -0
- package/dist/hq/protocol/browser.d.ts.map +1 -0
- package/dist/hq/protocol/client.d.ts +69 -0
- package/dist/hq/protocol/client.d.ts.map +1 -0
- package/dist/hq/protocol/core.d.ts +130 -0
- package/dist/hq/protocol/core.d.ts.map +1 -0
- package/dist/hq/protocol/fleet.d.ts +73 -0
- package/dist/hq/protocol/fleet.d.ts.map +1 -0
- package/dist/hq/protocol/mailbox.d.ts +76 -0
- package/dist/hq/protocol/mailbox.d.ts.map +1 -0
- package/dist/hq/protocol/mcp.d.ts +51 -0
- package/dist/hq/protocol/mcp.d.ts.map +1 -0
- package/dist/hq/protocol/project.d.ts +26 -0
- package/dist/hq/protocol/project.d.ts.map +1 -0
- package/dist/hq/protocol/session.d.ts +153 -0
- package/dist/hq/protocol/session.d.ts.map +1 -0
- package/dist/hq/protocol/tool.d.ts +31 -0
- package/dist/hq/protocol/tool.d.ts.map +1 -0
- package/dist/hq/protocol.d.ts +10 -559
- package/dist/hq/protocol.d.ts.map +1 -1
- package/dist/hq/publisher.d.ts +4 -0
- package/dist/hq/publisher.d.ts.map +1 -1
- package/dist/index.d.ts +24 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7676 -3432
- package/dist/index.js.map +4 -4
- package/dist/infrastructure/index.js +13 -11
- package/dist/infrastructure/index.js.map +2 -2
- package/dist/infrastructure/token-counter.d.ts.map +1 -1
- package/dist/kernel/events/agent-events.d.ts +283 -0
- package/dist/kernel/events/agent-events.d.ts.map +1 -0
- package/dist/kernel/events/brain-events.d.ts +176 -0
- package/dist/kernel/events/brain-events.d.ts.map +1 -0
- package/dist/kernel/events/fleet-events.d.ts +39 -0
- package/dist/kernel/events/fleet-events.d.ts.map +1 -0
- package/dist/kernel/events/memory-events.d.ts +165 -0
- package/dist/kernel/events/memory-events.d.ts.map +1 -0
- package/dist/kernel/events/provider-events.d.ts +98 -0
- package/dist/kernel/events/provider-events.d.ts.map +1 -0
- package/dist/kernel/events/sdd-events.d.ts +118 -0
- package/dist/kernel/events/sdd-events.d.ts.map +1 -0
- package/dist/kernel/events/session-events.d.ts +227 -0
- package/dist/kernel/events/session-events.d.ts.map +1 -0
- package/dist/kernel/events/tool-events.d.ts +185 -0
- package/dist/kernel/events/tool-events.d.ts.map +1 -0
- package/dist/kernel/events/worktree-events.d.ts +58 -0
- package/dist/kernel/events/worktree-events.d.ts.map +1 -0
- package/dist/kernel/events.d.ts +12 -1282
- package/dist/kernel/events.d.ts.map +1 -1
- package/dist/kernel/index.js +22 -2
- package/dist/kernel/index.js.map +2 -2
- package/dist/kernel/run-controller.d.ts +15 -0
- package/dist/kernel/run-controller.d.ts.map +1 -1
- package/dist/mailbox-attach.d.ts.map +1 -1
- package/dist/models/index.js +19 -6
- package/dist/models/index.js.map +2 -2
- package/dist/models/llm-selector.d.ts +7 -0
- package/dist/models/llm-selector.d.ts.map +1 -1
- package/dist/observability/index.js +16 -15
- package/dist/observability/index.js.map +2 -2
- package/dist/plugins/auto-review-plugin.d.ts +36 -0
- package/dist/plugins/auto-review-plugin.d.ts.map +1 -0
- package/dist/plugins/chimera-plugin.d.ts +8 -0
- package/dist/plugins/chimera-plugin.d.ts.map +1 -1
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +260 -23
- package/dist/security/index.js.map +4 -4
- package/dist/security/permission-policy-schema.d.ts +77 -0
- package/dist/security/permission-policy-schema.d.ts.map +1 -0
- package/dist/security/permission-policy.d.ts +5 -0
- package/dist/security/permission-policy.d.ts.map +1 -1
- package/dist/security/secret-scrubber.d.ts.map +1 -1
- package/dist/security/secret-vault.d.ts +9 -0
- package/dist/security/secret-vault.d.ts.map +1 -1
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/skills/index.js.map +2 -2
- package/dist/storage/config-loader.d.ts +10 -0
- package/dist/storage/config-loader.d.ts.map +1 -1
- package/dist/storage/goal-kanban.d.ts +69 -0
- package/dist/storage/goal-kanban.d.ts.map +1 -0
- package/dist/storage/goal-store.d.ts +1 -1
- package/dist/storage/goal-store.d.ts.map +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +572 -879
- package/dist/storage/index.js.map +4 -4
- package/dist/storage/memory-consolidator.d.ts +9 -1
- package/dist/storage/memory-consolidator.d.ts.map +1 -1
- package/dist/storage/plan-store.d.ts +1 -1
- package/dist/storage/plan-store.d.ts.map +1 -1
- package/dist/storage/queue-store.d.ts +4 -0
- package/dist/storage/queue-store.d.ts.map +1 -1
- package/dist/storage/session-store.d.ts +9 -0
- package/dist/storage/session-store.d.ts.map +1 -1
- package/dist/storage/task-store.d.ts +1 -1
- package/dist/storage/task-store.d.ts.map +1 -1
- package/dist/storage/todos-checkpoint.d.ts +2 -2
- package/dist/storage/todos-checkpoint.d.ts.map +1 -1
- package/dist/tools/council-tool.d.ts +25 -0
- package/dist/tools/council-tool.d.ts.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1884 -12
- package/dist/tools/index.js.map +4 -4
- package/dist/tools/one-shot-llm-tool.d.ts +51 -0
- package/dist/tools/one-shot-llm-tool.d.ts.map +1 -0
- package/dist/types/config.d.ts +236 -4
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/context-window.d.ts.map +1 -1
- package/dist/types/council.d.ts +134 -0
- package/dist/types/council.d.ts.map +1 -0
- package/dist/types/default-config.d.ts +3 -3
- package/dist/types/hooks.d.ts +14 -0
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +40 -8
- package/dist/types/index.js.map +3 -3
- package/dist/types/multi-agent.d.ts +8 -0
- package/dist/types/multi-agent.d.ts.map +1 -1
- package/dist/types/observability.d.ts +5 -0
- package/dist/types/observability.d.ts.map +1 -1
- package/dist/types/one-shot-llm.d.ts +128 -0
- package/dist/types/one-shot-llm.d.ts.map +1 -0
- package/dist/types/plugin.d.ts +6 -0
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/provider.d.ts +17 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/system-prompt.d.ts +2 -1
- package/dist/types/system-prompt.d.ts.map +1 -1
- package/dist/types/tool-executor.d.ts +10 -0
- package/dist/types/tool-executor.d.ts.map +1 -1
- package/dist/types/tool.d.ts +7 -0
- package/dist/types/tool.d.ts.map +1 -1
- package/dist/utils/atomic-write.d.ts.map +1 -1
- package/dist/utils/glob-expand.d.ts.map +1 -1
- package/dist/utils/incoming-images.d.ts +39 -0
- package/dist/utils/incoming-images.d.ts.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +156 -11
- package/dist/utils/index.js.map +4 -4
- package/dist/utils/tool-wire-compact.d.ts +1 -0
- package/dist/utils/tool-wire-compact.d.ts.map +1 -1
- package/dist/utils/wstack-paths.d.ts +20 -4
- package/dist/utils/wstack-paths.d.ts.map +1 -1
- package/dist/worktree/worktree-manager.d.ts +13 -4
- package/dist/worktree/worktree-manager.d.ts.map +1 -1
- package/instructions/agents/browser.md +25 -16
- package/instructions/cli/next-task-predictor.md +5 -3
- package/instructions/coordination/subagent-baseline.md +17 -2
- package/instructions/leader-after-task.md +19 -8
- package/instructions/llm/council-judge.md +19 -0
- package/instructions/llm/council-voter.md +22 -0
- package/instructions/modes/architect.md +14 -9
- package/instructions/modes/audit-lite.md +9 -7
- package/instructions/modes/brief-system.md +12 -12
- package/instructions/modes/brief.md +7 -5
- package/instructions/modes/code-auditor.md +13 -8
- package/instructions/modes/code-reviewer.md +13 -8
- package/instructions/modes/debug-lite.md +7 -6
- package/instructions/modes/debugger.md +13 -9
- package/instructions/modes/devops.md +13 -9
- package/instructions/modes/plan-lite.md +7 -6
- package/instructions/modes/refactor-lite.md +7 -6
- package/instructions/modes/refactorer.md +14 -9
- package/instructions/modes/research-lite.md +8 -6
- package/instructions/modes/research-web.md +18 -55
- package/instructions/modes/review-lite.md +7 -6
- package/instructions/modes/teach-system.md +15 -15
- package/instructions/modes/teach.md +13 -12
- package/instructions/modes/test-lite.md +6 -5
- package/instructions/modes/tester.md +13 -9
- package/instructions/modes/ui-design.md +14 -8
- package/instructions/sections/tool/commit-hygiene.md +1 -7
- package/instructions/sections/tool/common-patterns.md +7 -1
- package/instructions/sections/tool/delegation-full.md +3 -31
- package/instructions/sections/tool/mailbox-compact.md +1 -1
- package/instructions/sections/tool/mailbox-full.md +6 -22
- package/instructions/system.md +294 -8
- package/package.json +2 -2
- package/skills/docker-deploy/SKILL.md +1 -1
- package/skills/mailbox-bridge/SKILL.md +16 -7
- package/skills/output-standards/SKILL.md +16 -16
- package/skills/wrongstack-mailbox/SKILL.md +57 -12
- package/dist/coordination/director-construction.d.ts +0 -90
- package/dist/coordination/director-construction.d.ts.map +0 -1
- package/dist/hq/agent-bridge.d.ts +0 -27
- package/dist/hq/agent-bridge.d.ts.map +0 -1
- package/dist/plugins/git-plugin.d.ts +0 -15
- package/dist/plugins/git-plugin.d.ts.map +0 -1
- package/dist/plugins/observability-plugin.d.ts +0 -19
- package/dist/plugins/observability-plugin.d.ts.map +0 -1
- package/dist/plugins/plan-plugin.d.ts +0 -16
- package/dist/plugins/plan-plugin.d.ts.map +0 -1
- package/dist/storage/memory-store.d.ts +0 -119
- package/dist/storage/memory-store.d.ts.map +0 -1
package/dist/hq/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
// src/hq/protocol.ts
|
|
2
|
-
var HQ_PROTOCOL_VERSION = 1;
|
|
1
|
+
// src/hq/protocol/tool.ts
|
|
3
2
|
var DEFAULT_HQ_REDACTION_POLICY = {
|
|
4
3
|
rawContent: true,
|
|
5
4
|
toolArgs: "full",
|
|
6
5
|
paths: "full"
|
|
7
6
|
};
|
|
8
7
|
var HQ_TRANSCRIPT_TEXT_CAP = 16e3;
|
|
8
|
+
|
|
9
|
+
// src/hq/protocol/core.ts
|
|
10
|
+
var HQ_PROTOCOL_VERSION = 1;
|
|
9
11
|
var KNOWN_HQ_CLIENT_FRAME_TYPES = /* @__PURE__ */ new Set([
|
|
10
12
|
"client.hello",
|
|
11
13
|
"client.event",
|
|
@@ -140,8 +142,57 @@ var KNOWN_HQ_EVENT_PAYLOAD_TYPES = /* @__PURE__ */ new Set([
|
|
|
140
142
|
"tool.started",
|
|
141
143
|
"tool.completed",
|
|
142
144
|
"session.usage",
|
|
143
|
-
"client.heartbeat"
|
|
145
|
+
"client.heartbeat",
|
|
146
|
+
"mcp.health.snapshot",
|
|
147
|
+
"mcp.operation"
|
|
148
|
+
]);
|
|
149
|
+
function isHqMcpLatencySummary(x) {
|
|
150
|
+
if (typeof x !== "object" || x === null) return false;
|
|
151
|
+
const v = x;
|
|
152
|
+
return typeof v.count === "number" && (v.lastMs === void 0 || typeof v.lastMs === "number") && (v.minMs === void 0 || typeof v.minMs === "number") && (v.maxMs === void 0 || typeof v.maxMs === "number") && (v.p50Ms === void 0 || typeof v.p50Ms === "number") && (v.p95Ms === void 0 || typeof v.p95Ms === "number");
|
|
153
|
+
}
|
|
154
|
+
function isHqMcpFailureCounts(x) {
|
|
155
|
+
if (typeof x !== "object" || x === null) return false;
|
|
156
|
+
const v = x;
|
|
157
|
+
return typeof v.transport === "number" && typeof v.protocol === "number" && typeof v.tool === "number";
|
|
158
|
+
}
|
|
159
|
+
function isHqMcpHealthCheck(x) {
|
|
160
|
+
if (typeof x !== "object" || x === null) return false;
|
|
161
|
+
const v = x;
|
|
162
|
+
return typeof v.name === "string" && typeof v.passed === "boolean" && (v.value === void 0 || typeof v.value === "number") && (v.threshold === void 0 || typeof v.threshold === "number");
|
|
163
|
+
}
|
|
164
|
+
var HQ_MCP_HEALTH_STATES = /* @__PURE__ */ new Set([
|
|
165
|
+
"disabled",
|
|
166
|
+
"dormant",
|
|
167
|
+
"connecting",
|
|
168
|
+
"healthy",
|
|
169
|
+
"degraded",
|
|
170
|
+
"failed"
|
|
171
|
+
]);
|
|
172
|
+
var HQ_MCP_CONNECTION_STATES = /* @__PURE__ */ new Set([
|
|
173
|
+
"idle",
|
|
174
|
+
"dormant",
|
|
175
|
+
"connecting",
|
|
176
|
+
"connected",
|
|
177
|
+
"reconnecting",
|
|
178
|
+
"disconnected",
|
|
179
|
+
"failed"
|
|
144
180
|
]);
|
|
181
|
+
function isHqMcpServerHealth(x) {
|
|
182
|
+
if (typeof x !== "object" || x === null) return false;
|
|
183
|
+
const v = x;
|
|
184
|
+
return typeof v.name === "string" && typeof v.projectId === "string" && typeof v.clientId === "string" && typeof v.connectionState === "string" && HQ_MCP_CONNECTION_STATES.has(v.connectionState) && typeof v.healthState === "string" && HQ_MCP_HEALTH_STATES.has(v.healthState) && (v.lastSuccessAt === void 0 || typeof v.lastSuccessAt === "string") && (v.lastFailureAt === void 0 || typeof v.lastFailureAt === "string") && (v.lastFailureKind === void 0 || v.lastFailureKind === "transport" || v.lastFailureKind === "protocol" || v.lastFailureKind === "tool") && typeof v.consecutiveFailures === "number" && isHqMcpFailureCounts(v.failures) && typeof v.reconnectCount === "number" && typeof v.wakeCount === "number" && typeof v.sleepCount === "number" && typeof v.restartCount === "number" && isHqMcpLatencySummary(v.connectionLatency) && isHqMcpLatencySummary(v.discoveryLatency) && isHqMcpLatencySummary(v.callLatency) && typeof v.inFlightCalls === "number" && typeof v.peakInFlightCalls === "number" && Array.isArray(v.healthChecks) && v.healthChecks.every(isHqMcpHealthCheck);
|
|
185
|
+
}
|
|
186
|
+
function isHqMcpHealthSnapshotPayload(x) {
|
|
187
|
+
if (typeof x !== "object" || x === null) return false;
|
|
188
|
+
const v = x;
|
|
189
|
+
return Array.isArray(v.servers) && v.servers.every(isHqMcpServerHealth);
|
|
190
|
+
}
|
|
191
|
+
function isHqMcpOperationPayload(x) {
|
|
192
|
+
if (typeof x !== "object" || x === null) return false;
|
|
193
|
+
const v = x;
|
|
194
|
+
return typeof v.operation === "object" && v.operation !== null && Array.isArray(v.servers) && v.servers.every(isHqMcpServerHealth);
|
|
195
|
+
}
|
|
145
196
|
function isHqMailboxMessageSummary(x) {
|
|
146
197
|
if (typeof x !== "object" || x === null) return false;
|
|
147
198
|
const v = x;
|
|
@@ -351,6 +402,10 @@ function parseHqEventPayload(eventType, payload) {
|
|
|
351
402
|
return isHqUsagePayload(payload) ? { ok: true, payload } : { ok: false, reason: "malformed-payload" };
|
|
352
403
|
case "client.heartbeat":
|
|
353
404
|
return isHqClientHeartbeatPayload(payload) ? { ok: true, payload } : { ok: false, reason: "malformed-payload" };
|
|
405
|
+
case "mcp.health.snapshot":
|
|
406
|
+
return isHqMcpHealthSnapshotPayload(payload) ? { ok: true, payload } : { ok: false, reason: "malformed-payload" };
|
|
407
|
+
case "mcp.operation":
|
|
408
|
+
return isHqMcpOperationPayload(payload) ? { ok: true, payload } : { ok: false, reason: "malformed-payload" };
|
|
354
409
|
default: {
|
|
355
410
|
const _exhaustive = eventType;
|
|
356
411
|
return _exhaustive;
|
|
@@ -490,7 +545,12 @@ function hasCredentialAnchors(text) {
|
|
|
490
545
|
text.includes("_TOKEN=") || // ACCESS_TOKEN=, AUTH_TOKEN=, ...
|
|
491
546
|
text.includes("_SECRET=") || // API_SECRET=, CLIENT_SECRET=, ...
|
|
492
547
|
text.includes("_PASSWORD=") || // DB_PASSWORD=, ROOT_PASSWORD=, ...
|
|
493
|
-
text.includes("mongodb://") || text.includes("mongodb+srv://") || text.includes("postgres://") || text.includes("postgresql://") || text.includes("mysql://") || text.includes("redis://")
|
|
548
|
+
text.includes("mongodb://") || text.includes("mongodb+srv://") || text.includes("postgres://") || text.includes("postgresql://") || text.includes("mysql://") || text.includes("redis://") || // JSON-style credential keys: tool outputs often serialise objects as
|
|
549
|
+
// raw JSON strings rather than parsed objects. The value may not start
|
|
550
|
+
// with a recognisable prefix (sk-, ghp_, etc.), so we anchor on common
|
|
551
|
+
// key names. The `"` prefix avoids matching prose that happens to mention
|
|
552
|
+
// these words — JSON serialisation always quotes string keys.
|
|
553
|
+
text.includes('"apiKey"') || text.includes('"api_key"') || text.includes('"token"') || text.includes('"secret"') || text.includes('"password"') || text.includes('"authorization"') || text.includes('"bearer"') || text.includes('"private_key"') || text.includes('"access_token"') || text.includes('"refresh_token"') || text.includes('"client_secret"');
|
|
494
554
|
}
|
|
495
555
|
var DefaultSecretScrubber = class {
|
|
496
556
|
scrub(text) {
|
|
@@ -941,6 +1001,7 @@ var HqPublisher = class {
|
|
|
941
1001
|
this.reconnectMaxMs = options.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
|
|
942
1002
|
this.maxQueuedMessages = options.maxQueuedMessages ?? DEFAULT_MAX_QUEUED_MESSAGES;
|
|
943
1003
|
this.resolvedRedactionPolicy = resolveHqRedactionPolicy(options.redactionPolicy);
|
|
1004
|
+
this.logger = options.logger;
|
|
944
1005
|
}
|
|
945
1006
|
options;
|
|
946
1007
|
socketFactory;
|
|
@@ -952,6 +1013,7 @@ var HqPublisher = class {
|
|
|
952
1013
|
reconnectMaxMs;
|
|
953
1014
|
maxQueuedMessages;
|
|
954
1015
|
resolvedRedactionPolicy;
|
|
1016
|
+
logger;
|
|
955
1017
|
socket = null;
|
|
956
1018
|
seq = 0;
|
|
957
1019
|
queue = [];
|
|
@@ -1290,6 +1352,10 @@ var HqPublisher = class {
|
|
|
1290
1352
|
emitConnectWarning() {
|
|
1291
1353
|
const attempt = this.lastAttempt;
|
|
1292
1354
|
const message = `WrongStack HQ publisher: ${this.reconnectAttempt} consecutive connection failures${attempt !== null ? ` to ${attempt.url} (client token ${attempt.hadToken ? "present" : "absent"})` : ""}. Either the HQ server is unreachable or it rejected the token (401). If HQ runs in client-token mode, verify WRONGSTACK_HQ_TOKEN / auth.json. Retries continue with backoff.`;
|
|
1355
|
+
if (this.logger) {
|
|
1356
|
+
this.logger.warn(message, { event: "hq.publisher.connect_failed" });
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1293
1359
|
const warn = this.options.warn ?? ((msg) => console.warn(
|
|
1294
1360
|
JSON.stringify({
|
|
1295
1361
|
level: "warn",
|
|
@@ -1322,7 +1388,7 @@ var HqPublisher = class {
|
|
|
1322
1388
|
|
|
1323
1389
|
// src/hq/factory.ts
|
|
1324
1390
|
import { createHash as createHash2, randomUUID as randomUUID4 } from "node:crypto";
|
|
1325
|
-
import * as
|
|
1391
|
+
import * as fs4 from "node:fs";
|
|
1326
1392
|
import { hostname } from "node:os";
|
|
1327
1393
|
import { basename as basename5 } from "node:path";
|
|
1328
1394
|
|
|
@@ -1333,7 +1399,8 @@ import * as path4 from "node:path";
|
|
|
1333
1399
|
|
|
1334
1400
|
// src/utils/atomic-write.ts
|
|
1335
1401
|
import { randomBytes } from "node:crypto";
|
|
1336
|
-
import * as
|
|
1402
|
+
import * as fs2 from "node:fs/promises";
|
|
1403
|
+
import { watch as watchDir } from "node:fs";
|
|
1337
1404
|
import * as path3 from "node:path";
|
|
1338
1405
|
|
|
1339
1406
|
// src/utils/session-scoped-path.ts
|
|
@@ -1360,14 +1427,34 @@ function invalid(sessionId) {
|
|
|
1360
1427
|
|
|
1361
1428
|
// src/utils/wstack-paths.ts
|
|
1362
1429
|
import { createHash } from "node:crypto";
|
|
1430
|
+
import * as fs from "node:fs";
|
|
1363
1431
|
import * as os from "node:os";
|
|
1364
1432
|
import * as path2 from "node:path";
|
|
1433
|
+
function canonicalProjectRoot(absRoot) {
|
|
1434
|
+
const checkoutRoot = path2.resolve(absRoot);
|
|
1435
|
+
const dotGit = path2.join(checkoutRoot, ".git");
|
|
1436
|
+
try {
|
|
1437
|
+
if (!fs.statSync(dotGit).isFile()) return checkoutRoot;
|
|
1438
|
+
const gitDirLine = fs.readFileSync(dotGit, "utf8").trim();
|
|
1439
|
+
const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
|
|
1440
|
+
if (!match?.[1]) return checkoutRoot;
|
|
1441
|
+
const gitDir = path2.resolve(checkoutRoot, match[1].trim());
|
|
1442
|
+
const commonDirFile = path2.join(gitDir, "commondir");
|
|
1443
|
+
if (!fs.statSync(commonDirFile).isFile()) return checkoutRoot;
|
|
1444
|
+
const commonDir = path2.resolve(gitDir, fs.readFileSync(commonDirFile, "utf8").trim());
|
|
1445
|
+
if (path2.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
|
|
1446
|
+
return path2.dirname(commonDir);
|
|
1447
|
+
} catch {
|
|
1448
|
+
return checkoutRoot;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1365
1451
|
function projectHash(absRoot) {
|
|
1366
|
-
return createHash("sha256").update(
|
|
1452
|
+
return createHash("sha256").update(canonicalProjectRoot(absRoot)).digest("hex").slice(0, 12);
|
|
1367
1453
|
}
|
|
1368
1454
|
function projectSlug(absRoot) {
|
|
1369
|
-
const
|
|
1370
|
-
const
|
|
1455
|
+
const identityRoot = canonicalProjectRoot(absRoot);
|
|
1456
|
+
const base = slugify(path2.basename(identityRoot));
|
|
1457
|
+
const hash = createHash("sha256").update(identityRoot).digest("hex").slice(0, 6);
|
|
1371
1458
|
return `${base}-${hash}`;
|
|
1372
1459
|
}
|
|
1373
1460
|
function slugify(name) {
|
|
@@ -1538,16 +1625,16 @@ var FsError = class extends WrongStackError {
|
|
|
1538
1625
|
// src/utils/atomic-write.ts
|
|
1539
1626
|
async function atomicWrite(targetPath, content, opts = {}) {
|
|
1540
1627
|
const dir = path3.dirname(targetPath);
|
|
1541
|
-
await
|
|
1628
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
1542
1629
|
const tmp = path3.join(dir, `.${path3.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
1543
1630
|
try {
|
|
1544
1631
|
if (typeof content === "string") {
|
|
1545
|
-
await
|
|
1632
|
+
await fs2.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
1546
1633
|
} else {
|
|
1547
|
-
await
|
|
1634
|
+
await fs2.writeFile(tmp, content, { flag: "wx" });
|
|
1548
1635
|
}
|
|
1549
1636
|
try {
|
|
1550
|
-
const fh = await
|
|
1637
|
+
const fh = await fs2.open(tmp, "r+");
|
|
1551
1638
|
try {
|
|
1552
1639
|
await fh.sync();
|
|
1553
1640
|
} finally {
|
|
@@ -1557,24 +1644,24 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
1557
1644
|
}
|
|
1558
1645
|
let mode;
|
|
1559
1646
|
try {
|
|
1560
|
-
const stat4 = await
|
|
1647
|
+
const stat4 = await fs2.stat(targetPath);
|
|
1561
1648
|
mode = stat4.mode & 511;
|
|
1562
1649
|
} catch {
|
|
1563
1650
|
mode = opts.mode;
|
|
1564
1651
|
}
|
|
1565
1652
|
if (mode !== void 0) {
|
|
1566
|
-
await
|
|
1653
|
+
await fs2.chmod(tmp, mode);
|
|
1567
1654
|
}
|
|
1568
1655
|
await renameWithRetry(tmp, targetPath);
|
|
1569
1656
|
if (mode !== void 0 && process.platform === "win32") {
|
|
1570
1657
|
try {
|
|
1571
|
-
await
|
|
1658
|
+
await fs2.chmod(targetPath, mode);
|
|
1572
1659
|
} catch {
|
|
1573
1660
|
}
|
|
1574
1661
|
}
|
|
1575
1662
|
} catch (err) {
|
|
1576
1663
|
try {
|
|
1577
|
-
await
|
|
1664
|
+
await fs2.unlink(tmp);
|
|
1578
1665
|
} catch {
|
|
1579
1666
|
}
|
|
1580
1667
|
throw err;
|
|
@@ -1582,7 +1669,7 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
1582
1669
|
}
|
|
1583
1670
|
async function withFileLock(targetPath, fn, opts = {}) {
|
|
1584
1671
|
const dir = path3.dirname(targetPath);
|
|
1585
|
-
await
|
|
1672
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
1586
1673
|
const lockPath = path3.join(dir, `.${path3.basename(targetPath)}.lock`);
|
|
1587
1674
|
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
1588
1675
|
const staleMs = opts.staleMs ?? 3e4;
|
|
@@ -1590,26 +1677,27 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
1590
1677
|
let handle;
|
|
1591
1678
|
for (; ; ) {
|
|
1592
1679
|
try {
|
|
1593
|
-
handle = await
|
|
1680
|
+
handle = await fs2.open(lockPath, "wx");
|
|
1594
1681
|
await handle.writeFile(`${process.pid}:${Date.now()}`);
|
|
1595
1682
|
break;
|
|
1596
1683
|
} catch (err) {
|
|
1597
1684
|
const code = err.code;
|
|
1598
1685
|
if (code === "ENOENT") {
|
|
1599
|
-
await
|
|
1686
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
1600
1687
|
continue;
|
|
1601
1688
|
}
|
|
1602
|
-
if (code !== "EEXIST") throw err;
|
|
1689
|
+
if (code !== "EEXIST" && code !== "EPERM") throw err;
|
|
1603
1690
|
try {
|
|
1604
|
-
const stat4 = await
|
|
1691
|
+
const stat4 = await fs2.stat(lockPath);
|
|
1605
1692
|
if (Date.now() - stat4.mtimeMs > staleMs) {
|
|
1606
|
-
await
|
|
1693
|
+
await fs2.unlink(lockPath);
|
|
1607
1694
|
continue;
|
|
1608
1695
|
}
|
|
1609
1696
|
} catch {
|
|
1610
1697
|
continue;
|
|
1611
1698
|
}
|
|
1612
|
-
|
|
1699
|
+
const elapsed = Date.now() - started;
|
|
1700
|
+
if (elapsed >= timeoutMs) {
|
|
1613
1701
|
throw new FsError({
|
|
1614
1702
|
message: `Timed out waiting for file lock: ${targetPath}`,
|
|
1615
1703
|
code: "FS_ATOMIC_WRITE_FAILED",
|
|
@@ -1617,7 +1705,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
1617
1705
|
context: { timeoutMs }
|
|
1618
1706
|
});
|
|
1619
1707
|
}
|
|
1620
|
-
await
|
|
1708
|
+
await waitForLockRelease(lockPath, timeoutMs - elapsed);
|
|
1621
1709
|
}
|
|
1622
1710
|
}
|
|
1623
1711
|
try {
|
|
@@ -1628,22 +1716,66 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
1628
1716
|
} catch {
|
|
1629
1717
|
}
|
|
1630
1718
|
try {
|
|
1631
|
-
await
|
|
1719
|
+
await fs2.unlink(lockPath);
|
|
1632
1720
|
} catch {
|
|
1633
1721
|
}
|
|
1634
1722
|
}
|
|
1635
1723
|
}
|
|
1724
|
+
async function waitForLockRelease(lockPath, remainingMs) {
|
|
1725
|
+
const parentDir = path3.dirname(lockPath);
|
|
1726
|
+
const lockName = path3.basename(lockPath);
|
|
1727
|
+
const intervalMs = Math.min(remainingMs, 100);
|
|
1728
|
+
return new Promise((resolve4) => {
|
|
1729
|
+
let settled = false;
|
|
1730
|
+
let watcher = null;
|
|
1731
|
+
const timer = setTimeout(() => {
|
|
1732
|
+
settled = true;
|
|
1733
|
+
watcher?.close();
|
|
1734
|
+
resolve4();
|
|
1735
|
+
}, intervalMs);
|
|
1736
|
+
try {
|
|
1737
|
+
watcher = watchDir(parentDir, (eventType, filename) => {
|
|
1738
|
+
if (settled) return;
|
|
1739
|
+
if (filename === lockName && (eventType === "rename" || eventType === "change")) {
|
|
1740
|
+
settled = true;
|
|
1741
|
+
clearTimeout(timer);
|
|
1742
|
+
watcher?.close();
|
|
1743
|
+
resolve4();
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
} catch {
|
|
1747
|
+
clearTimeout(timer);
|
|
1748
|
+
if (!settled) {
|
|
1749
|
+
settled = true;
|
|
1750
|
+
setTimeout(resolve4, Math.min(remainingMs, 25));
|
|
1751
|
+
}
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
fs2.access(lockPath).then(
|
|
1755
|
+
() => {
|
|
1756
|
+
},
|
|
1757
|
+
() => {
|
|
1758
|
+
if (!settled) {
|
|
1759
|
+
settled = true;
|
|
1760
|
+
clearTimeout(timer);
|
|
1761
|
+
watcher?.close();
|
|
1762
|
+
resolve4();
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
);
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1636
1768
|
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
1637
1769
|
async function renameWithRetry(from, to) {
|
|
1638
1770
|
if (process.platform !== "win32") {
|
|
1639
|
-
await
|
|
1771
|
+
await fs2.rename(from, to);
|
|
1640
1772
|
return;
|
|
1641
1773
|
}
|
|
1642
1774
|
const delays = [10, 25, 60, 120, 250];
|
|
1643
1775
|
let lastErr;
|
|
1644
1776
|
for (let i = 0; i <= delays.length; i++) {
|
|
1645
1777
|
try {
|
|
1646
|
-
await
|
|
1778
|
+
await fs2.rename(from, to);
|
|
1647
1779
|
return;
|
|
1648
1780
|
} catch (err) {
|
|
1649
1781
|
lastErr = err;
|
|
@@ -1657,6 +1789,181 @@ async function renameWithRetry(from, to) {
|
|
|
1657
1789
|
throw lastErr;
|
|
1658
1790
|
}
|
|
1659
1791
|
|
|
1792
|
+
// src/coordination/mailbox-constants.ts
|
|
1793
|
+
var AGENT_STALE_MS = 6e4;
|
|
1794
|
+
var CLIENT_STALE_MS = 6e4;
|
|
1795
|
+
var HEARTBEAT_THROTTLE_MS = 5e3;
|
|
1796
|
+
var REGISTRY_CACHE_TTL_MS = 2e3;
|
|
1797
|
+
var LINE_SEPARATOR = "\n";
|
|
1798
|
+
var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
|
|
1799
|
+
var AUTO_COMPACT_INTERVAL_MS = 3e5;
|
|
1800
|
+
var AUTO_COMPACT_READ_MAX_AGE_MS = 6e5;
|
|
1801
|
+
var AUTO_COMPACT_DEFAULT_TTL_MS = 864e5;
|
|
1802
|
+
|
|
1803
|
+
// src/coordination/mailbox-message-codec.ts
|
|
1804
|
+
var MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
1805
|
+
"note",
|
|
1806
|
+
"ask",
|
|
1807
|
+
"assign",
|
|
1808
|
+
"steer",
|
|
1809
|
+
"btw",
|
|
1810
|
+
"broadcast",
|
|
1811
|
+
"status",
|
|
1812
|
+
"result",
|
|
1813
|
+
"review",
|
|
1814
|
+
"control"
|
|
1815
|
+
]);
|
|
1816
|
+
var PRIORITIES = /* @__PURE__ */ new Set(["low", "normal", "high"]);
|
|
1817
|
+
var TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
1818
|
+
"pending",
|
|
1819
|
+
"in_progress",
|
|
1820
|
+
"completed",
|
|
1821
|
+
"failed",
|
|
1822
|
+
"idle",
|
|
1823
|
+
"running",
|
|
1824
|
+
"streaming",
|
|
1825
|
+
"waiting_user",
|
|
1826
|
+
"error",
|
|
1827
|
+
"offline",
|
|
1828
|
+
"busy"
|
|
1829
|
+
]);
|
|
1830
|
+
function isRecord(value) {
|
|
1831
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1832
|
+
}
|
|
1833
|
+
function requiredString(record, key) {
|
|
1834
|
+
const value = record[key];
|
|
1835
|
+
if (typeof value !== "string") {
|
|
1836
|
+
throw new TypeError(`mailbox message field "${key}" must be a string`);
|
|
1837
|
+
}
|
|
1838
|
+
return value;
|
|
1839
|
+
}
|
|
1840
|
+
function optionalString(record, key) {
|
|
1841
|
+
const value = record[key];
|
|
1842
|
+
if (value === void 0) return {};
|
|
1843
|
+
if (typeof value !== "string") {
|
|
1844
|
+
throw new TypeError(`mailbox message field "${key}" must be a string when present`);
|
|
1845
|
+
}
|
|
1846
|
+
return { [key]: value };
|
|
1847
|
+
}
|
|
1848
|
+
function parseMessageType(value) {
|
|
1849
|
+
if (value === "info") return "note";
|
|
1850
|
+
if (value === "task") return "assign";
|
|
1851
|
+
if (typeof value === "string" && MESSAGE_TYPES.has(value)) {
|
|
1852
|
+
return value;
|
|
1853
|
+
}
|
|
1854
|
+
throw new TypeError('mailbox message field "type" is invalid');
|
|
1855
|
+
}
|
|
1856
|
+
function normalizeMailboxMessageType(value) {
|
|
1857
|
+
return parseMessageType(value);
|
|
1858
|
+
}
|
|
1859
|
+
function parsePriority(value) {
|
|
1860
|
+
if (typeof value === "string" && PRIORITIES.has(value)) {
|
|
1861
|
+
return value;
|
|
1862
|
+
}
|
|
1863
|
+
if (typeof value === "string") return "normal";
|
|
1864
|
+
throw new TypeError('mailbox message field "priority" must be a string');
|
|
1865
|
+
}
|
|
1866
|
+
function parseReadReceipts(record, to) {
|
|
1867
|
+
const value = record["readBy"];
|
|
1868
|
+
if (value === void 0) {
|
|
1869
|
+
const legacyReadAt = record["readAt"];
|
|
1870
|
+
return record["read"] === true && typeof legacyReadAt === "string" ? { [to || "unknown"]: legacyReadAt } : {};
|
|
1871
|
+
}
|
|
1872
|
+
if (!isRecord(value)) {
|
|
1873
|
+
throw new TypeError('mailbox message field "readBy" must be an object');
|
|
1874
|
+
}
|
|
1875
|
+
const receipts = {};
|
|
1876
|
+
for (const [agentId, timestamp] of Object.entries(value)) {
|
|
1877
|
+
if (typeof timestamp !== "string") {
|
|
1878
|
+
throw new TypeError("mailbox message read receipt timestamps must be strings");
|
|
1879
|
+
}
|
|
1880
|
+
receipts[agentId] = timestamp;
|
|
1881
|
+
}
|
|
1882
|
+
return receipts;
|
|
1883
|
+
}
|
|
1884
|
+
function parseTaskContext(value) {
|
|
1885
|
+
if (value === void 0) return void 0;
|
|
1886
|
+
if (!isRecord(value)) {
|
|
1887
|
+
throw new TypeError('mailbox message field "taskContext" must be an object');
|
|
1888
|
+
}
|
|
1889
|
+
const status = value["status"];
|
|
1890
|
+
if (status !== void 0 && (typeof status !== "string" || !TASK_STATUSES.has(status))) {
|
|
1891
|
+
throw new TypeError("mailbox message taskContext status is invalid");
|
|
1892
|
+
}
|
|
1893
|
+
return {
|
|
1894
|
+
...optionalString(value, "agentRole"),
|
|
1895
|
+
...optionalString(value, "agentName"),
|
|
1896
|
+
...optionalString(value, "taskId"),
|
|
1897
|
+
...status === void 0 ? {} : { status }
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
function parseMailboxMessage(value) {
|
|
1901
|
+
if (!isRecord(value)) throw new TypeError("mailbox message must be an object");
|
|
1902
|
+
const to = value["to"] === void 0 ? "" : requiredString(value, "to");
|
|
1903
|
+
const completed = value["completed"];
|
|
1904
|
+
if (typeof completed !== "boolean") {
|
|
1905
|
+
throw new TypeError('mailbox message field "completed" must be a boolean');
|
|
1906
|
+
}
|
|
1907
|
+
const taskContext = parseTaskContext(value["taskContext"]);
|
|
1908
|
+
return {
|
|
1909
|
+
id: requiredString(value, "id"),
|
|
1910
|
+
from: requiredString(value, "from"),
|
|
1911
|
+
to,
|
|
1912
|
+
type: parseMessageType(value["type"]),
|
|
1913
|
+
subject: requiredString(value, "subject"),
|
|
1914
|
+
body: requiredString(value, "body"),
|
|
1915
|
+
priority: parsePriority(value["priority"]),
|
|
1916
|
+
readBy: parseReadReceipts(value, to),
|
|
1917
|
+
completed,
|
|
1918
|
+
timestamp: requiredString(value, "timestamp"),
|
|
1919
|
+
...optionalString(value, "completedBy"),
|
|
1920
|
+
...optionalString(value, "outcome"),
|
|
1921
|
+
...optionalString(value, "completedAt"),
|
|
1922
|
+
...optionalString(value, "deletedAt"),
|
|
1923
|
+
...optionalString(value, "deletedBy"),
|
|
1924
|
+
...optionalString(value, "replyTo"),
|
|
1925
|
+
...optionalString(value, "senderSessionId"),
|
|
1926
|
+
...optionalString(value, "expiresAt"),
|
|
1927
|
+
...taskContext === void 0 ? {} : { taskContext }
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
function isAckRecord(value) {
|
|
1931
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && value["__ack"] === true;
|
|
1932
|
+
}
|
|
1933
|
+
function parseMailboxLine(line) {
|
|
1934
|
+
try {
|
|
1935
|
+
const parsed = JSON.parse(line);
|
|
1936
|
+
if (isAckRecord(parsed)) return parsed;
|
|
1937
|
+
return parseMailboxMessage(parsed);
|
|
1938
|
+
} catch {
|
|
1939
|
+
return null;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
function applyAckToMessage(msg, ack) {
|
|
1943
|
+
if (ack.read && !(ack.readerId in msg.readBy)) {
|
|
1944
|
+
msg.readBy[ack.readerId] = ack.timestamp;
|
|
1945
|
+
}
|
|
1946
|
+
if (ack.completed && !msg.completed) {
|
|
1947
|
+
msg.completed = true;
|
|
1948
|
+
msg.completedBy = ack.completedBy ?? ack.readerId;
|
|
1949
|
+
msg.completedAt = ack.timestamp;
|
|
1950
|
+
}
|
|
1951
|
+
if (ack.outcome !== void 0 && msg.outcome !== ack.outcome) {
|
|
1952
|
+
msg.outcome = ack.outcome;
|
|
1953
|
+
}
|
|
1954
|
+
if (ack.deleted === true) {
|
|
1955
|
+
msg.deletedAt = ack.timestamp;
|
|
1956
|
+
msg.deletedBy = ack.deletedBy ?? ack.readerId;
|
|
1957
|
+
}
|
|
1958
|
+
if (ack.deleted === false) {
|
|
1959
|
+
delete msg.deletedAt;
|
|
1960
|
+
delete msg.deletedBy;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
function serializeAckRecord(ack) {
|
|
1964
|
+
return JSON.stringify(ack) + "\n";
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1660
1967
|
// src/coordination/mailbox-types.ts
|
|
1661
1968
|
function normalizeRecipient(to) {
|
|
1662
1969
|
return to.trim().toLowerCase() === "all" ? "*" : to.trim();
|
|
@@ -1665,13 +1972,6 @@ function normalizeRecipient(to) {
|
|
|
1665
1972
|
// src/coordination/global-mailbox.ts
|
|
1666
1973
|
var MAILBOX_FILE = "_mailbox.jsonl";
|
|
1667
1974
|
var CLIENT_REGISTRY_FILE = "_mailbox.clients.json";
|
|
1668
|
-
var AGENT_STALE_MS = 6e4;
|
|
1669
|
-
var AGENT_PURGE_MS = 864e5;
|
|
1670
|
-
var CLIENT_STALE_MS = 6e4;
|
|
1671
|
-
var HEARTBEAT_THROTTLE_MS = 5e3;
|
|
1672
|
-
var REGISTRY_CACHE_TTL_MS = 2e3;
|
|
1673
|
-
var LINE_SEPARATOR = "\n";
|
|
1674
|
-
var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
|
|
1675
1975
|
var GlobalMailbox = class {
|
|
1676
1976
|
/** Path to the JSONL message file. */
|
|
1677
1977
|
messagePath;
|
|
@@ -1683,6 +1983,12 @@ var GlobalMailbox = class {
|
|
|
1683
1983
|
_events;
|
|
1684
1984
|
/** Optional HQ publisher for cross-project command-center telemetry. */
|
|
1685
1985
|
_hqPublisher;
|
|
1986
|
+
/**
|
|
1987
|
+
* Optional SSE event emitter for real-time push to HTTP bridge clients.
|
|
1988
|
+
* When present, `send`/`ack`/`softDelete`/`restore` emit events that
|
|
1989
|
+
* connected SSE subscribers receive instantly.
|
|
1990
|
+
*/
|
|
1991
|
+
eventEmitter;
|
|
1686
1992
|
/**
|
|
1687
1993
|
* Local cache of the agent registry to avoid re-reading on every call.
|
|
1688
1994
|
* Time-bounded: the registry file is shared ACROSS PROCESSES (that's the
|
|
@@ -1725,20 +2031,47 @@ var GlobalMailbox = class {
|
|
|
1725
2031
|
* appended message). The chain runs each read to completion before
|
|
1726
2032
|
* the next starts, in issue order — readers are read-only and never
|
|
1727
2033
|
* conflict with each other on content, only on the cache mutation
|
|
1728
|
-
* that follows the read. Pattern mirrors
|
|
2034
|
+
* that follows the read. Pattern mirrors a serialized mutation chain.
|
|
1729
2035
|
*/
|
|
1730
2036
|
_readChain = Promise.resolve([]);
|
|
2037
|
+
/**
|
|
2038
|
+
* Recipient → Set of indices into `_messageCache`. Maintained alongside
|
|
2039
|
+
* the cache so `query({ to })` and `unreadCount(agentId)` can skip the
|
|
2040
|
+
* O(N) full scan and iterate only the messages addressed to the target
|
|
2041
|
+
* recipient (plus broadcasts `*`).
|
|
2042
|
+
*
|
|
2043
|
+
* The index is rebuilt whenever the cache array is replaced
|
|
2044
|
+
* (`_setMessageCache`), extended when new messages are pushed
|
|
2045
|
+
* (`_pushToCache`), and cleared on `close()`.
|
|
2046
|
+
* It is `null` when the cache itself is null (> MESSAGE_CACHE_MAX_ENTRIES
|
|
2047
|
+
* or never populated).
|
|
2048
|
+
*
|
|
2049
|
+
* Safety: array indices are valid only as long as the cache array
|
|
2050
|
+
* reference hasn't changed. Since we rebuild the index in the same
|
|
2051
|
+
* synchronous step that replaces the cache, a reader that calls
|
|
2052
|
+
* `_readMessagesCached()` either gets the old array + old index, or
|
|
2053
|
+
* the new array + new index — never a mismatch.
|
|
2054
|
+
*/
|
|
2055
|
+
_recipientIndex = null;
|
|
2056
|
+
/**
|
|
2057
|
+
* Sender → Set of indices into `_messageCache`. Same lifecycle and
|
|
2058
|
+
* safety properties as `_recipientIndex`, but keyed on `msg.from`
|
|
2059
|
+
* so `query({ from })` can skip the full scan.
|
|
2060
|
+
*/
|
|
2061
|
+
_senderIndex = null;
|
|
1731
2062
|
/**
|
|
1732
2063
|
* @param projectDir — `~/.wrongstack/projects/<slug>/`
|
|
1733
2064
|
* @param events — optional EventBus for real-time TUI/WebUI notifications
|
|
1734
2065
|
* @param hqPublisher — optional HQ publisher, or getter, for cross-project telemetry
|
|
2066
|
+
* @param eventEmitter — optional SSE event emitter for HTTP bridge push
|
|
1735
2067
|
*/
|
|
1736
|
-
constructor(projectDir, events, hqPublisher) {
|
|
2068
|
+
constructor(projectDir, events, hqPublisher, eventEmitter) {
|
|
1737
2069
|
this.messagePath = path4.join(projectDir, MAILBOX_FILE);
|
|
1738
2070
|
this.registryPath = path4.join(projectDir, "_mailbox.registry.json");
|
|
1739
2071
|
this.clientRegistryPath = path4.join(projectDir, CLIENT_REGISTRY_FILE);
|
|
1740
2072
|
this._events = events;
|
|
1741
2073
|
this._hqPublisher = hqPublisher;
|
|
2074
|
+
this.eventEmitter = eventEmitter;
|
|
1742
2075
|
}
|
|
1743
2076
|
get hqMailboxId() {
|
|
1744
2077
|
return `${path4.basename(path4.dirname(this.messagePath))}:mailbox`;
|
|
@@ -1767,7 +2100,7 @@ var GlobalMailbox = class {
|
|
|
1767
2100
|
// "all" is an accepted spelling of the broadcast address — canonical
|
|
1768
2101
|
// form on disk is '*' so every query/checker matches it.
|
|
1769
2102
|
to: normalizeRecipient(input.to),
|
|
1770
|
-
type: input.type,
|
|
2103
|
+
type: normalizeMailboxMessageType(input.type),
|
|
1771
2104
|
subject: input.subject,
|
|
1772
2105
|
body: input.body,
|
|
1773
2106
|
priority: input.priority ?? "normal",
|
|
@@ -1775,7 +2108,8 @@ var GlobalMailbox = class {
|
|
|
1775
2108
|
completed: false,
|
|
1776
2109
|
timestamp: now,
|
|
1777
2110
|
replyTo: input.replyTo,
|
|
1778
|
-
taskContext: input.taskContext
|
|
2111
|
+
taskContext: input.taskContext,
|
|
2112
|
+
expiresAt: input.ttlMs !== void 0 ? new Date(Date.now() + input.ttlMs).toISOString() : void 0
|
|
1779
2113
|
};
|
|
1780
2114
|
const line = JSON.stringify(msg) + LINE_SEPARATOR;
|
|
1781
2115
|
await fsp.mkdir(path4.dirname(this.messagePath), { recursive: true });
|
|
@@ -1790,21 +2124,49 @@ var GlobalMailbox = class {
|
|
|
1790
2124
|
message: msg
|
|
1791
2125
|
});
|
|
1792
2126
|
this.publishHqMailboxSnapshot();
|
|
2127
|
+
this._events?.emitCustom("mailbox.message_sent", {
|
|
2128
|
+
messageId: msg.id,
|
|
2129
|
+
from: msg.from,
|
|
2130
|
+
to: msg.to,
|
|
2131
|
+
type: msg.type,
|
|
2132
|
+
subject: msg.subject
|
|
2133
|
+
});
|
|
2134
|
+
this.eventEmitter?.emit({
|
|
2135
|
+
type: "message.sent",
|
|
2136
|
+
messageId: msg.id,
|
|
2137
|
+
from: msg.from,
|
|
2138
|
+
to: msg.to,
|
|
2139
|
+
timestamp: now
|
|
2140
|
+
});
|
|
1793
2141
|
return msg;
|
|
1794
2142
|
}
|
|
1795
2143
|
async query(q) {
|
|
2144
|
+
const queryType = q.type === void 0 ? void 0 : normalizeMailboxMessageType(q.type);
|
|
1796
2145
|
const all = await this._readMessagesCached();
|
|
1797
2146
|
const limit = q.limit ?? 50;
|
|
1798
2147
|
const order = q.minPriority !== void 0 ? { low: 0, normal: 1, high: 2 } : null;
|
|
1799
2148
|
const minPriorityRank = order && q.minPriority !== void 0 ? order[q.minPriority] : 0;
|
|
1800
2149
|
const out = [];
|
|
1801
|
-
|
|
1802
|
-
|
|
2150
|
+
let candidates;
|
|
2151
|
+
if (q.to !== void 0 && this._recipientIndex !== null && q.from === void 0 && q.type === void 0 && q.minPriority === void 0) {
|
|
2152
|
+
const indices = /* @__PURE__ */ new Set();
|
|
2153
|
+
const direct = this._recipientIndex.get(q.to);
|
|
2154
|
+
if (direct !== void 0) for (const i of direct) indices.add(i);
|
|
2155
|
+
const broadcasts = this._recipientIndex.get("*");
|
|
2156
|
+
if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
|
|
2157
|
+
candidates = [...indices].sort((a, b) => a - b).map((i) => all[i]);
|
|
2158
|
+
} else if (q.from !== void 0 && q.to === void 0 && this._senderIndex !== null && q.type === void 0 && q.minPriority === void 0) {
|
|
2159
|
+
const indices = this._senderIndex.get(q.from);
|
|
2160
|
+
candidates = indices !== void 0 ? [...indices].sort((a, b) => a - b).map((i) => all[i]) : [];
|
|
2161
|
+
} else {
|
|
2162
|
+
candidates = all;
|
|
2163
|
+
}
|
|
2164
|
+
for (const m of candidates) {
|
|
1803
2165
|
if (q.to !== void 0 && m.to !== q.to && m.to !== "*") continue;
|
|
1804
2166
|
if (q.from !== void 0 && m.from !== q.from) continue;
|
|
1805
2167
|
if (q.unreadBy !== void 0 && q.unreadBy in m.readBy) continue;
|
|
1806
2168
|
if (q.incompleteOnly && m.completed) continue;
|
|
1807
|
-
if (
|
|
2169
|
+
if (queryType !== void 0 && m.type !== queryType) continue;
|
|
1808
2170
|
if (order !== null && (order[m.priority] ?? 1) < minPriorityRank) {
|
|
1809
2171
|
continue;
|
|
1810
2172
|
}
|
|
@@ -1819,42 +2181,100 @@ var GlobalMailbox = class {
|
|
|
1819
2181
|
const updated = await this.ackMany({ acks: [input] });
|
|
1820
2182
|
return updated.length > 0 ? updated[0] : null;
|
|
1821
2183
|
}
|
|
2184
|
+
/** Diagnostic counters exposed for test/tool introspection. */
|
|
2185
|
+
diag = {
|
|
2186
|
+
ackManyCacheDesync: 0,
|
|
2187
|
+
ackManyPreLockMtime: 0,
|
|
2188
|
+
ackManyPostLockMtime: 0,
|
|
2189
|
+
ackManyPreLockSize: 0,
|
|
2190
|
+
ackManyPostLockSize: 0,
|
|
2191
|
+
ackManyMessageCount: 0,
|
|
2192
|
+
ackManyAckCount: 0,
|
|
2193
|
+
cacheHitCount: 0,
|
|
2194
|
+
cacheMissCount: 0,
|
|
2195
|
+
cacheDesyncNoOp: 0,
|
|
2196
|
+
applyAckTargetMissing: 0,
|
|
2197
|
+
setMessageCacheCount: 0,
|
|
2198
|
+
pushToCacheCount: 0
|
|
2199
|
+
};
|
|
1822
2200
|
async ackMany(input) {
|
|
1823
2201
|
if (input.acks.length === 0) return [];
|
|
2202
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2203
|
+
const ackRecords = [];
|
|
1824
2204
|
const updated = [];
|
|
2205
|
+
const targetIds = /* @__PURE__ */ new Set();
|
|
1825
2206
|
const byId = /* @__PURE__ */ new Map();
|
|
1826
2207
|
for (const a of input.acks) {
|
|
1827
2208
|
byId.set(a.messageId, a);
|
|
2209
|
+
targetIds.add(a.messageId);
|
|
1828
2210
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
msg.completedBy = a.readerId;
|
|
1844
|
-
msg.completedAt = now;
|
|
1845
|
-
changed = true;
|
|
1846
|
-
}
|
|
1847
|
-
if (a.outcome !== void 0 && msg.outcome !== a.outcome) {
|
|
1848
|
-
msg.outcome = a.outcome;
|
|
1849
|
-
changed = true;
|
|
1850
|
-
}
|
|
2211
|
+
const preLockMtime = this._messageCacheMtime;
|
|
2212
|
+
const preLockSize = this._messageCacheSize;
|
|
2213
|
+
this.diag.ackManyPreLockMtime = preLockMtime;
|
|
2214
|
+
this.diag.ackManyPreLockSize = preLockSize;
|
|
2215
|
+
const all = await this._readMessagesCached();
|
|
2216
|
+
for (const msg of all) {
|
|
2217
|
+
const a = byId.get(msg.id);
|
|
2218
|
+
if (!a) continue;
|
|
2219
|
+
const needsRead = a.read !== false && !(a.readerId in msg.readBy);
|
|
2220
|
+
const needsComplete = a.completed && !msg.completed;
|
|
2221
|
+
const needsOutcome = a.outcome !== void 0 && msg.outcome !== a.outcome;
|
|
2222
|
+
if (!needsRead && !needsComplete && !needsOutcome) {
|
|
2223
|
+
updated.push({ ...msg, readBy: { ...msg.readBy } });
|
|
2224
|
+
continue;
|
|
1851
2225
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
2226
|
+
const copy = { ...msg, readBy: { ...msg.readBy } };
|
|
2227
|
+
if (needsRead) copy.readBy[a.readerId] = now;
|
|
2228
|
+
if (needsComplete) {
|
|
2229
|
+
copy.completed = true;
|
|
2230
|
+
copy.completedBy = a.readerId;
|
|
2231
|
+
copy.completedAt = now;
|
|
1855
2232
|
}
|
|
2233
|
+
if (needsOutcome) copy.outcome = a.outcome;
|
|
2234
|
+
updated.push(copy);
|
|
2235
|
+
ackRecords.push({
|
|
2236
|
+
__ack: true,
|
|
2237
|
+
messageId: msg.id,
|
|
2238
|
+
readerId: a.readerId,
|
|
2239
|
+
timestamp: now,
|
|
2240
|
+
read: a.read !== false,
|
|
2241
|
+
completed: a.completed,
|
|
2242
|
+
completedBy: a.completed ? a.readerId : void 0,
|
|
2243
|
+
outcome: a.outcome
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
if (ackRecords.length === 0) {
|
|
2247
|
+
if (updated.length > 0) {
|
|
2248
|
+
for (const message of updated) {
|
|
2249
|
+
this.publishHqMailboxEvent({
|
|
2250
|
+
mailboxId: this.hqMailboxId,
|
|
2251
|
+
action: message.completed ? "message.completed" : "message.read",
|
|
2252
|
+
message
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
return updated;
|
|
2257
|
+
}
|
|
2258
|
+
const serialized = ackRecords.map((a) => serializeAckRecord(a)).join("");
|
|
2259
|
+
await withFileLock(this.messagePath, async () => {
|
|
2260
|
+
await fsp.appendFile(this.messagePath, serialized, "utf8");
|
|
1856
2261
|
const { mtimeMs, size } = await this._statMessageFile();
|
|
1857
|
-
|
|
2262
|
+
for (const ack of ackRecords) {
|
|
2263
|
+
this._applyAckToCache(ack);
|
|
2264
|
+
}
|
|
2265
|
+
this._messageCacheMtime = mtimeMs;
|
|
2266
|
+
this._messageCacheSize = size;
|
|
2267
|
+
this.diag.ackManyPostLockMtime = mtimeMs;
|
|
2268
|
+
this.diag.ackManyPostLockSize = size;
|
|
2269
|
+
this.diag.ackManyMessageCount = this._messageCache?.length ?? -1;
|
|
2270
|
+
this.diag.ackManyAckCount = ackRecords.length;
|
|
2271
|
+
const expectedMinSize = preLockSize + serialized.length;
|
|
2272
|
+
if (preLockSize > 0 && size > expectedMinSize) {
|
|
2273
|
+
const extraBytes = size - expectedMinSize;
|
|
2274
|
+
console.error(
|
|
2275
|
+
`[MAILBOX-DIAG] ackMany: post-lock size ${size} exceeds expected ${expectedMinSize} by ${extraBytes}B (mtime ${preLockMtime}->${mtimeMs}). Cache may be missing cross-process messages. diag.ackManyCacheDesync=${++this.diag.ackManyCacheDesync}`
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
1858
2278
|
});
|
|
1859
2279
|
for (const message of updated) {
|
|
1860
2280
|
this.publishHqMailboxEvent({
|
|
@@ -1862,6 +2282,13 @@ var GlobalMailbox = class {
|
|
|
1862
2282
|
action: message.completed ? "message.completed" : "message.read",
|
|
1863
2283
|
message
|
|
1864
2284
|
});
|
|
2285
|
+
this.eventEmitter?.emit({
|
|
2286
|
+
type: "message.acked",
|
|
2287
|
+
messageId: message.id,
|
|
2288
|
+
from: message.from,
|
|
2289
|
+
to: message.to,
|
|
2290
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2291
|
+
});
|
|
1865
2292
|
}
|
|
1866
2293
|
if (updated.length > 0) this.publishHqMailboxSnapshot();
|
|
1867
2294
|
return updated;
|
|
@@ -1869,68 +2296,108 @@ var GlobalMailbox = class {
|
|
|
1869
2296
|
async unreadCount(forAgentId) {
|
|
1870
2297
|
const all = await this._readMessagesCached();
|
|
1871
2298
|
let count = 0;
|
|
1872
|
-
|
|
1873
|
-
const
|
|
1874
|
-
|
|
1875
|
-
|
|
2299
|
+
if (this._recipientIndex !== null) {
|
|
2300
|
+
const indices = /* @__PURE__ */ new Set();
|
|
2301
|
+
const direct = this._recipientIndex.get(forAgentId);
|
|
2302
|
+
if (direct !== void 0) for (const i of direct) indices.add(i);
|
|
2303
|
+
const broadcasts = this._recipientIndex.get("*");
|
|
2304
|
+
if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
|
|
2305
|
+
for (const i of indices) {
|
|
2306
|
+
const m = all[i];
|
|
2307
|
+
if (!(forAgentId in m.readBy) && !m.completed) count++;
|
|
2308
|
+
}
|
|
2309
|
+
} else {
|
|
2310
|
+
for (let i = 0; i < all.length; i++) {
|
|
2311
|
+
const m = all[i];
|
|
2312
|
+
if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed) {
|
|
2313
|
+
count++;
|
|
2314
|
+
}
|
|
1876
2315
|
}
|
|
1877
2316
|
}
|
|
1878
2317
|
return count;
|
|
1879
2318
|
}
|
|
1880
2319
|
async softDelete(mailId, by) {
|
|
1881
|
-
|
|
2320
|
+
const all = await this._readMessagesCached();
|
|
2321
|
+
const target = all.find((m) => m.id === mailId);
|
|
2322
|
+
if (target === void 0) return null;
|
|
2323
|
+
if (target.deletedAt !== void 0) {
|
|
2324
|
+
return { ...target, readBy: { ...target.readBy } };
|
|
2325
|
+
}
|
|
2326
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2327
|
+
const ack = {
|
|
2328
|
+
__ack: true,
|
|
2329
|
+
messageId: mailId,
|
|
2330
|
+
readerId: by,
|
|
2331
|
+
timestamp: now,
|
|
2332
|
+
read: true,
|
|
2333
|
+
deleted: true,
|
|
2334
|
+
deletedBy: by
|
|
2335
|
+
};
|
|
1882
2336
|
await withFileLock(this.messagePath, async () => {
|
|
1883
|
-
|
|
1884
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1885
|
-
for (const m of all) {
|
|
1886
|
-
if (m.id !== mailId) continue;
|
|
1887
|
-
if (m.deletedAt !== void 0) {
|
|
1888
|
-
updated = m;
|
|
1889
|
-
continue;
|
|
1890
|
-
}
|
|
1891
|
-
m.deletedAt = now;
|
|
1892
|
-
m.deletedBy = by;
|
|
1893
|
-
updated = m;
|
|
1894
|
-
}
|
|
1895
|
-
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
1896
|
-
await fsp.writeFile(this.messagePath, serialized, "utf8");
|
|
2337
|
+
await fsp.appendFile(this.messagePath, serializeAckRecord(ack), "utf8");
|
|
1897
2338
|
const { mtimeMs, size } = await this._statMessageFile();
|
|
1898
|
-
this.
|
|
2339
|
+
this._applyAckToCache(ack);
|
|
2340
|
+
this._messageCacheMtime = mtimeMs;
|
|
2341
|
+
this._messageCacheSize = size;
|
|
1899
2342
|
});
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
}
|
|
1908
|
-
|
|
2343
|
+
const msg = { ...target, readBy: { ...target.readBy } };
|
|
2344
|
+
msg.deletedAt = now;
|
|
2345
|
+
msg.deletedBy = by;
|
|
2346
|
+
this.publishHqMailboxEvent({
|
|
2347
|
+
mailboxId: this.hqMailboxId,
|
|
2348
|
+
action: "message.updated",
|
|
2349
|
+
message: msg
|
|
2350
|
+
});
|
|
2351
|
+
this.publishHqMailboxSnapshot();
|
|
2352
|
+
this.eventEmitter?.emit({
|
|
2353
|
+
type: "message.deleted",
|
|
2354
|
+
messageId: msg.id,
|
|
2355
|
+
from: msg.from,
|
|
2356
|
+
to: msg.to,
|
|
2357
|
+
timestamp: msg.deletedAt ?? now
|
|
2358
|
+
});
|
|
2359
|
+
return msg;
|
|
1909
2360
|
}
|
|
1910
2361
|
async restore(mailId) {
|
|
1911
|
-
|
|
2362
|
+
const all = await this._readMessagesCached();
|
|
2363
|
+
const target = all.find((m) => m.id === mailId);
|
|
2364
|
+
if (target === void 0) return null;
|
|
2365
|
+
if (target.deletedAt === void 0 && target.deletedBy === void 0) {
|
|
2366
|
+
return { ...target, readBy: { ...target.readBy } };
|
|
2367
|
+
}
|
|
2368
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2369
|
+
const ack = {
|
|
2370
|
+
__ack: true,
|
|
2371
|
+
messageId: mailId,
|
|
2372
|
+
readerId: "",
|
|
2373
|
+
timestamp: now,
|
|
2374
|
+
read: true,
|
|
2375
|
+
deleted: false
|
|
2376
|
+
};
|
|
1912
2377
|
await withFileLock(this.messagePath, async () => {
|
|
1913
|
-
|
|
1914
|
-
for (const m of all) {
|
|
1915
|
-
if (m.id !== mailId) continue;
|
|
1916
|
-
delete m.deletedAt;
|
|
1917
|
-
delete m.deletedBy;
|
|
1918
|
-
updated = m;
|
|
1919
|
-
}
|
|
1920
|
-
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
1921
|
-
await fsp.writeFile(this.messagePath, serialized, "utf8");
|
|
2378
|
+
await fsp.appendFile(this.messagePath, serializeAckRecord(ack), "utf8");
|
|
1922
2379
|
const { mtimeMs, size } = await this._statMessageFile();
|
|
1923
|
-
this.
|
|
2380
|
+
this._applyAckToCache(ack);
|
|
2381
|
+
this._messageCacheMtime = mtimeMs;
|
|
2382
|
+
this._messageCacheSize = size;
|
|
1924
2383
|
});
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
}
|
|
1933
|
-
|
|
2384
|
+
const msg = { ...target, readBy: { ...target.readBy } };
|
|
2385
|
+
delete msg.deletedAt;
|
|
2386
|
+
delete msg.deletedBy;
|
|
2387
|
+
this.publishHqMailboxEvent({
|
|
2388
|
+
mailboxId: this.hqMailboxId,
|
|
2389
|
+
action: "message.updated",
|
|
2390
|
+
message: msg
|
|
2391
|
+
});
|
|
2392
|
+
this.publishHqMailboxSnapshot();
|
|
2393
|
+
this.eventEmitter?.emit({
|
|
2394
|
+
type: "message.restored",
|
|
2395
|
+
messageId: msg.id,
|
|
2396
|
+
from: msg.from,
|
|
2397
|
+
to: msg.to,
|
|
2398
|
+
timestamp: now
|
|
2399
|
+
});
|
|
2400
|
+
return msg;
|
|
1934
2401
|
}
|
|
1935
2402
|
// ── Agent registry ──────────────────────────────────────────────────────
|
|
1936
2403
|
async registerAgent(input) {
|
|
@@ -2061,8 +2528,19 @@ var GlobalMailbox = class {
|
|
|
2061
2528
|
}
|
|
2062
2529
|
async getAgentStatuses() {
|
|
2063
2530
|
await this._ensureRegistry();
|
|
2064
|
-
|
|
2531
|
+
let registry = await this._readRegistry();
|
|
2532
|
+
const before = registry.size;
|
|
2065
2533
|
this._pruneStaleInPlace(registry);
|
|
2534
|
+
if (registry.size < before) {
|
|
2535
|
+
await withFileLock(this.registryPath, async () => {
|
|
2536
|
+
const fresh = await this._readRegistry({ fresh: true });
|
|
2537
|
+
this._pruneStaleInPlace(fresh);
|
|
2538
|
+
this._registryCache = fresh;
|
|
2539
|
+
this._registryCacheAt = Date.now();
|
|
2540
|
+
await this._writeRegistry(fresh);
|
|
2541
|
+
registry = fresh;
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2066
2544
|
const now = Date.now();
|
|
2067
2545
|
return Array.from(registry.values()).map((a) => ({
|
|
2068
2546
|
agentId: a.agentId,
|
|
@@ -2114,6 +2592,20 @@ var GlobalMailbox = class {
|
|
|
2114
2592
|
});
|
|
2115
2593
|
this.publishHqMailboxSnapshot();
|
|
2116
2594
|
}
|
|
2595
|
+
async deregisterClient(clientId) {
|
|
2596
|
+
await this._ensureClientRegistry();
|
|
2597
|
+
await withFileLock(this.clientRegistryPath, async () => {
|
|
2598
|
+
const registry = await this._readClientRegistry({ fresh: true });
|
|
2599
|
+
this._pruneStaleClientsInPlace(registry);
|
|
2600
|
+
registry.delete(clientId);
|
|
2601
|
+
this._clientRegistryCache = registry;
|
|
2602
|
+
this._clientRegistryCacheAt = Date.now();
|
|
2603
|
+
await this._writeClientRegistry(registry);
|
|
2604
|
+
});
|
|
2605
|
+
this._lastClientHeartbeat.delete(clientId);
|
|
2606
|
+
this._events?.emitCustom("mailbox.client_deregistered", { clientId });
|
|
2607
|
+
this.publishHqMailboxSnapshot();
|
|
2608
|
+
}
|
|
2117
2609
|
async clientHeartbeat(input) {
|
|
2118
2610
|
const last = this._lastClientHeartbeat.get(input.clientId) ?? 0;
|
|
2119
2611
|
const now = Date.now();
|
|
@@ -2142,8 +2634,19 @@ var GlobalMailbox = class {
|
|
|
2142
2634
|
}
|
|
2143
2635
|
async getClientStatuses() {
|
|
2144
2636
|
await this._ensureClientRegistry();
|
|
2145
|
-
|
|
2637
|
+
let registry = await this._readClientRegistry();
|
|
2638
|
+
const before = registry.size;
|
|
2146
2639
|
this._pruneStaleClientsInPlace(registry);
|
|
2640
|
+
if (registry.size < before) {
|
|
2641
|
+
await withFileLock(this.clientRegistryPath, async () => {
|
|
2642
|
+
registry = await this._readClientRegistry({ fresh: true });
|
|
2643
|
+
const postLockBefore = registry.size;
|
|
2644
|
+
this._pruneStaleClientsInPlace(registry);
|
|
2645
|
+
if (registry.size < postLockBefore) {
|
|
2646
|
+
await this._writeClientRegistry(registry);
|
|
2647
|
+
}
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2147
2650
|
const now = Date.now();
|
|
2148
2651
|
return Array.from(registry.values()).map((c) => ({
|
|
2149
2652
|
clientId: c.clientId,
|
|
@@ -2155,6 +2658,32 @@ var GlobalMailbox = class {
|
|
|
2155
2658
|
pid: c.pid
|
|
2156
2659
|
})).sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
|
|
2157
2660
|
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Explicitly purge stale clients from the registry and write back to disk.
|
|
2663
|
+
* Removes client entries whose lastSeenAt is older than CLIENT_STALE_MS.
|
|
2664
|
+
* Returns the number of entries purged.
|
|
2665
|
+
*
|
|
2666
|
+
* Idempotent — safe to call regularly. The same pruning runs implicitly
|
|
2667
|
+
* inside registerClient, clientHeartbeat, and getClientStatuses, but if
|
|
2668
|
+
* no client registers or heartbeats for a long time, this public method
|
|
2669
|
+
* ensures the file gets cleaned up on demand.
|
|
2670
|
+
*/
|
|
2671
|
+
async purgeClients() {
|
|
2672
|
+
await this._ensureClientRegistry();
|
|
2673
|
+
let purged = 0;
|
|
2674
|
+
await withFileLock(this.clientRegistryPath, async () => {
|
|
2675
|
+
const registry = await this._readClientRegistry({ fresh: true });
|
|
2676
|
+
const before = registry.size;
|
|
2677
|
+
this._pruneStaleClientsInPlace(registry);
|
|
2678
|
+
purged = before - registry.size;
|
|
2679
|
+
if (purged > 0) {
|
|
2680
|
+
this._clientRegistryCache = registry;
|
|
2681
|
+
this._clientRegistryCacheAt = Date.now();
|
|
2682
|
+
await this._writeClientRegistry(registry);
|
|
2683
|
+
}
|
|
2684
|
+
});
|
|
2685
|
+
return purged;
|
|
2686
|
+
}
|
|
2158
2687
|
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
2159
2688
|
async close() {
|
|
2160
2689
|
this._registryCache = null;
|
|
@@ -2162,6 +2691,8 @@ var GlobalMailbox = class {
|
|
|
2162
2691
|
this._messageCache = null;
|
|
2163
2692
|
this._messageCacheMtime = -1;
|
|
2164
2693
|
this._messageCacheSize = -1;
|
|
2694
|
+
this._recipientIndex = null;
|
|
2695
|
+
this._senderIndex = null;
|
|
2165
2696
|
}
|
|
2166
2697
|
async clearAll() {
|
|
2167
2698
|
await withFileLock(this.messagePath, async () => {
|
|
@@ -2210,6 +2741,100 @@ var GlobalMailbox = class {
|
|
|
2210
2741
|
remaining
|
|
2211
2742
|
};
|
|
2212
2743
|
}
|
|
2744
|
+
// ── Auto-compaction ─────────────────────────────────────────────────────
|
|
2745
|
+
_autoCompactTimer = null;
|
|
2746
|
+
async autoCompact(opts) {
|
|
2747
|
+
const readMaxAgeMs = opts?.readMaxAgeMs ?? AUTO_COMPACT_READ_MAX_AGE_MS;
|
|
2748
|
+
const defaultTtlMs = opts?.defaultTtlMs ?? AUTO_COMPACT_DEFAULT_TTL_MS;
|
|
2749
|
+
const completedMaxAgeMs = opts?.completedMaxAgeMs ?? 864e5;
|
|
2750
|
+
const incompleteMaxAgeMs = opts?.incompleteMaxAgeMs ?? 6048e5;
|
|
2751
|
+
let expiredPurged = 0;
|
|
2752
|
+
let readByAllPurged = 0;
|
|
2753
|
+
let completedPurged = 0;
|
|
2754
|
+
let incompletePurged = 0;
|
|
2755
|
+
let remaining = 0;
|
|
2756
|
+
let onlineAgentIds = null;
|
|
2757
|
+
try {
|
|
2758
|
+
const statuses = await this.getAgentStatuses();
|
|
2759
|
+
const online = statuses.filter((s) => s.online);
|
|
2760
|
+
onlineAgentIds = online.length > 0 ? new Set(online.map((s) => s.agentId)) : null;
|
|
2761
|
+
} catch {
|
|
2762
|
+
onlineAgentIds = null;
|
|
2763
|
+
}
|
|
2764
|
+
await withFileLock(this.messagePath, async () => {
|
|
2765
|
+
const all = await this._readMessagesFresh();
|
|
2766
|
+
const now = Date.now();
|
|
2767
|
+
const cutoffReadAge = now - readMaxAgeMs;
|
|
2768
|
+
const cutoffCompleted = now - completedMaxAgeMs;
|
|
2769
|
+
const cutoffIncomplete = now - incompleteMaxAgeMs;
|
|
2770
|
+
const cutoffDefaultTtl = now - defaultTtlMs;
|
|
2771
|
+
const kept = [];
|
|
2772
|
+
for (const msg of all) {
|
|
2773
|
+
const msgTime = new Date(msg.timestamp).getTime();
|
|
2774
|
+
if (msg.expiresAt !== void 0) {
|
|
2775
|
+
if (new Date(msg.expiresAt).getTime() < now) {
|
|
2776
|
+
expiredPurged++;
|
|
2777
|
+
continue;
|
|
2778
|
+
}
|
|
2779
|
+
} else if (msgTime < cutoffDefaultTtl) {
|
|
2780
|
+
expiredPurged++;
|
|
2781
|
+
continue;
|
|
2782
|
+
}
|
|
2783
|
+
if (onlineAgentIds !== null && !msg.completed) {
|
|
2784
|
+
const readByAll = onlineAgentIds.size > 0 && [...onlineAgentIds].every((id) => id in msg.readBy);
|
|
2785
|
+
if (readByAll) {
|
|
2786
|
+
const readTimes = Object.values(msg.readBy).map((t) => new Date(t).getTime());
|
|
2787
|
+
const latestRead = Math.max(...readTimes);
|
|
2788
|
+
if (latestRead < cutoffReadAge) {
|
|
2789
|
+
readByAllPurged++;
|
|
2790
|
+
continue;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
const completedTime = msg.completedAt ? new Date(msg.completedAt).getTime() : 0;
|
|
2795
|
+
if (msg.completed && completedTime < cutoffCompleted) {
|
|
2796
|
+
completedPurged++;
|
|
2797
|
+
continue;
|
|
2798
|
+
}
|
|
2799
|
+
if (!msg.completed && msgTime < cutoffIncomplete) {
|
|
2800
|
+
incompletePurged++;
|
|
2801
|
+
continue;
|
|
2802
|
+
}
|
|
2803
|
+
kept.push(msg);
|
|
2804
|
+
}
|
|
2805
|
+
remaining = kept.length;
|
|
2806
|
+
if (kept.length < all.length) {
|
|
2807
|
+
const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
2808
|
+
await fsp.writeFile(this.messagePath, content, "utf8");
|
|
2809
|
+
}
|
|
2810
|
+
const { mtimeMs, size } = await this._statMessageFile();
|
|
2811
|
+
this._setMessageCache(kept, mtimeMs, size);
|
|
2812
|
+
});
|
|
2813
|
+
const totalRemoved = expiredPurged + readByAllPurged + completedPurged + incompletePurged;
|
|
2814
|
+
return {
|
|
2815
|
+
expiredRemoved: expiredPurged,
|
|
2816
|
+
readByAllRemoved: readByAllPurged,
|
|
2817
|
+
stalePurged: completedPurged + incompletePurged,
|
|
2818
|
+
totalRemoved,
|
|
2819
|
+
remaining
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
startAutoCompactTimer(opts) {
|
|
2823
|
+
if (this._autoCompactTimer !== null) {
|
|
2824
|
+
clearInterval(this._autoCompactTimer);
|
|
2825
|
+
}
|
|
2826
|
+
const intervalMs = opts?.intervalMs ?? AUTO_COMPACT_INTERVAL_MS;
|
|
2827
|
+
const timer = setInterval(() => {
|
|
2828
|
+
this.autoCompact(opts).catch(() => {
|
|
2829
|
+
});
|
|
2830
|
+
}, intervalMs);
|
|
2831
|
+
timer.unref?.();
|
|
2832
|
+
this._autoCompactTimer = timer;
|
|
2833
|
+
return () => {
|
|
2834
|
+
clearInterval(timer);
|
|
2835
|
+
this._autoCompactTimer = null;
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2213
2838
|
// ── Internal ────────────────────────────────────────────────────────────
|
|
2214
2839
|
/**
|
|
2215
2840
|
* Read all messages from the JSONL file. Always reads + parses the file.
|
|
@@ -2226,61 +2851,23 @@ var GlobalMailbox = class {
|
|
|
2226
2851
|
throw err;
|
|
2227
2852
|
}
|
|
2228
2853
|
}
|
|
2229
|
-
/**
|
|
2230
|
-
* Read only newly-appended bytes from the file and append them to the
|
|
2231
|
-
* in-memory cache. This avoids re-reading and re-parsing the entire file
|
|
2232
|
-
* when another process appended messages since our last read.
|
|
2233
|
-
*
|
|
2234
|
-
* Only safe when the file grew in size (messages are append-only). When
|
|
2235
|
-
* the file was rewritten (ack/purge changed existing content), callers
|
|
2236
|
-
* must fall back to {@link _readMessages}.
|
|
2237
|
-
*
|
|
2238
|
-
* @returns The (now up-to-date) message cache.
|
|
2239
|
-
*/
|
|
2240
|
-
async _readNewMessagesOnly(fd, oldSize, newSize) {
|
|
2241
|
-
const tailLen = newSize - oldSize;
|
|
2242
|
-
const buf = Buffer.alloc(tailLen);
|
|
2243
|
-
await fd.read(buf, 0, tailLen, oldSize);
|
|
2244
|
-
const tail = buf.toString("utf8");
|
|
2245
|
-
for (const line of tail.split(LINE_SEPARATOR)) {
|
|
2246
|
-
if (!line.trim()) continue;
|
|
2247
|
-
try {
|
|
2248
|
-
const parsed = JSON.parse(line);
|
|
2249
|
-
if (!parsed["readBy"]) {
|
|
2250
|
-
const readBy = {};
|
|
2251
|
-
if (parsed["read"] && parsed["readAt"]) {
|
|
2252
|
-
readBy[parsed["to"]] = parsed["readAt"];
|
|
2253
|
-
}
|
|
2254
|
-
parsed["readBy"] = readBy;
|
|
2255
|
-
delete parsed["read"];
|
|
2256
|
-
delete parsed["readAt"];
|
|
2257
|
-
}
|
|
2258
|
-
this._messageCache.push(parsed);
|
|
2259
|
-
} catch {
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
return this._messageCache;
|
|
2263
|
-
}
|
|
2264
|
-
/** Parse a JSONL string into MailboxMessage[], including migration. */
|
|
2854
|
+
/** Parse a JSONL string into MailboxMessage[], applying ack records. */
|
|
2265
2855
|
_parseLines(raw) {
|
|
2266
2856
|
const lines = raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
|
|
2267
2857
|
const messages = [];
|
|
2858
|
+
const acks = [];
|
|
2268
2859
|
for (const line of lines) {
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
if (parsed["read"] && parsed["readAt"]) {
|
|
2274
|
-
readBy[parsed["to"]] = parsed["readAt"];
|
|
2275
|
-
}
|
|
2276
|
-
parsed["readBy"] = readBy;
|
|
2277
|
-
delete parsed["read"];
|
|
2278
|
-
delete parsed["readAt"];
|
|
2279
|
-
}
|
|
2860
|
+
const parsed = parseMailboxLine(line);
|
|
2861
|
+
if (isAckRecord(parsed)) {
|
|
2862
|
+
acks.push(parsed);
|
|
2863
|
+
} else if (parsed !== null) {
|
|
2280
2864
|
messages.push(parsed);
|
|
2281
|
-
} catch {
|
|
2282
2865
|
}
|
|
2283
2866
|
}
|
|
2867
|
+
for (const ack of acks) {
|
|
2868
|
+
const target = messages.find((m) => m.id === ack.messageId);
|
|
2869
|
+
if (target) applyAckToMessage(target, ack);
|
|
2870
|
+
}
|
|
2284
2871
|
return messages;
|
|
2285
2872
|
}
|
|
2286
2873
|
/**
|
|
@@ -2362,17 +2949,6 @@ var GlobalMailbox = class {
|
|
|
2362
2949
|
if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
|
|
2363
2950
|
return this._messageCache;
|
|
2364
2951
|
}
|
|
2365
|
-
if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
|
|
2366
|
-
const fd = await fsp.open(this.messagePath, "r");
|
|
2367
|
-
try {
|
|
2368
|
-
const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
|
|
2369
|
-
this._messageCacheMtime = st.mtimeMs;
|
|
2370
|
-
this._messageCacheSize = st.size;
|
|
2371
|
-
return updated;
|
|
2372
|
-
} finally {
|
|
2373
|
-
await fd.close();
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
2952
|
const all = await this._readMessages();
|
|
2377
2953
|
this._setMessageCache(all, st.mtimeMs, st.size);
|
|
2378
2954
|
return all;
|
|
@@ -2404,11 +2980,61 @@ var GlobalMailbox = class {
|
|
|
2404
2980
|
this._messageCache = null;
|
|
2405
2981
|
this._messageCacheMtime = -1;
|
|
2406
2982
|
this._messageCacheSize = -1;
|
|
2983
|
+
this._recipientIndex = null;
|
|
2984
|
+
this._senderIndex = null;
|
|
2407
2985
|
return;
|
|
2408
2986
|
}
|
|
2409
2987
|
this._messageCache = messages;
|
|
2410
2988
|
this._messageCacheMtime = mtime;
|
|
2411
2989
|
this._messageCacheSize = size;
|
|
2990
|
+
this._rebuildRecipientIndex();
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Fold a single ack record into the in-memory cache, mutating the target
|
|
2994
|
+
* message in place. Called from `ackMany` after appending the record to
|
|
2995
|
+
* disk so the cache reflects the ack without a full re-read + re-parse.
|
|
2996
|
+
*
|
|
2997
|
+
* An ack only touches `readBy`/`completed*`/`outcome` — none of which
|
|
2998
|
+
* affect the recipient/sender indexes — so no index rebuild is needed.
|
|
2999
|
+
* No-op when the cache is empty or the target message isn't cached.
|
|
3000
|
+
*/
|
|
3001
|
+
_applyAckToCache(ack) {
|
|
3002
|
+
const cache = this._messageCache;
|
|
3003
|
+
if (cache === null) return;
|
|
3004
|
+
const target = cache.find((m) => m.id === ack.messageId);
|
|
3005
|
+
if (target !== void 0) applyAckToMessage(target, ack);
|
|
3006
|
+
}
|
|
3007
|
+
/**
|
|
3008
|
+
* Build the recipient AND sender indexes from the current `_messageCache`.
|
|
3009
|
+
* Called only from `_setMessageCache` so both indexes are always in sync
|
|
3010
|
+
* with a freshly-replaced cache array.
|
|
3011
|
+
*/
|
|
3012
|
+
_rebuildRecipientIndex() {
|
|
3013
|
+
const cache = this._messageCache;
|
|
3014
|
+
if (cache === null) {
|
|
3015
|
+
this._recipientIndex = null;
|
|
3016
|
+
this._senderIndex = null;
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
const recIdx = /* @__PURE__ */ new Map();
|
|
3020
|
+
const sndIdx = /* @__PURE__ */ new Map();
|
|
3021
|
+
for (let i = 0; i < cache.length; i++) {
|
|
3022
|
+
const msg = cache[i];
|
|
3023
|
+
let rSet = recIdx.get(msg.to);
|
|
3024
|
+
if (rSet === void 0) {
|
|
3025
|
+
rSet = /* @__PURE__ */ new Set();
|
|
3026
|
+
recIdx.set(msg.to, rSet);
|
|
3027
|
+
}
|
|
3028
|
+
rSet.add(i);
|
|
3029
|
+
let sSet = sndIdx.get(msg.from);
|
|
3030
|
+
if (sSet === void 0) {
|
|
3031
|
+
sSet = /* @__PURE__ */ new Set();
|
|
3032
|
+
sndIdx.set(msg.from, sSet);
|
|
3033
|
+
}
|
|
3034
|
+
sSet.add(i);
|
|
3035
|
+
}
|
|
3036
|
+
this._recipientIndex = recIdx;
|
|
3037
|
+
this._senderIndex = sndIdx;
|
|
2412
3038
|
}
|
|
2413
3039
|
/**
|
|
2414
3040
|
* Append a single just-sent message to the in-memory cache without
|
|
@@ -2435,6 +3061,23 @@ var GlobalMailbox = class {
|
|
|
2435
3061
|
return;
|
|
2436
3062
|
}
|
|
2437
3063
|
this._messageCache.push(msg);
|
|
3064
|
+
const newIdx = this._messageCache.length - 1;
|
|
3065
|
+
if (this._recipientIndex !== null) {
|
|
3066
|
+
let set = this._recipientIndex.get(msg.to);
|
|
3067
|
+
if (set === void 0) {
|
|
3068
|
+
set = /* @__PURE__ */ new Set();
|
|
3069
|
+
this._recipientIndex.set(msg.to, set);
|
|
3070
|
+
}
|
|
3071
|
+
set.add(newIdx);
|
|
3072
|
+
}
|
|
3073
|
+
if (this._senderIndex !== null) {
|
|
3074
|
+
let set = this._senderIndex.get(msg.from);
|
|
3075
|
+
if (set === void 0) {
|
|
3076
|
+
set = /* @__PURE__ */ new Set();
|
|
3077
|
+
this._senderIndex.set(msg.from, set);
|
|
3078
|
+
}
|
|
3079
|
+
set.add(newIdx);
|
|
3080
|
+
}
|
|
2438
3081
|
this._messageCacheMtime = mtime;
|
|
2439
3082
|
this._messageCacheSize = size;
|
|
2440
3083
|
}
|
|
@@ -2466,22 +3109,13 @@ var GlobalMailbox = class {
|
|
|
2466
3109
|
}
|
|
2467
3110
|
}
|
|
2468
3111
|
_pruneStaleInPlace(registry) {
|
|
2469
|
-
const
|
|
2470
|
-
const purgeCutoff = Date.now() - AGENT_PURGE_MS;
|
|
2471
|
-
const toDelete = [];
|
|
3112
|
+
const cutoff = Date.now() - AGENT_STALE_MS;
|
|
2472
3113
|
for (const [id, agent] of registry) {
|
|
2473
|
-
const lastSeen =
|
|
2474
|
-
if (lastSeen <
|
|
2475
|
-
|
|
2476
|
-
continue;
|
|
2477
|
-
}
|
|
2478
|
-
if (lastSeen < staleCutoff) {
|
|
2479
|
-
agent.status = "idle";
|
|
3114
|
+
const lastSeen = Date.parse(agent.lastSeenAt);
|
|
3115
|
+
if (!Number.isFinite(lastSeen) || lastSeen < cutoff) {
|
|
3116
|
+
registry.delete(id);
|
|
2480
3117
|
}
|
|
2481
3118
|
}
|
|
2482
|
-
for (const id of toDelete) {
|
|
2483
|
-
registry.delete(id);
|
|
2484
|
-
}
|
|
2485
3119
|
}
|
|
2486
3120
|
async _writeRegistry(registry) {
|
|
2487
3121
|
const obj = {};
|
|
@@ -2522,9 +3156,10 @@ var GlobalMailbox = class {
|
|
|
2522
3156
|
}
|
|
2523
3157
|
_pruneStaleClientsInPlace(registry) {
|
|
2524
3158
|
const cutoff = Date.now() - CLIENT_STALE_MS;
|
|
2525
|
-
for (const client of registry
|
|
2526
|
-
|
|
2527
|
-
|
|
3159
|
+
for (const [id, client] of registry) {
|
|
3160
|
+
const lastSeen = Date.parse(client.lastSeenAt);
|
|
3161
|
+
if (!Number.isFinite(lastSeen) || lastSeen < cutoff) {
|
|
3162
|
+
registry.delete(id);
|
|
2528
3163
|
}
|
|
2529
3164
|
}
|
|
2530
3165
|
}
|
|
@@ -2540,8 +3175,8 @@ var GlobalMailbox = class {
|
|
|
2540
3175
|
};
|
|
2541
3176
|
|
|
2542
3177
|
// src/hq/auth-store.ts
|
|
2543
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2544
|
-
import * as
|
|
3178
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID3, scrypt, timingSafeEqual } from "node:crypto";
|
|
3179
|
+
import * as fs3 from "node:fs/promises";
|
|
2545
3180
|
import * as syncFs from "node:fs";
|
|
2546
3181
|
import * as path5 from "node:path";
|
|
2547
3182
|
var HQ_AUTH_FILE_VERSION = 1;
|
|
@@ -2601,29 +3236,31 @@ function readHqRuntimeFileSync(dataDir) {
|
|
|
2601
3236
|
return void 0;
|
|
2602
3237
|
}
|
|
2603
3238
|
}
|
|
2604
|
-
async function readHqAuthFile(dataDir,
|
|
3239
|
+
async function readHqAuthFile(dataDir, _opts = {}) {
|
|
2605
3240
|
const file = hqAuthFilePath(dataDir);
|
|
2606
3241
|
let raw;
|
|
2607
3242
|
try {
|
|
2608
|
-
raw = await
|
|
3243
|
+
raw = await fs3.readFile(file, "utf8");
|
|
2609
3244
|
} catch (err) {
|
|
2610
3245
|
if (err.code === "ENOENT") return emptyHqAuthFile();
|
|
2611
|
-
|
|
2612
|
-
|
|
3246
|
+
throw new Error(
|
|
3247
|
+
`HQ auth file at ${file} cannot be read: ${err.message}. Fix the file permissions or delete it to start with an empty auth state.`
|
|
3248
|
+
);
|
|
2613
3249
|
}
|
|
3250
|
+
let parsed;
|
|
2614
3251
|
try {
|
|
2615
|
-
|
|
2616
|
-
if (parsed.version !== HQ_AUTH_FILE_VERSION) {
|
|
2617
|
-
opts.warn?.(
|
|
2618
|
-
`HQ auth file at ${file} has unsupported version ${String(parsed.version)} (expected ${String(HQ_AUTH_FILE_VERSION)}); ignoring stored policy/tokens.`
|
|
2619
|
-
);
|
|
2620
|
-
return emptyHqAuthFile();
|
|
2621
|
-
}
|
|
2622
|
-
return parsed;
|
|
3252
|
+
parsed = JSON.parse(raw);
|
|
2623
3253
|
} catch (err) {
|
|
2624
|
-
|
|
2625
|
-
|
|
3254
|
+
throw new Error(
|
|
3255
|
+
`HQ auth file at ${file} is not valid JSON: ${err.message}. Fix the file or delete it to start with an empty auth state.`
|
|
3256
|
+
);
|
|
2626
3257
|
}
|
|
3258
|
+
if (parsed.version !== HQ_AUTH_FILE_VERSION) {
|
|
3259
|
+
throw new Error(
|
|
3260
|
+
`HQ auth file at ${file} has unsupported version ${String(parsed.version)} (expected ${String(HQ_AUTH_FILE_VERSION)}). Remove or update the file to match the current schema version.`
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
return parsed;
|
|
2627
3264
|
}
|
|
2628
3265
|
async function writeHqAuthFile(dataDir, file) {
|
|
2629
3266
|
const target = hqAuthFilePath(dataDir);
|
|
@@ -2638,7 +3275,7 @@ async function writeHqAuthFile(dataDir, file) {
|
|
|
2638
3275
|
async function ensureHqFirstRunAuthFile(dataDir, opts = {}) {
|
|
2639
3276
|
const file = hqAuthFilePath(dataDir);
|
|
2640
3277
|
try {
|
|
2641
|
-
await
|
|
3278
|
+
await fs3.access(file);
|
|
2642
3279
|
return { authFile: await readHqAuthFile(dataDir, opts), created: false };
|
|
2643
3280
|
} catch (err) {
|
|
2644
3281
|
if (err.code !== "ENOENT") {
|
|
@@ -2654,6 +3291,10 @@ async function ensureHqFirstRunAuthFile(dataDir, opts = {}) {
|
|
|
2654
3291
|
browserTokens: [browserToken],
|
|
2655
3292
|
clientTokens: [clientToken]
|
|
2656
3293
|
};
|
|
3294
|
+
if (opts.password) {
|
|
3295
|
+
authFile.passwordHash = await hashHqPassword(opts.password);
|
|
3296
|
+
authFile.cookieSecret = mintHqCookieSecret();
|
|
3297
|
+
}
|
|
2657
3298
|
await writeHqAuthFile(dataDir, authFile);
|
|
2658
3299
|
return { authFile: await readHqAuthFile(dataDir, opts), created: true, browserToken, clientToken };
|
|
2659
3300
|
}
|
|
@@ -2663,6 +3304,42 @@ async function mutateHqAuthFile(dataDir, mutator, opts = {}) {
|
|
|
2663
3304
|
await writeHqAuthFile(dataDir, next);
|
|
2664
3305
|
return next;
|
|
2665
3306
|
}
|
|
3307
|
+
var HQ_PASSWORD_SALT_BYTES = 16;
|
|
3308
|
+
var HQ_PASSWORD_HASH_BYTES = 32;
|
|
3309
|
+
var HQ_PASSWORD_SCRYPT_PARAMS = { N: 16384, r: 8, p: 1 };
|
|
3310
|
+
function hashHqPassword(password) {
|
|
3311
|
+
return new Promise((resolve4, reject) => {
|
|
3312
|
+
const salt = randomBytes2(HQ_PASSWORD_SALT_BYTES);
|
|
3313
|
+
scrypt(password, salt, HQ_PASSWORD_HASH_BYTES, HQ_PASSWORD_SCRYPT_PARAMS, (err, derivedKey) => {
|
|
3314
|
+
if (err) return reject(err);
|
|
3315
|
+
const payload = `scrypt$${salt.toString("base64url")}$${derivedKey.toString("base64url")}`;
|
|
3316
|
+
resolve4(payload);
|
|
3317
|
+
});
|
|
3318
|
+
});
|
|
3319
|
+
}
|
|
3320
|
+
async function verifyHqPassword(password, hash) {
|
|
3321
|
+
const parts = hash.split("$");
|
|
3322
|
+
if (parts.length !== 3 || parts[0] !== "scrypt" || !parts[1] || !parts[2]) return false;
|
|
3323
|
+
let salt;
|
|
3324
|
+
let expected;
|
|
3325
|
+
try {
|
|
3326
|
+
salt = Buffer.from(parts[1], "base64url");
|
|
3327
|
+
expected = Buffer.from(parts[2], "base64url");
|
|
3328
|
+
} catch {
|
|
3329
|
+
return false;
|
|
3330
|
+
}
|
|
3331
|
+
if (salt.length === 0 || expected.length === 0) return false;
|
|
3332
|
+
const derived = await new Promise((resolve4, reject) => {
|
|
3333
|
+
scrypt(password, salt, expected.length, HQ_PASSWORD_SCRYPT_PARAMS, (err, key) => {
|
|
3334
|
+
if (err) reject(err);
|
|
3335
|
+
else resolve4(key);
|
|
3336
|
+
});
|
|
3337
|
+
});
|
|
3338
|
+
return timingSafeEqual(derived, expected);
|
|
3339
|
+
}
|
|
3340
|
+
function mintHqCookieSecret() {
|
|
3341
|
+
return randomBytes2(32).toString("base64url");
|
|
3342
|
+
}
|
|
2666
3343
|
function mintHqToken(label) {
|
|
2667
3344
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2668
3345
|
return {
|
|
@@ -2731,7 +3408,7 @@ function discoverLocalHqEndpoint(options = {}) {
|
|
|
2731
3408
|
}
|
|
2732
3409
|
function readFirstClientTokenFromAuthFile(dataDir) {
|
|
2733
3410
|
try {
|
|
2734
|
-
const raw =
|
|
3411
|
+
const raw = fs4.readFileSync(hqAuthFilePath(dataDir), "utf8");
|
|
2735
3412
|
const parsed = JSON.parse(raw);
|
|
2736
3413
|
return parsed.clientTokens?.find((t) => t.token.trim().length > 0)?.token;
|
|
2737
3414
|
} catch {
|
|
@@ -2833,70 +3510,16 @@ function createHqPublisherFromEnv(options) {
|
|
|
2833
3510
|
// Auto-discovery: re-read the local HQ runtime marker + client token on
|
|
2834
3511
|
// every connect attempt so late-started/restarted HQs are picked up.
|
|
2835
3512
|
...config.discover ? { resolveEndpoint: () => discoverLocalHqEndpoint({ dataDir: discoveryDataDir }) } : {},
|
|
2836
|
-
...options.discoveryPollMs !== void 0 ? { discoveryPollMs: options.discoveryPollMs } : {}
|
|
3513
|
+
...options.discoveryPollMs !== void 0 ? { discoveryPollMs: options.discoveryPollMs } : {},
|
|
3514
|
+
...options.logger !== void 0 ? { logger: options.logger } : {}
|
|
2837
3515
|
});
|
|
2838
3516
|
}
|
|
2839
3517
|
function createGlobalMailbox(options) {
|
|
2840
3518
|
return new GlobalMailbox(options.projectDir, options.events, options.hqPublisher);
|
|
2841
3519
|
}
|
|
2842
3520
|
|
|
2843
|
-
// src/hq/agent-bridge.ts
|
|
2844
|
-
function startAgentMonitorEventBridge(opts) {
|
|
2845
|
-
const { events, clientId, projectId, publish } = opts;
|
|
2846
|
-
const seq = { current: 0 };
|
|
2847
|
-
function nextSeq() {
|
|
2848
|
-
seq.current += 1;
|
|
2849
|
-
return seq.current;
|
|
2850
|
-
}
|
|
2851
|
-
function buildEnvelope(type, payload, sessionId) {
|
|
2852
|
-
return createHqEventEnvelope({
|
|
2853
|
-
id: `${Date.now().toString(36)}-${nextSeq()}`,
|
|
2854
|
-
type,
|
|
2855
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2856
|
-
clientId,
|
|
2857
|
-
projectId,
|
|
2858
|
-
seq: nextSeq(),
|
|
2859
|
-
payload,
|
|
2860
|
-
// Carry the parent session id so HQ can scope per-agent transcripts by
|
|
2861
|
-
// (sessionId, subagentId) — without it, two sessions whose leaders both
|
|
2862
|
-
// use the default id 'leader' would collide into one shared ring.
|
|
2863
|
-
...sessionId !== void 0 ? { sessionId } : {}
|
|
2864
|
-
});
|
|
2865
|
-
}
|
|
2866
|
-
const offMessage = events.on("agent.timeline.message", (payload) => {
|
|
2867
|
-
if (!publish) return;
|
|
2868
|
-
const msgPayload = {
|
|
2869
|
-
subagentId: payload.subagentId,
|
|
2870
|
-
agentName: payload.agentName,
|
|
2871
|
-
content: payload.content,
|
|
2872
|
-
kind: payload.kind,
|
|
2873
|
-
iteration: payload.iteration,
|
|
2874
|
-
ts: payload.ts
|
|
2875
|
-
};
|
|
2876
|
-
if (payload.toolName !== void 0) msgPayload.toolName = payload.toolName;
|
|
2877
|
-
if (payload.costUsd !== void 0) msgPayload.costUsd = payload.costUsd;
|
|
2878
|
-
publish(buildEnvelope("agent.message", msgPayload, payload.sessionId));
|
|
2879
|
-
});
|
|
2880
|
-
const offStatus = events.on("agent.status_changed", (payload) => {
|
|
2881
|
-
if (!publish) return;
|
|
2882
|
-
const statusPayload = {
|
|
2883
|
-
subagentId: payload.subagentId,
|
|
2884
|
-
agentName: payload.agentName,
|
|
2885
|
-
status: payload.status,
|
|
2886
|
-
ts: payload.ts
|
|
2887
|
-
};
|
|
2888
|
-
if (payload.summary !== void 0) statusPayload.summary = payload.summary;
|
|
2889
|
-
if (payload.task !== void 0) statusPayload.task = payload.task;
|
|
2890
|
-
publish(buildEnvelope("agent.status", statusPayload));
|
|
2891
|
-
});
|
|
2892
|
-
return () => {
|
|
2893
|
-
offMessage();
|
|
2894
|
-
offStatus();
|
|
2895
|
-
};
|
|
2896
|
-
}
|
|
2897
|
-
|
|
2898
3521
|
// src/hq/session-bridge.ts
|
|
2899
|
-
import * as
|
|
3522
|
+
import * as fs5 from "node:fs";
|
|
2900
3523
|
import * as fsp2 from "node:fs/promises";
|
|
2901
3524
|
|
|
2902
3525
|
// src/hq/transcript-mapper.ts
|
|
@@ -3166,7 +3789,7 @@ function startSessionTelemetryBridge(opts) {
|
|
|
3166
3789
|
function setupWatcher() {
|
|
3167
3790
|
if (disposed || watcher) return;
|
|
3168
3791
|
try {
|
|
3169
|
-
const nextWatcher =
|
|
3792
|
+
const nextWatcher = fs5.watch(sessionFile, () => {
|
|
3170
3793
|
if (watchPending || disposed) return;
|
|
3171
3794
|
watchPending = true;
|
|
3172
3795
|
setTimeout(() => {
|
|
@@ -3263,7 +3886,11 @@ function startFleetTelemetryBridge(opts) {
|
|
|
3263
3886
|
const subagents = stats.subagentStatuses.map((s) => ({
|
|
3264
3887
|
subagentId: s.subagentId,
|
|
3265
3888
|
...s.taskId ? { task: s.taskId } : {},
|
|
3266
|
-
status: normalizeSubagentStatus(s.status)
|
|
3889
|
+
status: normalizeSubagentStatus(s.status),
|
|
3890
|
+
...s.model !== void 0 ? { model: s.model } : {},
|
|
3891
|
+
...s.costUsd !== void 0 ? { costUsd: s.costUsd } : {},
|
|
3892
|
+
...s.runtimeMs !== void 0 ? { runtimeMs: s.runtimeMs } : {},
|
|
3893
|
+
...s.lastActivityAt !== void 0 ? { lastActivityAt: s.lastActivityAt } : {}
|
|
3267
3894
|
}));
|
|
3268
3895
|
return {
|
|
3269
3896
|
runId,
|
|
@@ -3271,6 +3898,7 @@ function startFleetTelemetryBridge(opts) {
|
|
|
3271
3898
|
queuedTasks: stats.pending,
|
|
3272
3899
|
completedTasks: stats.completed,
|
|
3273
3900
|
failedTasks: stats.stopped,
|
|
3901
|
+
...stats.totalCostUsd !== void 0 ? { totalCostUsd: stats.totalCostUsd } : {},
|
|
3274
3902
|
subagents
|
|
3275
3903
|
};
|
|
3276
3904
|
}
|
|
@@ -3586,6 +4214,10 @@ function startCostTelemetryBridge(opts) {
|
|
|
3586
4214
|
totalTokens: p.usage.input + p.usage.output,
|
|
3587
4215
|
costUsd: p.cost.total
|
|
3588
4216
|
};
|
|
4217
|
+
if (p.provider !== void 0) payload.provider = p.provider;
|
|
4218
|
+
if (p.model !== void 0) payload.model = p.model;
|
|
4219
|
+
if (p.usage.cacheRead !== void 0) payload.cacheRead = p.usage.cacheRead;
|
|
4220
|
+
if (p.usage.cacheWrite !== void 0) payload.cacheWrite = p.usage.cacheWrite;
|
|
3589
4221
|
const sessionId = opts.sessionId ?? p.sessionId;
|
|
3590
4222
|
publisher.publishEvent({
|
|
3591
4223
|
type: "session.usage",
|
|
@@ -3602,7 +4234,7 @@ function startCostTelemetryBridge(opts) {
|
|
|
3602
4234
|
}
|
|
3603
4235
|
|
|
3604
4236
|
// src/hq/persistence.ts
|
|
3605
|
-
import * as
|
|
4237
|
+
import * as fs6 from "node:fs/promises";
|
|
3606
4238
|
import * as path6 from "node:path";
|
|
3607
4239
|
var DEFAULT_EVENT_LOG_MAX_LINES = 5e4;
|
|
3608
4240
|
var DEFAULT_EVENT_LOG_ROTATE_KEEP = 2e4;
|
|
@@ -3634,7 +4266,7 @@ var HqEventLog = class {
|
|
|
3634
4266
|
this.counted = true;
|
|
3635
4267
|
}
|
|
3636
4268
|
const line = JSON.stringify(event) + "\n";
|
|
3637
|
-
await
|
|
4269
|
+
await fs6.appendFile(this.filePath, line, { encoding: "utf8" });
|
|
3638
4270
|
this.lineCount += 1;
|
|
3639
4271
|
if (this.lineCount >= this.maxLines) {
|
|
3640
4272
|
await this.rotate();
|
|
@@ -3643,7 +4275,7 @@ var HqEventLog = class {
|
|
|
3643
4275
|
async rotate() {
|
|
3644
4276
|
await withFileLock(this.filePath, async () => {
|
|
3645
4277
|
try {
|
|
3646
|
-
const content = await
|
|
4278
|
+
const content = await fs6.readFile(this.filePath, "utf8");
|
|
3647
4279
|
const lines = content.split("\n").filter((l) => l.length > 0);
|
|
3648
4280
|
if (lines.length <= this.rotateKeep) {
|
|
3649
4281
|
this.lineCount = lines.length;
|
|
@@ -3658,7 +4290,7 @@ var HqEventLog = class {
|
|
|
3658
4290
|
}
|
|
3659
4291
|
async countLines() {
|
|
3660
4292
|
try {
|
|
3661
|
-
const content = await
|
|
4293
|
+
const content = await fs6.readFile(this.filePath, "utf8");
|
|
3662
4294
|
return content.split("\n").filter((l) => l.length > 0).length;
|
|
3663
4295
|
} catch {
|
|
3664
4296
|
return 0;
|
|
@@ -3671,7 +4303,7 @@ var HqEventLog = class {
|
|
|
3671
4303
|
async recent(limit, typeFilter) {
|
|
3672
4304
|
let content;
|
|
3673
4305
|
try {
|
|
3674
|
-
content = await
|
|
4306
|
+
content = await fs6.readFile(this.filePath, "utf8");
|
|
3675
4307
|
} catch {
|
|
3676
4308
|
return [];
|
|
3677
4309
|
}
|
|
@@ -3713,7 +4345,7 @@ var HqSnapshotStore = class {
|
|
|
3713
4345
|
/** Read the last persisted snapshot, or `null` if none. */
|
|
3714
4346
|
async load() {
|
|
3715
4347
|
try {
|
|
3716
|
-
const content = await
|
|
4348
|
+
const content = await fs6.readFile(this.filePath, "utf8");
|
|
3717
4349
|
return JSON.parse(content);
|
|
3718
4350
|
} catch {
|
|
3719
4351
|
return null;
|
|
@@ -3747,6 +4379,21 @@ var HqTimeseriesStore = class {
|
|
|
3747
4379
|
if (signal.outputTokens !== void 0) bucket.outputTokens += signal.outputTokens;
|
|
3748
4380
|
if (signal.toolCalls !== void 0) bucket.toolCalls += signal.toolCalls;
|
|
3749
4381
|
if (signal.activeAgents !== void 0) bucket.activeAgents = signal.activeAgents;
|
|
4382
|
+
if (signal.model !== void 0 || signal.provider !== void 0) {
|
|
4383
|
+
const entry = {
|
|
4384
|
+
costUsd: signal.costUsd ?? 0,
|
|
4385
|
+
inputTokens: signal.inputTokens ?? 0,
|
|
4386
|
+
outputTokens: signal.outputTokens ?? 0,
|
|
4387
|
+
...signal.cacheRead !== void 0 ? { cacheRead: signal.cacheRead } : {},
|
|
4388
|
+
...signal.cacheWrite !== void 0 ? { cacheWrite: signal.cacheWrite } : {}
|
|
4389
|
+
};
|
|
4390
|
+
if (signal.model !== void 0) {
|
|
4391
|
+
bucket.byModel = foldBreakdown(bucket.byModel, signal.model, entry);
|
|
4392
|
+
}
|
|
4393
|
+
if (signal.provider !== void 0) {
|
|
4394
|
+
bucket.byProvider = foldBreakdown(bucket.byProvider, signal.provider, entry);
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
3750
4397
|
if (this.buckets.size > this.maxBuckets) {
|
|
3751
4398
|
const sorted = Array.from(this.buckets.keys()).sort((a, b) => a - b);
|
|
3752
4399
|
while (this.buckets.size > this.maxBuckets && sorted.length > 0) {
|
|
@@ -3771,7 +4418,7 @@ var HqTimeseriesStore = class {
|
|
|
3771
4418
|
async flushInternal(toWrite) {
|
|
3772
4419
|
const lines = toWrite.map((b) => JSON.stringify(b)).join("\n") + "\n";
|
|
3773
4420
|
await withFileLock(this.filePath, async () => {
|
|
3774
|
-
await
|
|
4421
|
+
await fs6.appendFile(this.filePath, lines, { encoding: "utf8" });
|
|
3775
4422
|
});
|
|
3776
4423
|
const sorted = Array.from(this.buckets.keys()).sort((a, b) => a - b);
|
|
3777
4424
|
while (this.buckets.size > this.maxBuckets) {
|
|
@@ -3790,7 +4437,7 @@ var HqTimeseriesStore = class {
|
|
|
3790
4437
|
async load() {
|
|
3791
4438
|
let content;
|
|
3792
4439
|
try {
|
|
3793
|
-
content = await
|
|
4440
|
+
content = await fs6.readFile(this.filePath, "utf8");
|
|
3794
4441
|
} catch {
|
|
3795
4442
|
return;
|
|
3796
4443
|
}
|
|
@@ -3811,11 +4458,66 @@ var HqTimeseriesStore = class {
|
|
|
3811
4458
|
}
|
|
3812
4459
|
}
|
|
3813
4460
|
};
|
|
4461
|
+
function foldBreakdown(map, key, entry) {
|
|
4462
|
+
const next = { ...map };
|
|
4463
|
+
const existing = next[key];
|
|
4464
|
+
if (existing === void 0) {
|
|
4465
|
+
next[key] = { ...entry };
|
|
4466
|
+
} else {
|
|
4467
|
+
next[key] = {
|
|
4468
|
+
costUsd: existing.costUsd + entry.costUsd,
|
|
4469
|
+
inputTokens: existing.inputTokens + entry.inputTokens,
|
|
4470
|
+
outputTokens: existing.outputTokens + entry.outputTokens,
|
|
4471
|
+
cacheRead: existing.cacheRead !== void 0 || entry.cacheRead !== void 0 ? (existing.cacheRead ?? 0) + (entry.cacheRead ?? 0) : void 0,
|
|
4472
|
+
cacheWrite: existing.cacheWrite !== void 0 || entry.cacheWrite !== void 0 ? (existing.cacheWrite ?? 0) + (entry.cacheWrite ?? 0) : void 0
|
|
4473
|
+
};
|
|
4474
|
+
}
|
|
4475
|
+
return next;
|
|
4476
|
+
}
|
|
4477
|
+
var HqSimpleLog = class {
|
|
4478
|
+
filePath;
|
|
4479
|
+
writeChain = Promise.resolve();
|
|
4480
|
+
constructor(dataDir, filename) {
|
|
4481
|
+
this.filePath = path6.join(dataDir, filename);
|
|
4482
|
+
}
|
|
4483
|
+
/** Append one record as a JSON line. Best-effort, never rejects. */
|
|
4484
|
+
append(record) {
|
|
4485
|
+
this.writeChain = this.writeChain.then(() => fs6.appendFile(this.filePath, `${JSON.stringify(record)}
|
|
4486
|
+
`, { encoding: "utf8" })).catch(() => {
|
|
4487
|
+
});
|
|
4488
|
+
}
|
|
4489
|
+
/** Resolves once all queued appends have settled. For tests. */
|
|
4490
|
+
async drain() {
|
|
4491
|
+
await this.writeChain.catch(() => {
|
|
4492
|
+
});
|
|
4493
|
+
}
|
|
4494
|
+
/** Read all records oldest-first. Returns `[]` if the file doesn't exist. */
|
|
4495
|
+
async readAll() {
|
|
4496
|
+
let content;
|
|
4497
|
+
try {
|
|
4498
|
+
content = await fs6.readFile(this.filePath, "utf8");
|
|
4499
|
+
} catch {
|
|
4500
|
+
return [];
|
|
4501
|
+
}
|
|
4502
|
+
const out = [];
|
|
4503
|
+
for (const line of content.split("\n")) {
|
|
4504
|
+
const trimmed = line.trim();
|
|
4505
|
+
if (!trimmed) continue;
|
|
4506
|
+
try {
|
|
4507
|
+
out.push(JSON.parse(trimmed));
|
|
4508
|
+
} catch {
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
return out;
|
|
4512
|
+
}
|
|
4513
|
+
};
|
|
3814
4514
|
function createHqPersistence(dataDir) {
|
|
3815
4515
|
return {
|
|
3816
4516
|
eventLog: new HqEventLog({ dataDir }),
|
|
3817
4517
|
snapshotStore: new HqSnapshotStore({ dataDir }),
|
|
3818
|
-
timeseries: new HqTimeseriesStore({ dataDir })
|
|
4518
|
+
timeseries: new HqTimeseriesStore({ dataDir }),
|
|
4519
|
+
commandLog: new HqSimpleLog(dataDir, "commands.jsonl"),
|
|
4520
|
+
alertLog: new HqSimpleLog(dataDir, "alerts.jsonl")
|
|
3819
4521
|
};
|
|
3820
4522
|
}
|
|
3821
4523
|
|
|
@@ -3896,18 +4598,24 @@ function validateHqCommand(queued) {
|
|
|
3896
4598
|
var HqCommandAuditLog = class {
|
|
3897
4599
|
entries = [];
|
|
3898
4600
|
max;
|
|
3899
|
-
|
|
4601
|
+
onPersist;
|
|
4602
|
+
constructor(max = 1e3, onPersist) {
|
|
3900
4603
|
this.max = max;
|
|
4604
|
+
this.onPersist = onPersist;
|
|
3901
4605
|
}
|
|
3902
4606
|
record(entry) {
|
|
3903
4607
|
this.entries.push(entry);
|
|
3904
4608
|
if (this.entries.length > this.max) {
|
|
3905
4609
|
this.entries.splice(0, this.entries.length - this.max);
|
|
3906
4610
|
}
|
|
4611
|
+
this.onPersist?.(entry);
|
|
3907
4612
|
}
|
|
3908
4613
|
update(commandId, patch) {
|
|
3909
4614
|
const entry = this.entries.find((e) => e.commandId === commandId);
|
|
3910
|
-
if (entry)
|
|
4615
|
+
if (entry) {
|
|
4616
|
+
Object.assign(entry, patch);
|
|
4617
|
+
this.onPersist?.(entry);
|
|
4618
|
+
}
|
|
3911
4619
|
}
|
|
3912
4620
|
/** Update only when the command belongs to the authenticated client. */
|
|
3913
4621
|
updateForClient(commandId, clientId, patch) {
|
|
@@ -3916,8 +4624,18 @@ var HqCommandAuditLog = class {
|
|
|
3916
4624
|
);
|
|
3917
4625
|
if (!entry) return false;
|
|
3918
4626
|
Object.assign(entry, patch);
|
|
4627
|
+
this.onPersist?.(entry);
|
|
3919
4628
|
return true;
|
|
3920
4629
|
}
|
|
4630
|
+
/** Seed the ring from a durable store on boot (no persist callback fired). */
|
|
4631
|
+
seed(entries) {
|
|
4632
|
+
for (const entry of entries) {
|
|
4633
|
+
this.entries.push(entry);
|
|
4634
|
+
}
|
|
4635
|
+
if (this.entries.length > this.max) {
|
|
4636
|
+
this.entries.splice(0, this.entries.length - this.max);
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
3921
4639
|
recent(limit = 200) {
|
|
3922
4640
|
return this.entries.slice(-limit);
|
|
3923
4641
|
}
|
|
@@ -3992,9 +4710,11 @@ var HqAlertEngine = class {
|
|
|
3992
4710
|
maxHistory;
|
|
3993
4711
|
timer = null;
|
|
3994
4712
|
onAlert;
|
|
4713
|
+
onPersist;
|
|
3995
4714
|
constructor(opts) {
|
|
3996
4715
|
this.onAlert = opts.onAlert;
|
|
3997
4716
|
this.maxHistory = opts.maxHistory ?? 500;
|
|
4717
|
+
this.onPersist = opts.onPersist;
|
|
3998
4718
|
}
|
|
3999
4719
|
/**
|
|
4000
4720
|
* Evaluate all rules against the snapshot. Emits (via the `onAlert`
|
|
@@ -4024,6 +4744,7 @@ var HqAlertEngine = class {
|
|
|
4024
4744
|
if (this.history.length > this.maxHistory) this.history.splice(0, this.history.length - this.maxHistory);
|
|
4025
4745
|
fired.push(alert);
|
|
4026
4746
|
this.onAlert(alert);
|
|
4747
|
+
this.onPersist?.(alert);
|
|
4027
4748
|
} else {
|
|
4028
4749
|
existing.lastFiredAt = now;
|
|
4029
4750
|
}
|
|
@@ -4044,6 +4765,15 @@ var HqAlertEngine = class {
|
|
|
4044
4765
|
recentAlerts(limit = 100) {
|
|
4045
4766
|
return this.history.slice(-limit);
|
|
4046
4767
|
}
|
|
4768
|
+
/** Seed the history from a durable store on boot (no alert callbacks fired). */
|
|
4769
|
+
seed(alerts) {
|
|
4770
|
+
for (const alert of alerts) {
|
|
4771
|
+
this.history.push(alert);
|
|
4772
|
+
}
|
|
4773
|
+
if (this.history.length > this.maxHistory) {
|
|
4774
|
+
this.history.splice(0, this.history.length - this.maxHistory);
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4047
4777
|
/**
|
|
4048
4778
|
* Start periodic evaluation against a snapshot getter. The timer is
|
|
4049
4779
|
* unref'd so it never keeps the process alive. Returns a disposer.
|
|
@@ -4085,6 +4815,7 @@ export {
|
|
|
4085
4815
|
HqCommandAuditLog,
|
|
4086
4816
|
HqEventLog,
|
|
4087
4817
|
HqPublisher,
|
|
4818
|
+
HqSimpleLog,
|
|
4088
4819
|
HqSnapshotStore,
|
|
4089
4820
|
HqTimeseriesStore,
|
|
4090
4821
|
buildTranscriptFromEvents,
|
|
@@ -4099,6 +4830,7 @@ export {
|
|
|
4099
4830
|
discoverLocalHqEndpoint,
|
|
4100
4831
|
emptyHqAuthFile,
|
|
4101
4832
|
ensureHqFirstRunAuthFile,
|
|
4833
|
+
hashHqPassword,
|
|
4102
4834
|
hqAuthFilePath,
|
|
4103
4835
|
hqRuntimeFilePath,
|
|
4104
4836
|
mapMailboxAgentToHqSummary,
|
|
@@ -4106,6 +4838,7 @@ export {
|
|
|
4106
4838
|
mapSessionEventToEntries,
|
|
4107
4839
|
mergeToolResults,
|
|
4108
4840
|
mintHqBrowserToken,
|
|
4841
|
+
mintHqCookieSecret,
|
|
4109
4842
|
mintHqToken,
|
|
4110
4843
|
mutateHqAuthFile,
|
|
4111
4844
|
parseHqEventPayload,
|
|
@@ -4119,7 +4852,6 @@ export {
|
|
|
4119
4852
|
resolveHqDataDir,
|
|
4120
4853
|
resolveHqRedactionPolicy,
|
|
4121
4854
|
scrubAndTruncateHqPreview,
|
|
4122
|
-
startAgentMonitorEventBridge,
|
|
4123
4855
|
startBrainTelemetryBridge,
|
|
4124
4856
|
startCostTelemetryBridge,
|
|
4125
4857
|
startFleetTelemetryBridge,
|
|
@@ -4131,6 +4863,7 @@ export {
|
|
|
4131
4863
|
toAlertMessage,
|
|
4132
4864
|
tokenHasCapability,
|
|
4133
4865
|
validateHqCommand,
|
|
4866
|
+
verifyHqPassword,
|
|
4134
4867
|
watchHqAuthFile,
|
|
4135
4868
|
writeHqAuthFile,
|
|
4136
4869
|
writeHqRuntimeFile
|