@wrongstack/core 0.287.0 → 0.291.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/agent-status-tracker.d.ts +13 -0
- package/dist/agent-status-tracker.d.ts.map +1 -1
- package/dist/boot.d.ts.map +1 -1
- package/dist/chronicle/context.d.ts +13 -0
- package/dist/chronicle/context.d.ts.map +1 -0
- package/dist/chronicle/decision-adapter.d.ts +13 -0
- package/dist/chronicle/decision-adapter.d.ts.map +1 -0
- package/dist/chronicle/domain-adapter.d.ts +13 -0
- package/dist/chronicle/domain-adapter.d.ts.map +1 -0
- package/dist/chronicle/file-observer.d.ts +20 -0
- package/dist/chronicle/file-observer.d.ts.map +1 -0
- package/dist/chronicle/health-monitor.d.ts +11 -0
- package/dist/chronicle/health-monitor.d.ts.map +1 -0
- package/dist/chronicle/identity.d.ts +18 -0
- package/dist/chronicle/identity.d.ts.map +1 -0
- package/dist/chronicle/index.d.ts +16 -0
- package/dist/chronicle/index.d.ts.map +1 -0
- package/dist/chronicle/index.js +2555 -0
- package/dist/chronicle/index.js.map +7 -0
- package/dist/chronicle/journal.d.ts +81 -0
- package/dist/chronicle/journal.d.ts.map +1 -0
- package/dist/chronicle/process-adapter.d.ts +14 -0
- package/dist/chronicle/process-adapter.d.ts.map +1 -0
- package/dist/chronicle/prompt-manifest.d.ts +34 -0
- package/dist/chronicle/prompt-manifest.d.ts.map +1 -0
- package/dist/chronicle/provider-adapter.d.ts +13 -0
- package/dist/chronicle/provider-adapter.d.ts.map +1 -0
- package/dist/chronicle/query.d.ts +108 -0
- package/dist/chronicle/query.d.ts.map +1 -0
- package/dist/chronicle/rollup-adapter.d.ts +14 -0
- package/dist/chronicle/rollup-adapter.d.ts.map +1 -0
- package/dist/chronicle/stream-adapter.d.ts +13 -0
- package/dist/chronicle/stream-adapter.d.ts.map +1 -0
- package/dist/chronicle/tool-adapter.d.ts +15 -0
- package/dist/chronicle/tool-adapter.d.ts.map +1 -0
- package/dist/chronicle/types.d.ts +82 -0
- package/dist/chronicle/types.d.ts.map +1 -0
- package/dist/coordination/agent-monitor.d.ts +40 -0
- package/dist/coordination/agent-monitor.d.ts.map +1 -1
- package/dist/coordination/agent-subagent-runner.d.ts +7 -1
- package/dist/coordination/agent-subagent-runner.d.ts.map +1 -1
- package/dist/coordination/agents/phase6-domain.d.ts.map +1 -1
- package/dist/coordination/autonomous-coordinator.d.ts +2 -0
- package/dist/coordination/autonomous-coordinator.d.ts.map +1 -1
- package/dist/coordination/brain-monitor.d.ts +1 -1
- package/dist/coordination/brain.d.ts +3 -2
- package/dist/coordination/brain.d.ts.map +1 -1
- package/dist/coordination/collab-debug.d.ts +0 -9
- package/dist/coordination/collab-debug.d.ts.map +1 -1
- package/dist/coordination/coordinator/error-classifier.d.ts.map +1 -1
- package/dist/coordination/dep-watcher.d.ts +2 -1
- package/dist/coordination/dep-watcher.d.ts.map +1 -1
- package/dist/coordination/director/director-collab.d.ts +1 -1
- package/dist/coordination/director/director-collab.d.ts.map +1 -1
- package/dist/coordination/director-tools.d.ts.map +1 -1
- package/dist/coordination/director.d.ts +10 -1
- package/dist/coordination/director.d.ts.map +1 -1
- package/dist/coordination/file-author-tracker.d.ts.map +1 -1
- package/dist/coordination/fleet-bus.d.ts.map +1 -1
- package/dist/coordination/fleet-event-validation.d.ts +0 -7
- package/dist/coordination/fleet-event-validation.d.ts.map +1 -1
- package/dist/coordination/fleet-spawn.d.ts.map +1 -1
- package/dist/coordination/fleet-supervisor.d.ts +1 -1
- package/dist/coordination/fleet-supervisor.d.ts.map +1 -1
- package/dist/coordination/fleet.d.ts +0 -44
- package/dist/coordination/fleet.d.ts.map +1 -1
- package/dist/coordination/global-mailbox.d.ts +7 -1
- package/dist/coordination/global-mailbox.d.ts.map +1 -1
- package/dist/coordination/index.d.ts +5 -3
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +2426 -299
- package/dist/coordination/index.js.map +4 -4
- package/dist/coordination/knowledge-graph.d.ts +2 -1
- package/dist/coordination/knowledge-graph.d.ts.map +1 -1
- package/dist/coordination/large-answer-store.d.ts +0 -6
- package/dist/coordination/large-answer-store.d.ts.map +1 -1
- package/dist/coordination/mail-tools.d.ts +2 -2
- package/dist/coordination/mail-tools.d.ts.map +1 -1
- package/dist/coordination/mailbox-hooks.d.ts +2 -0
- package/dist/coordination/mailbox-hooks.d.ts.map +1 -1
- package/dist/coordination/mailbox-http-router.d.ts +43 -0
- package/dist/coordination/mailbox-http-router.d.ts.map +1 -0
- package/dist/coordination/mailbox-message-codec.d.ts +35 -0
- package/dist/coordination/mailbox-message-codec.d.ts.map +1 -1
- package/dist/coordination/mailbox-tool.d.ts +1 -1
- package/dist/coordination/mailbox-tool.d.ts.map +1 -1
- package/dist/coordination/mailbox-types.d.ts +170 -10
- package/dist/coordination/mailbox-types.d.ts.map +1 -1
- package/dist/coordination/mailbox.d.ts +1 -1
- package/dist/coordination/mailbox.d.ts.map +1 -1
- package/dist/coordination/package-author-tracker.d.ts.map +1 -1
- package/dist/coordination/provider-status-tracker.d.ts +236 -0
- package/dist/coordination/provider-status-tracker.d.ts.map +1 -0
- package/dist/coordination/subagent-budget.d.ts +2 -1
- package/dist/coordination/subagent-budget.d.ts.map +1 -1
- package/dist/core/agent-loop.d.ts +8 -3
- package/dist/core/agent-loop.d.ts.map +1 -1
- package/dist/core/agent-response.d.ts +5 -4
- package/dist/core/agent-response.d.ts.map +1 -1
- package/dist/core/agent-tools.d.ts +1 -1
- package/dist/core/agent-tools.d.ts.map +1 -1
- package/dist/core/agent.d.ts +4 -2
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/context.d.ts +55 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/fallback-model.d.ts +14 -8
- package/dist/core/fallback-model.d.ts.map +1 -1
- package/dist/core/fallback-profile-manager.d.ts +118 -0
- package/dist/core/fallback-profile-manager.d.ts.map +1 -0
- package/dist/core/fleet-pulse.d.ts +2 -1
- package/dist/core/fleet-pulse.d.ts.map +1 -1
- package/dist/core/iteration-limit.d.ts +2 -1
- package/dist/core/iteration-limit.d.ts.map +1 -1
- package/dist/core/mailbox-loop.d.ts +16 -1
- package/dist/core/mailbox-loop.d.ts.map +1 -1
- package/dist/core/model-availability-calendar.d.ts +27 -0
- package/dist/core/model-availability-calendar.d.ts.map +1 -0
- package/dist/core/provider-runner.d.ts.map +1 -1
- package/dist/core/system-prompt-builder.d.ts +2 -1
- package/dist/core/system-prompt-builder.d.ts.map +1 -1
- package/dist/defaults/index.js +1920 -598
- package/dist/defaults/index.js.map +4 -4
- package/dist/execution/auto-compaction-middleware.d.ts +3 -2
- package/dist/execution/auto-compaction-middleware.d.ts.map +1 -1
- package/dist/execution/compaction-core.d.ts.map +1 -1
- package/dist/execution/council-brain.d.ts +28 -49
- package/dist/execution/council-brain.d.ts.map +1 -1
- package/dist/execution/council-orchestrator.d.ts +30 -0
- package/dist/execution/council-orchestrator.d.ts.map +1 -1
- package/dist/execution/enhance-recovery.d.ts +8 -0
- package/dist/execution/enhance-recovery.d.ts.map +1 -1
- package/dist/execution/error-handler.d.ts +2 -2
- package/dist/execution/error-handler.d.ts.map +1 -1
- package/dist/execution/eternal-autonomy.d.ts.map +1 -1
- package/dist/execution/index.js +2600 -1903
- package/dist/execution/index.js.map +4 -4
- package/dist/execution/intelligent-compactor.d.ts +0 -16
- package/dist/execution/intelligent-compactor.d.ts.map +1 -1
- package/dist/execution/model-runtime.d.ts.map +1 -1
- package/dist/execution/one-shot-llm.d.ts +9 -1
- package/dist/execution/one-shot-llm.d.ts.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +36 -0
- package/dist/execution/prompt-enhancer.d.ts.map +1 -1
- package/dist/execution/prompt-enhancer.js +158 -10
- package/dist/execution/prompt-enhancer.js.map +2 -2
- package/dist/execution/retry-policy.d.ts +0 -13
- package/dist/execution/retry-policy.d.ts.map +1 -1
- package/dist/execution/skill-loader.d.ts.map +1 -1
- package/dist/execution/tool-executor.d.ts.map +1 -1
- package/dist/goal/checkpoint.d.ts.map +1 -0
- package/dist/{autophase/auto-phase-planner.d.ts → goal/goal-planner.d.ts} +8 -8
- package/dist/goal/goal-planner.d.ts.map +1 -0
- package/dist/{autophase/auto-phase-runner.d.ts → goal/goal-runner.d.ts} +10 -10
- package/dist/goal/goal-runner.d.ts.map +1 -0
- package/dist/{autophase → goal}/index.d.ts +3 -3
- package/dist/goal/index.d.ts.map +1 -0
- package/dist/goal/phase-graph-builder.d.ts.map +1 -0
- package/dist/{autophase → goal}/phase-orchestrator.d.ts +2 -2
- package/dist/goal/phase-orchestrator.d.ts.map +1 -0
- package/dist/goal/phase-store.d.ts.map +1 -0
- package/dist/{autophase → goal}/types.d.ts +2 -2
- package/dist/goal/types.d.ts.map +1 -0
- package/dist/hq/auth-store.d.ts.map +1 -1
- package/dist/hq/commands.d.ts +2 -1
- package/dist/hq/commands.d.ts.map +1 -1
- package/dist/hq/exposure.d.ts +66 -0
- package/dist/hq/exposure.d.ts.map +1 -0
- package/dist/hq/index.d.ts +1 -0
- package/dist/hq/index.d.ts.map +1 -1
- package/dist/hq/index.js +190 -21
- package/dist/hq/index.js.map +4 -4
- package/dist/hq/mailbox-mapper.d.ts +13 -1
- package/dist/hq/mailbox-mapper.d.ts.map +1 -1
- package/dist/hq/protocol/browser.d.ts +9 -1
- package/dist/hq/protocol/browser.d.ts.map +1 -1
- package/dist/hq/protocol/client.d.ts +2 -2
- package/dist/hq/protocol/client.d.ts.map +1 -1
- package/dist/hq/protocol/core.d.ts.map +1 -1
- package/dist/hq/protocol/mailbox.d.ts +6 -0
- package/dist/hq/protocol/mailbox.d.ts.map +1 -1
- package/dist/hq/protocol/session.d.ts +4 -0
- package/dist/hq/protocol/session.d.ts.map +1 -1
- package/dist/hq/publisher.d.ts.map +1 -1
- package/dist/index.d.ts +34 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30287 -22214
- package/dist/index.js.map +4 -4
- package/dist/infrastructure/index.js +60 -5
- package/dist/infrastructure/index.js.map +2 -2
- package/dist/kernel/events/agent-events.d.ts +33 -1
- package/dist/kernel/events/agent-events.d.ts.map +1 -1
- package/dist/kernel/events/file-events.d.ts +65 -0
- package/dist/kernel/events/file-events.d.ts.map +1 -0
- package/dist/kernel/events/memory-events.d.ts +36 -0
- package/dist/kernel/events/memory-events.d.ts.map +1 -1
- package/dist/kernel/events/network-events.d.ts +60 -0
- package/dist/kernel/events/network-events.d.ts.map +1 -0
- package/dist/kernel/events/process-events.d.ts +44 -0
- package/dist/kernel/events/process-events.d.ts.map +1 -0
- package/dist/kernel/events/provider-events.d.ts +89 -0
- package/dist/kernel/events/provider-events.d.ts.map +1 -1
- package/dist/kernel/events/tool-events.d.ts +10 -0
- package/dist/kernel/events/tool-events.d.ts.map +1 -1
- package/dist/kernel/events/worktree-events.d.ts +1 -1
- package/dist/kernel/events.d.ts +14 -1
- package/dist/kernel/events.d.ts.map +1 -1
- package/dist/kernel/index.js +2 -1
- package/dist/kernel/index.js.map +2 -2
- package/dist/kernel/tokens.d.ts +3 -1
- package/dist/kernel/tokens.d.ts.map +1 -1
- package/dist/mailbox-attach.d.ts.map +1 -1
- package/dist/models/index.js.map +2 -2
- package/dist/observability/index.d.ts +3 -0
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +278 -0
- package/dist/observability/index.js.map +4 -4
- package/dist/observability/network-telemetry.d.ts +15 -0
- package/dist/observability/network-telemetry.d.ts.map +1 -0
- package/dist/observability/process-telemetry.d.ts +18 -0
- package/dist/observability/process-telemetry.d.ts.map +1 -0
- package/dist/observability/redact-command.d.ts +20 -0
- package/dist/observability/redact-command.d.ts.map +1 -0
- package/dist/plugins/auto-review-plugin.d.ts +56 -3
- package/dist/plugins/auto-review-plugin.d.ts.map +1 -1
- package/dist/plugins/chimera-plugin.d.ts +164 -5
- package/dist/plugins/chimera-plugin.d.ts.map +1 -1
- package/dist/plugins/review-context-builder.d.ts +52 -0
- package/dist/plugins/review-context-builder.d.ts.map +1 -0
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +186 -9
- package/dist/security/index.js.map +4 -4
- package/dist/security/kanban-boundary.d.ts +10 -0
- package/dist/security/kanban-boundary.d.ts.map +1 -0
- package/dist/security/permission-policy.d.ts +1 -0
- package/dist/security/permission-policy.d.ts.map +1 -1
- package/dist/session-registry.d.ts +80 -0
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/skills/index.js.map +2 -2
- package/dist/storage/config-loader.d.ts +7 -0
- package/dist/storage/config-loader.d.ts.map +1 -1
- package/dist/storage/file-session-writer.d.ts +20 -7
- package/dist/storage/file-session-writer.d.ts.map +1 -1
- package/dist/storage/goal-coordination.d.ts +40 -0
- package/dist/storage/goal-coordination.d.ts.map +1 -0
- package/dist/storage/goal-store.d.ts +9 -5
- package/dist/storage/goal-store.d.ts.map +1 -1
- package/dist/storage/index.d.ts +4 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +1066 -226
- package/dist/storage/index.js.map +4 -4
- package/dist/storage/memory-consolidator.d.ts +49 -4
- package/dist/storage/memory-consolidator.d.ts.map +1 -1
- package/dist/storage/memory-graph-backend.d.ts +11 -0
- package/dist/storage/memory-graph-backend.d.ts.map +1 -1
- package/dist/storage/provider-config-watcher.d.ts +19 -0
- package/dist/storage/provider-config-watcher.d.ts.map +1 -1
- package/dist/storage/session-event-bridge.d.ts +2 -2
- package/dist/storage/session-event-bridge.d.ts.map +1 -1
- package/dist/storage/session-resume-validation.d.ts +10 -0
- package/dist/storage/session-resume-validation.d.ts.map +1 -1
- package/dist/storage/session-rewind-apply.d.ts +53 -0
- package/dist/storage/session-rewind-apply.d.ts.map +1 -0
- package/dist/storage/session-rewinder.d.ts.map +1 -1
- package/dist/storage/session-store.d.ts.map +1 -1
- package/dist/tasking/index.js.map +1 -1
- package/dist/tasking/task-store.d.ts +1 -1
- package/dist/tools/council-tool.d.ts +3 -0
- package/dist/tools/council-tool.d.ts.map +1 -1
- package/dist/tools/fallback-manage-tools.d.ts +82 -0
- package/dist/tools/fallback-manage-tools.d.ts.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3423 -192
- package/dist/tools/index.js.map +4 -4
- package/dist/tools/one-shot-llm-tool.d.ts +2 -0
- package/dist/tools/one-shot-llm-tool.d.ts.map +1 -1
- package/dist/types/config.d.ts +76 -7
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/file-event-record.d.ts +68 -0
- package/dist/types/file-event-record.d.ts.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +13 -8
- package/dist/types/index.js.map +3 -3
- package/dist/types/memory.d.ts +12 -0
- package/dist/types/memory.d.ts.map +1 -1
- package/dist/types/multi-agent.d.ts +12 -1
- package/dist/types/multi-agent.d.ts.map +1 -1
- package/dist/types/one-shot-llm.d.ts +16 -7
- package/dist/types/one-shot-llm.d.ts.map +1 -1
- package/dist/types/provider.d.ts +1 -1
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/session.d.ts +68 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/tool-executor.d.ts +2 -0
- package/dist/types/tool-executor.d.ts.map +1 -1
- package/dist/types/tool.d.ts +5 -0
- package/dist/types/tool.d.ts.map +1 -1
- package/dist/utils/atomic-write.d.ts.map +1 -1
- package/dist/utils/index.d.ts +10 -10
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +497 -254
- package/dist/utils/index.js.map +4 -4
- package/dist/utils/message-invariants.d.ts +9 -0
- package/dist/utils/message-invariants.d.ts.map +1 -1
- package/dist/utils/safe-json.d.ts +1 -1
- package/dist/utils/safe-json.d.ts.map +1 -1
- package/dist/utils/term.d.ts +279 -0
- package/dist/utils/term.d.ts.map +1 -1
- package/dist/utils/tool-wire-compact.d.ts +9 -0
- package/dist/utils/tool-wire-compact.d.ts.map +1 -1
- package/dist/utils/wstack-paths.d.ts +12 -2
- package/dist/utils/wstack-paths.d.ts.map +1 -1
- package/dist/worktree/worktree-manager.d.ts +1 -1
- package/dist/worktree/worktree-manager.d.ts.map +1 -1
- package/instructions/agents/ios.md +113 -0
- package/instructions/agents/search.md +1 -1
- package/instructions/llm/chimera-review.md +66 -9
- package/instructions/llm/memory-consolidator.md +4 -12
- package/instructions/llm/prompt-enhancer.md +3 -1
- package/instructions/sections/tool/common-patterns.md +3 -3
- package/instructions/system.md +40 -5
- package/package.json +7 -3
- package/skills/auto-review/SKILL.md +176 -0
- package/skills/mnemosyne/SKILL.md +156 -0
- package/skills/mnemosyne/instructions/agent-prompt.md +200 -0
- package/dist/autophase/auto-phase-planner.d.ts.map +0 -1
- package/dist/autophase/auto-phase-runner.d.ts.map +0 -1
- package/dist/autophase/checkpoint.d.ts.map +0 -1
- package/dist/autophase/index.d.ts.map +0 -1
- package/dist/autophase/phase-graph-builder.d.ts.map +0 -1
- package/dist/autophase/phase-orchestrator.d.ts.map +0 -1
- package/dist/autophase/phase-store.d.ts.map +0 -1
- package/dist/autophase/types.d.ts.map +0 -1
- /package/dist/{autophase → goal}/checkpoint.d.ts +0 -0
- /package/dist/{autophase → goal}/phase-graph-builder.d.ts +0 -0
- /package/dist/{autophase → goal}/phase-store.d.ts +0 -0
- /package/instructions/{autophase → goal}/phase-planner.md +0 -0
|
@@ -0,0 +1,2555 @@
|
|
|
1
|
+
// src/chronicle/context.ts
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
function createChronicleContext(scope, traceId = randomUUID()) {
|
|
4
|
+
return {
|
|
5
|
+
scope: { ...scope },
|
|
6
|
+
correlation: { traceId, spanId: randomUUID() }
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function childChronicleContext(parent, overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
scope: { ...parent.scope, ...overrides.scope },
|
|
12
|
+
correlation: {
|
|
13
|
+
...parent.correlation,
|
|
14
|
+
...overrides.correlation,
|
|
15
|
+
traceId: parent.correlation.traceId,
|
|
16
|
+
parentSpanId: parent.correlation.spanId,
|
|
17
|
+
spanId: overrides.correlation?.spanId ?? randomUUID()
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/chronicle/identity.ts
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
import * as os from "node:os";
|
|
25
|
+
import * as path from "node:path";
|
|
26
|
+
function resolveChronicleRuntimeLocation(input) {
|
|
27
|
+
const day = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
28
|
+
return {
|
|
29
|
+
installationId: stableId("installation", path.resolve(input.globalRoot)),
|
|
30
|
+
machineId: stableId("machine", `${os.hostname()}\0${os.platform()}\0${os.arch()}`),
|
|
31
|
+
projectId: input.projectId,
|
|
32
|
+
journalPath: path.join(input.projectDir, "chronicle", `${day}.events.jsonl`)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function stableId(prefix, value) {
|
|
36
|
+
return `${prefix}_${createHash("sha256").update(value).digest("hex").slice(0, 24)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/chronicle/file-observer.ts
|
|
40
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
41
|
+
import * as fs from "node:fs";
|
|
42
|
+
import * as fsp from "node:fs/promises";
|
|
43
|
+
import * as path2 from "node:path";
|
|
44
|
+
var DEFAULT_EXCLUDED = [".git", ".wrongstack", "node_modules", "dist", "coverage", ".temp_files"];
|
|
45
|
+
async function startChronicleFileObserver(options) {
|
|
46
|
+
const root = path2.resolve(options.projectRoot);
|
|
47
|
+
const excluded = new Set(options.excludedDirectories ?? DEFAULT_EXCLUDED);
|
|
48
|
+
const debounceMs = options.debounceMs ?? 120;
|
|
49
|
+
const maxHashBytes = options.maxHashBytes ?? 8 * 1024 * 1024;
|
|
50
|
+
const known = await scanProject(root, excluded, maxHashBytes, options.onError);
|
|
51
|
+
const recentToolMutations = /* @__PURE__ */ new Map();
|
|
52
|
+
const offToolProgress = options.events?.on("tool.progress", (event) => {
|
|
53
|
+
if (event.event.type !== "file_changed" || !event.event.path) return;
|
|
54
|
+
const absolute = path2.isAbsolute(event.event.path) ? path2.normalize(event.event.path) : path2.resolve(root, event.event.path);
|
|
55
|
+
const relative2 = normalizeRelative(path2.relative(root, absolute));
|
|
56
|
+
if (relative2.startsWith("../") || isExcluded(relative2, excluded)) return;
|
|
57
|
+
recentToolMutations.set(relative2, {
|
|
58
|
+
at: Date.now(),
|
|
59
|
+
toolUseId: event.id,
|
|
60
|
+
toolName: event.name,
|
|
61
|
+
agentId: event.agentId
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
const pending = /* @__PURE__ */ new Set();
|
|
65
|
+
let timer;
|
|
66
|
+
let closed = false;
|
|
67
|
+
let flushTail = Promise.resolve();
|
|
68
|
+
const schedule = (filename) => {
|
|
69
|
+
if (closed) return;
|
|
70
|
+
if (filename === null) {
|
|
71
|
+
pending.add("*");
|
|
72
|
+
} else {
|
|
73
|
+
const relative2 = normalizeRelative(String(filename));
|
|
74
|
+
if (!relative2 || isExcluded(relative2, excluded)) return;
|
|
75
|
+
pending.add(relative2);
|
|
76
|
+
}
|
|
77
|
+
if (timer) clearTimeout(timer);
|
|
78
|
+
timer = setTimeout(() => {
|
|
79
|
+
timer = void 0;
|
|
80
|
+
const paths = [...pending];
|
|
81
|
+
pending.clear();
|
|
82
|
+
flushTail = flushTail.then(() => reconcile(paths)).catch((error) => options.onError?.(error));
|
|
83
|
+
}, debounceMs);
|
|
84
|
+
};
|
|
85
|
+
const reconcile = async (changedPaths) => {
|
|
86
|
+
const candidates = changedPaths.includes("*") ? unionKeys(known, await scanProject(root, excluded, maxHashBytes, options.onError)) : changedPaths;
|
|
87
|
+
const changes = [];
|
|
88
|
+
for (const relative2 of candidates) {
|
|
89
|
+
const before = known.get(relative2);
|
|
90
|
+
const after = await fingerprint(path2.join(root, relative2), maxHashBytes);
|
|
91
|
+
if (sameFingerprint(before, after)) continue;
|
|
92
|
+
changes.push({ relative: relative2, before, after });
|
|
93
|
+
}
|
|
94
|
+
const deleted = changes.filter((change) => change.before && !change.after);
|
|
95
|
+
const created = changes.filter((change) => !change.before && change.after);
|
|
96
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
97
|
+
for (const from of deleted) {
|
|
98
|
+
const match = created.find(
|
|
99
|
+
(to) => !consumed.has(to.relative) && from.before?.hash !== void 0 && from.before.hash === to.after?.hash
|
|
100
|
+
);
|
|
101
|
+
if (!match) continue;
|
|
102
|
+
consumed.add(from.relative);
|
|
103
|
+
consumed.add(match.relative);
|
|
104
|
+
known.delete(from.relative);
|
|
105
|
+
known.set(match.relative, match.after);
|
|
106
|
+
await recordMutation(options, "file.external.renamed", match.relative, match.after, {
|
|
107
|
+
operation: "rename",
|
|
108
|
+
previousPath: from.relative,
|
|
109
|
+
previousResourceId: resourceId(from.relative),
|
|
110
|
+
actor: "external"
|
|
111
|
+
}, mutationAttribution(match.relative, recentToolMutations));
|
|
112
|
+
}
|
|
113
|
+
for (const change of changes) {
|
|
114
|
+
if (consumed.has(change.relative)) continue;
|
|
115
|
+
if (!change.after) {
|
|
116
|
+
known.delete(change.relative);
|
|
117
|
+
await recordMutation(options, "file.external.deleted", change.relative, change.before, {
|
|
118
|
+
operation: "delete",
|
|
119
|
+
actor: "external",
|
|
120
|
+
previousHash: change.before?.hash,
|
|
121
|
+
previousSize: change.before?.size
|
|
122
|
+
}, mutationAttribution(change.relative, recentToolMutations));
|
|
123
|
+
} else if (!change.before) {
|
|
124
|
+
known.set(change.relative, change.after);
|
|
125
|
+
await recordMutation(options, "file.external.created", change.relative, change.after, {
|
|
126
|
+
operation: "write",
|
|
127
|
+
actor: "external"
|
|
128
|
+
}, mutationAttribution(change.relative, recentToolMutations));
|
|
129
|
+
} else {
|
|
130
|
+
known.set(change.relative, change.after);
|
|
131
|
+
await recordMutation(options, "file.external.modified", change.relative, change.after, {
|
|
132
|
+
operation: "edit",
|
|
133
|
+
actor: "external",
|
|
134
|
+
previousHash: change.before.hash,
|
|
135
|
+
previousSize: change.before.size
|
|
136
|
+
}, mutationAttribution(change.relative, recentToolMutations));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
let watcher;
|
|
141
|
+
try {
|
|
142
|
+
watcher = fs.watch(root, { recursive: true, persistent: false }, (_eventType, filename) => schedule(filename));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
options.onError?.(error);
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
watcher.on("error", (error) => options.onError?.(error));
|
|
148
|
+
return {
|
|
149
|
+
get watchedFiles() {
|
|
150
|
+
return known.size;
|
|
151
|
+
},
|
|
152
|
+
async close() {
|
|
153
|
+
if (closed) return;
|
|
154
|
+
closed = true;
|
|
155
|
+
offToolProgress?.();
|
|
156
|
+
watcher.close();
|
|
157
|
+
if (timer) {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
timer = void 0;
|
|
160
|
+
const paths = [...pending];
|
|
161
|
+
pending.clear();
|
|
162
|
+
if (paths.length > 0) flushTail = flushTail.then(() => reconcile(paths));
|
|
163
|
+
}
|
|
164
|
+
await flushTail;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async function recordMutation(options, eventType, relativePath, state, attributes, attribution) {
|
|
169
|
+
const context = typeof options.context === "function" ? options.context() : options.context;
|
|
170
|
+
const operation = attributes["operation"];
|
|
171
|
+
options.events?.emit("file.activity", {
|
|
172
|
+
filePath: path2.join(options.projectRoot, relativePath),
|
|
173
|
+
operation,
|
|
174
|
+
phase: "changed",
|
|
175
|
+
source: attribution ? "tool" : "external",
|
|
176
|
+
at: Date.now(),
|
|
177
|
+
sessionId: context.scope.sessionId,
|
|
178
|
+
traceId: context.correlation.traceId,
|
|
179
|
+
agentId: attribution?.agentId ?? context.scope.agentId,
|
|
180
|
+
...attribution ? { toolUseId: attribution.toolUseId, toolName: attribution.toolName } : {}
|
|
181
|
+
});
|
|
182
|
+
const input = {
|
|
183
|
+
eventType: attribution ? eventType.replace(".external.", ".tool.") : eventType,
|
|
184
|
+
scope: context.scope,
|
|
185
|
+
correlation: {
|
|
186
|
+
...context.correlation,
|
|
187
|
+
...attribution ? { toolCallId: attribution.toolUseId } : {}
|
|
188
|
+
},
|
|
189
|
+
outcome: "success",
|
|
190
|
+
resource: {
|
|
191
|
+
kind: "file",
|
|
192
|
+
id: resourceId(relativePath),
|
|
193
|
+
path: normalizeRelative(relativePath),
|
|
194
|
+
...state?.hash ? { contentHashAfter: state.hash } : {}
|
|
195
|
+
},
|
|
196
|
+
attributes: {
|
|
197
|
+
...attributes,
|
|
198
|
+
actor: attribution ? "agent" : attributes["actor"],
|
|
199
|
+
source: attribution ? "tool" : "external",
|
|
200
|
+
toolName: attribution?.toolName,
|
|
201
|
+
size: state?.size,
|
|
202
|
+
mtimeMs: state?.mtimeMs,
|
|
203
|
+
observedBy: "fs.watch"
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
await options.journal.append(input);
|
|
207
|
+
}
|
|
208
|
+
function mutationAttribution(relativePath, recent) {
|
|
209
|
+
const value = recent.get(relativePath);
|
|
210
|
+
if (!value) return void 0;
|
|
211
|
+
recent.delete(relativePath);
|
|
212
|
+
return Date.now() - value.at <= 2e3 ? value : void 0;
|
|
213
|
+
}
|
|
214
|
+
async function scanProject(root, excluded, maxHashBytes, onError) {
|
|
215
|
+
const result = /* @__PURE__ */ new Map();
|
|
216
|
+
const dirs = [""];
|
|
217
|
+
while (dirs.length > 0) {
|
|
218
|
+
const relativeDir = dirs.pop();
|
|
219
|
+
try {
|
|
220
|
+
const entries = await fsp.readdir(path2.join(root, relativeDir), { withFileTypes: true });
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
const relative2 = normalizeRelative(path2.join(relativeDir, entry.name));
|
|
223
|
+
if (entry.isDirectory()) {
|
|
224
|
+
if (!excluded.has(entry.name)) dirs.push(relative2);
|
|
225
|
+
} else if (entry.isFile()) {
|
|
226
|
+
const value = await fingerprint(path2.join(root, relative2), maxHashBytes);
|
|
227
|
+
if (value) result.set(relative2, value);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
onError?.(error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
async function fingerprint(filePath, maxHashBytes) {
|
|
237
|
+
try {
|
|
238
|
+
const stat5 = await fsp.stat(filePath);
|
|
239
|
+
if (!stat5.isFile()) return void 0;
|
|
240
|
+
const base = { size: stat5.size, mtimeMs: stat5.mtimeMs };
|
|
241
|
+
if (stat5.size <= maxHashBytes) {
|
|
242
|
+
base.hash = createHash2("sha256").update(await fsp.readFile(filePath)).digest("hex");
|
|
243
|
+
}
|
|
244
|
+
return base;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return void 0;
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function sameFingerprint(a, b) {
|
|
251
|
+
if (!a || !b) return a === b;
|
|
252
|
+
if (a.hash !== void 0 && b.hash !== void 0) return a.hash === b.hash;
|
|
253
|
+
return a.size === b.size && a.mtimeMs === b.mtimeMs;
|
|
254
|
+
}
|
|
255
|
+
function isExcluded(relative2, excluded) {
|
|
256
|
+
return normalizeRelative(relative2).split("/").some((segment) => excluded.has(segment));
|
|
257
|
+
}
|
|
258
|
+
function normalizeRelative(value) {
|
|
259
|
+
return value.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
260
|
+
}
|
|
261
|
+
function resourceId(relativePath) {
|
|
262
|
+
return `file_${createHash2("sha256").update(normalizeRelative(relativePath)).digest("hex").slice(0, 24)}`;
|
|
263
|
+
}
|
|
264
|
+
function unionKeys(a, b) {
|
|
265
|
+
return [.../* @__PURE__ */ new Set([...a.keys(), ...b.keys()])];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/chronicle/journal.ts
|
|
269
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
|
|
270
|
+
import * as fs3 from "node:fs/promises";
|
|
271
|
+
import * as path4 from "node:path";
|
|
272
|
+
|
|
273
|
+
// src/utils/atomic-write.ts
|
|
274
|
+
import { randomBytes } from "node:crypto";
|
|
275
|
+
import * as fs2 from "node:fs/promises";
|
|
276
|
+
import { watch as watchDir } from "node:fs";
|
|
277
|
+
import * as path3 from "node:path";
|
|
278
|
+
|
|
279
|
+
// src/types/errors.ts
|
|
280
|
+
var ERROR_CODES = {
|
|
281
|
+
// Provider
|
|
282
|
+
PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
|
|
283
|
+
PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
|
|
284
|
+
PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
|
|
285
|
+
PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
|
|
286
|
+
PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
|
|
287
|
+
PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
|
|
288
|
+
PROVIDER_CONTEXT_OVERFLOW: "PROVIDER_CONTEXT_OVERFLOW",
|
|
289
|
+
// Tool
|
|
290
|
+
TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
|
|
291
|
+
TOOL_PERMISSION_DENIED: "TOOL_PERMISSION_DENIED",
|
|
292
|
+
TOOL_EXECUTION_FAILED: "TOOL_EXECUTION_FAILED",
|
|
293
|
+
TOOL_TIMEOUT: "TOOL_TIMEOUT",
|
|
294
|
+
TOOL_INPUT_INVALID: "TOOL_INPUT_INVALID",
|
|
295
|
+
// Config
|
|
296
|
+
CONFIG_INVALID: "CONFIG_INVALID",
|
|
297
|
+
CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
|
|
298
|
+
CONFIG_PARSE_FAILED: "CONFIG_PARSE_FAILED",
|
|
299
|
+
CONFIG_MIGRATION_NEEDED: "CONFIG_MIGRATION_NEEDED",
|
|
300
|
+
// Plugin
|
|
301
|
+
PLUGIN_LOAD_FAILED: "PLUGIN_LOAD_FAILED",
|
|
302
|
+
PLUGIN_API_MISMATCH: "PLUGIN_API_MISMATCH",
|
|
303
|
+
PLUGIN_MISSING_DEPENDENCY: "PLUGIN_MISSING_DEPENDENCY",
|
|
304
|
+
// Agent
|
|
305
|
+
AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
|
|
306
|
+
AGENT_CONTEXT_OVERFLOW: "AGENT_CONTEXT_OVERFLOW",
|
|
307
|
+
AGENT_ABORTED: "AGENT_ABORTED",
|
|
308
|
+
AGENT_RUN_FAILED: "AGENT_RUN_FAILED",
|
|
309
|
+
// Session
|
|
310
|
+
SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
|
|
311
|
+
SESSION_CORRUPTED: "SESSION_CORRUPTED",
|
|
312
|
+
SESSION_WRITE_FAILED: "SESSION_WRITE_FAILED",
|
|
313
|
+
// Container / Registry
|
|
314
|
+
CONTAINER_TOKEN_ALREADY_BOUND: "CONTAINER_TOKEN_ALREADY_BOUND",
|
|
315
|
+
CONTAINER_TOKEN_NOT_BOUND: "CONTAINER_TOKEN_NOT_BOUND",
|
|
316
|
+
CONTAINER_CIRCULAR_DEPENDENCY: "CONTAINER_CIRCULAR_DEPENDENCY",
|
|
317
|
+
REGISTRY_DUPLICATE: "REGISTRY_DUPLICATE",
|
|
318
|
+
REGISTRY_NOT_FOUND: "REGISTRY_NOT_FOUND",
|
|
319
|
+
REGISTRY_INVALID: "REGISTRY_INVALID",
|
|
320
|
+
// File system
|
|
321
|
+
FS_READ_FAILED: "FS_READ_FAILED",
|
|
322
|
+
FS_WRITE_FAILED: "FS_WRITE_FAILED",
|
|
323
|
+
FS_MKDIR_FAILED: "FS_MKDIR_FAILED",
|
|
324
|
+
FS_DELETE_FAILED: "FS_DELETE_FAILED",
|
|
325
|
+
FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED",
|
|
326
|
+
// SDD (Spec-Driven Development)
|
|
327
|
+
SDD_VALIDATION_FAILED: "SDD_VALIDATION_FAILED",
|
|
328
|
+
SDD_PARSE_FAILED: "SDD_PARSE_FAILED",
|
|
329
|
+
SDD_INVALID_STATE: "SDD_INVALID_STATE",
|
|
330
|
+
SDD_NOT_READY: "SDD_NOT_READY",
|
|
331
|
+
// General
|
|
332
|
+
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
333
|
+
PARSE_FAILED: "PARSE_FAILED",
|
|
334
|
+
UNKNOWN: "UNKNOWN"
|
|
335
|
+
};
|
|
336
|
+
var WrongStackError = class extends Error {
|
|
337
|
+
code;
|
|
338
|
+
subsystem;
|
|
339
|
+
severity;
|
|
340
|
+
recoverable;
|
|
341
|
+
context;
|
|
342
|
+
constructor(opts) {
|
|
343
|
+
super(opts.message, { cause: opts.cause });
|
|
344
|
+
this.name = "WrongStackError";
|
|
345
|
+
this.code = opts.code;
|
|
346
|
+
this.subsystem = opts.subsystem;
|
|
347
|
+
this.severity = opts.severity ?? "error";
|
|
348
|
+
this.recoverable = opts.recoverable ?? false;
|
|
349
|
+
this.context = opts.context;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Render a one-line user-facing description.
|
|
353
|
+
* Subclasses should override for domain-specific formatting.
|
|
354
|
+
*/
|
|
355
|
+
describe() {
|
|
356
|
+
const ctx = this.context ? ` ${formatContext(this.context)}` : "";
|
|
357
|
+
return `${this.code}: ${this.message}${ctx}`;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
function formatContext(ctx) {
|
|
361
|
+
const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
|
|
362
|
+
return parts.length > 0 ? `[${parts.join(" ")}]` : "";
|
|
363
|
+
}
|
|
364
|
+
var FsError = class extends WrongStackError {
|
|
365
|
+
path;
|
|
366
|
+
constructor(opts) {
|
|
367
|
+
super({
|
|
368
|
+
message: opts.message,
|
|
369
|
+
code: opts.code,
|
|
370
|
+
subsystem: "fs",
|
|
371
|
+
severity: "error",
|
|
372
|
+
recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
|
|
373
|
+
context: { path: opts.path, ...opts.context },
|
|
374
|
+
cause: opts.cause
|
|
375
|
+
});
|
|
376
|
+
this.name = "FsError";
|
|
377
|
+
this.path = opts.path;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/utils/atomic-write.ts
|
|
382
|
+
async function atomicWrite(targetPath, content, opts = {}) {
|
|
383
|
+
const dir = path3.dirname(targetPath);
|
|
384
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
385
|
+
const tmp = path3.join(dir, `.${path3.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
386
|
+
try {
|
|
387
|
+
if (typeof content === "string") {
|
|
388
|
+
await fs2.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
389
|
+
} else {
|
|
390
|
+
await fs2.writeFile(tmp, content, { flag: "wx" });
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
const fh = await fs2.open(tmp, "r+");
|
|
394
|
+
try {
|
|
395
|
+
await fh.sync();
|
|
396
|
+
} finally {
|
|
397
|
+
await fh.close();
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
let mode;
|
|
402
|
+
try {
|
|
403
|
+
const stat5 = await fs2.stat(targetPath);
|
|
404
|
+
mode = stat5.mode & 511;
|
|
405
|
+
} catch {
|
|
406
|
+
mode = opts.mode;
|
|
407
|
+
}
|
|
408
|
+
if (mode !== void 0) {
|
|
409
|
+
await fs2.chmod(tmp, mode);
|
|
410
|
+
}
|
|
411
|
+
await renameWithRetry(tmp, targetPath);
|
|
412
|
+
if (mode !== void 0 && process.platform === "win32") {
|
|
413
|
+
try {
|
|
414
|
+
await fs2.chmod(targetPath, mode);
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} catch (err) {
|
|
419
|
+
try {
|
|
420
|
+
await fs2.unlink(tmp);
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
throw err;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async function ensureDir(dir) {
|
|
427
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
428
|
+
}
|
|
429
|
+
async function withFileLock(targetPath, fn, opts = {}) {
|
|
430
|
+
const dir = path3.dirname(targetPath);
|
|
431
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
432
|
+
const lockPath = path3.join(dir, `.${path3.basename(targetPath)}.lock`);
|
|
433
|
+
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
434
|
+
const staleMs = opts.staleMs ?? 3e4;
|
|
435
|
+
const started = Date.now();
|
|
436
|
+
let handle;
|
|
437
|
+
for (; ; ) {
|
|
438
|
+
try {
|
|
439
|
+
handle = await fs2.open(lockPath, "wx");
|
|
440
|
+
await handle.writeFile(`${process.pid}:${Date.now()}`);
|
|
441
|
+
break;
|
|
442
|
+
} catch (err) {
|
|
443
|
+
if (handle) {
|
|
444
|
+
await handle.close().catch(() => {
|
|
445
|
+
});
|
|
446
|
+
await fs2.unlink(lockPath).catch(() => {
|
|
447
|
+
});
|
|
448
|
+
handle = void 0;
|
|
449
|
+
}
|
|
450
|
+
const code = err.code;
|
|
451
|
+
if (code === "ENOENT") {
|
|
452
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (code !== "EEXIST" && code !== "EPERM") throw err;
|
|
456
|
+
try {
|
|
457
|
+
const stat5 = await fs2.stat(lockPath);
|
|
458
|
+
if (Date.now() - stat5.mtimeMs > staleMs) {
|
|
459
|
+
await fs2.unlink(lockPath);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
} catch {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const elapsed = Date.now() - started;
|
|
466
|
+
if (elapsed >= timeoutMs) {
|
|
467
|
+
throw new FsError({
|
|
468
|
+
message: `Timed out waiting for file lock: ${targetPath}`,
|
|
469
|
+
code: "FS_ATOMIC_WRITE_FAILED",
|
|
470
|
+
path: targetPath,
|
|
471
|
+
context: { timeoutMs }
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
await waitForLockRelease(lockPath, timeoutMs - elapsed);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
return await fn();
|
|
479
|
+
} finally {
|
|
480
|
+
try {
|
|
481
|
+
await handle?.close();
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
await fs2.unlink(lockPath);
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async function waitForLockRelease(lockPath, remainingMs) {
|
|
491
|
+
const parentDir = path3.dirname(lockPath);
|
|
492
|
+
const lockName = path3.basename(lockPath);
|
|
493
|
+
const intervalMs = Math.min(remainingMs, 100);
|
|
494
|
+
return new Promise((resolve5) => {
|
|
495
|
+
let settled = false;
|
|
496
|
+
let watcher = null;
|
|
497
|
+
const timer = setTimeout(() => {
|
|
498
|
+
settled = true;
|
|
499
|
+
watcher?.close();
|
|
500
|
+
resolve5();
|
|
501
|
+
}, intervalMs);
|
|
502
|
+
try {
|
|
503
|
+
watcher = watchDir(parentDir, (eventType, filename) => {
|
|
504
|
+
if (settled) return;
|
|
505
|
+
if (filename === lockName && (eventType === "rename" || eventType === "change")) {
|
|
506
|
+
settled = true;
|
|
507
|
+
clearTimeout(timer);
|
|
508
|
+
watcher?.close();
|
|
509
|
+
resolve5();
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
} catch {
|
|
513
|
+
clearTimeout(timer);
|
|
514
|
+
if (!settled) {
|
|
515
|
+
settled = true;
|
|
516
|
+
setTimeout(resolve5, Math.min(remainingMs, 25));
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
fs2.access(lockPath).then(
|
|
521
|
+
() => {
|
|
522
|
+
},
|
|
523
|
+
() => {
|
|
524
|
+
if (!settled) {
|
|
525
|
+
settled = true;
|
|
526
|
+
clearTimeout(timer);
|
|
527
|
+
watcher?.close();
|
|
528
|
+
resolve5();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
535
|
+
async function renameWithRetry(from, to) {
|
|
536
|
+
if (process.platform !== "win32") {
|
|
537
|
+
await fs2.rename(from, to);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const delays = [10, 25, 60, 120, 250];
|
|
541
|
+
let lastErr;
|
|
542
|
+
for (let i = 0; i <= delays.length; i++) {
|
|
543
|
+
try {
|
|
544
|
+
await fs2.rename(from, to);
|
|
545
|
+
return;
|
|
546
|
+
} catch (err) {
|
|
547
|
+
lastErr = err;
|
|
548
|
+
const code = err?.code;
|
|
549
|
+
if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
|
|
550
|
+
throw err;
|
|
551
|
+
}
|
|
552
|
+
await new Promise((resolve5) => setTimeout(resolve5, delays[i]));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
throw lastErr;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/chronicle/types.ts
|
|
559
|
+
var CHRONICLE_SCHEMA_VERSION = 1;
|
|
560
|
+
|
|
561
|
+
// src/chronicle/journal.ts
|
|
562
|
+
var GENESIS_HASH = "0".repeat(64);
|
|
563
|
+
var DEFAULT_MAX_PARTITION_BYTES = 100 * 1024 * 1024;
|
|
564
|
+
var DEFAULT_ROTATION_WINDOW_MS = 60 * 60 * 1e3;
|
|
565
|
+
var RETENTION_CHECKPOINT_VERSION = 1;
|
|
566
|
+
var ChronicleJournal = class {
|
|
567
|
+
basePath;
|
|
568
|
+
now;
|
|
569
|
+
monotonicNow;
|
|
570
|
+
idFactory;
|
|
571
|
+
maxPending;
|
|
572
|
+
batchWindowMs;
|
|
573
|
+
maxPartitionSizeBytes;
|
|
574
|
+
rotationWindowMs;
|
|
575
|
+
retentionDays;
|
|
576
|
+
autoPurgeIntervalMs;
|
|
577
|
+
pending = [];
|
|
578
|
+
drainPromise;
|
|
579
|
+
drainScheduled = false;
|
|
580
|
+
drainTimer;
|
|
581
|
+
counters = { acceptedEvents: 0, persistedEvents: 0, rejectedEvents: 0, failedEvents: 0, batches: 0, maxObservedPending: 0, largestBatch: 0, partitionRolls: 0 };
|
|
582
|
+
lastBatchDurationMs;
|
|
583
|
+
partitionIndex = 0;
|
|
584
|
+
partitionStartedAt;
|
|
585
|
+
lastSequence = 0;
|
|
586
|
+
lastHash = GENESIS_HASH;
|
|
587
|
+
lastAutoPurgeAt = 0;
|
|
588
|
+
constructor(options) {
|
|
589
|
+
this.basePath = path4.resolve(options.filePath);
|
|
590
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
591
|
+
this.monotonicNow = options.monotonicNow ?? (() => process.hrtime.bigint());
|
|
592
|
+
this.idFactory = options.idFactory ?? randomUUID2;
|
|
593
|
+
this.maxPending = Math.max(1, options.maxPending ?? 1e5);
|
|
594
|
+
this.batchWindowMs = Math.max(0, options.batchWindowMs ?? 5);
|
|
595
|
+
this.maxPartitionSizeBytes = options.maxPartitionSizeBytes ?? DEFAULT_MAX_PARTITION_BYTES;
|
|
596
|
+
this.rotationWindowMs = options.rotationWindowMs ?? DEFAULT_ROTATION_WINDOW_MS;
|
|
597
|
+
this.retentionDays = options.retentionDays && Number.isFinite(options.retentionDays) && options.retentionDays > 0 ? options.retentionDays : 0;
|
|
598
|
+
this.autoPurgeIntervalMs = Math.max(0, options.autoPurgeIntervalMs ?? 36e5);
|
|
599
|
+
this.partitionStartedAt = Date.now();
|
|
600
|
+
}
|
|
601
|
+
get path() {
|
|
602
|
+
return this.partitionIndex === 0 ? this.basePath : rotatedPath(this.basePath, this.partitionIndex);
|
|
603
|
+
}
|
|
604
|
+
stats() {
|
|
605
|
+
return { ...this.counters, pendingEvents: this.pending.length, ...this.lastBatchDurationMs !== void 0 ? { lastBatchDurationMs: this.lastBatchDurationMs } : {} };
|
|
606
|
+
}
|
|
607
|
+
append(input) {
|
|
608
|
+
if (this.pending.length >= this.maxPending) {
|
|
609
|
+
this.counters.rejectedEvents++;
|
|
610
|
+
return Promise.reject(new Error(`Chronicle backpressure limit reached (${this.maxPending} pending events)`));
|
|
611
|
+
}
|
|
612
|
+
const promise = new Promise((resolve5, reject) => {
|
|
613
|
+
this.pending.push({ input, resolve: resolve5, reject });
|
|
614
|
+
});
|
|
615
|
+
this.counters.acceptedEvents++;
|
|
616
|
+
this.counters.maxObservedPending = Math.max(this.counters.maxObservedPending, this.pending.length);
|
|
617
|
+
this.scheduleDrain();
|
|
618
|
+
return promise;
|
|
619
|
+
}
|
|
620
|
+
async readAll() {
|
|
621
|
+
await this.flush();
|
|
622
|
+
const files = await collectPartitions(this.basePath);
|
|
623
|
+
const entries = [];
|
|
624
|
+
for (const file of files) entries.push(...await readEntriesStrict(file));
|
|
625
|
+
return entries;
|
|
626
|
+
}
|
|
627
|
+
async flush() {
|
|
628
|
+
if (this.drainTimer) {
|
|
629
|
+
clearTimeout(this.drainTimer);
|
|
630
|
+
this.drainTimer = void 0;
|
|
631
|
+
this.drainScheduled = false;
|
|
632
|
+
}
|
|
633
|
+
while (this.pending.length > 0 || this.drainPromise) {
|
|
634
|
+
if (this.pending.length > 0 && !this.drainPromise) this.startDrain();
|
|
635
|
+
await this.drainPromise;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async verify() {
|
|
639
|
+
await this.flush();
|
|
640
|
+
const files = await collectPartitions(this.basePath);
|
|
641
|
+
const checkpointResult = await readRetentionCheckpoint(this.basePath);
|
|
642
|
+
if (checkpointResult.error) return { ok: false, entries: 0, brokenAt: 0, reason: checkpointResult.error };
|
|
643
|
+
return verifyPartitionFiles(files, checkpointResult.checkpoint);
|
|
644
|
+
}
|
|
645
|
+
async purge(options) {
|
|
646
|
+
await this.flush();
|
|
647
|
+
if (!Number.isFinite(options.retentionDays) || options.retentionDays <= 0) {
|
|
648
|
+
throw new TypeError("Chronicle retentionDays must be a positive finite number");
|
|
649
|
+
}
|
|
650
|
+
await this.refreshStateFromDisk();
|
|
651
|
+
const cutoff = Date.now() - options.retentionDays * 864e5;
|
|
652
|
+
const activePath = path4.resolve(this.path);
|
|
653
|
+
const errors = [];
|
|
654
|
+
let dc = 0, db = 0, sc = 0;
|
|
655
|
+
const suppliedFiles = options.files === void 0 ? await collectJournalPartitions(this.basePath) : [...options.files];
|
|
656
|
+
const eligible = /* @__PURE__ */ new Set();
|
|
657
|
+
for (const suppliedPath of new Set(suppliedFiles)) {
|
|
658
|
+
const file = path4.resolve(suppliedPath);
|
|
659
|
+
if (!isJournalPartition(file, path4.dirname(this.basePath), this.basePath)) {
|
|
660
|
+
errors.push({ file: suppliedPath, reason: "not a Chronicle journal partition in this journal directory" });
|
|
661
|
+
sc++;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (file === activePath) {
|
|
665
|
+
sc++;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
let mtimeMs;
|
|
669
|
+
try {
|
|
670
|
+
const fileStat = await fs3.lstat(file);
|
|
671
|
+
if (!fileStat.isFile()) {
|
|
672
|
+
sc++;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
mtimeMs = fileStat.mtimeMs;
|
|
676
|
+
} catch (error) {
|
|
677
|
+
if (isNotFound(error)) continue;
|
|
678
|
+
errors.push({ file, reason: errorMessage(error) });
|
|
679
|
+
sc++;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (mtimeMs > cutoff) {
|
|
683
|
+
sc++;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
eligible.add(file);
|
|
687
|
+
}
|
|
688
|
+
const candidates = [];
|
|
689
|
+
const allPartitions = await collectJournalPartitions(this.basePath);
|
|
690
|
+
for (const family of groupPartitionsByFamily(allPartitions).values()) {
|
|
691
|
+
for (const file of family) {
|
|
692
|
+
if (file === activePath || !eligible.has(file)) break;
|
|
693
|
+
candidates.push(file);
|
|
694
|
+
eligible.delete(file);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
sc += eligible.size;
|
|
698
|
+
if (!options.dryRun) {
|
|
699
|
+
for (const file of candidates) {
|
|
700
|
+
try {
|
|
701
|
+
const familyBase = partitionFamilyBase(file);
|
|
702
|
+
let deletedBytes;
|
|
703
|
+
await withFileLock(familyBase, async () => {
|
|
704
|
+
const fileStat = await fs3.lstat(file);
|
|
705
|
+
if (!fileStat.isFile() || fileStat.mtimeMs > cutoff) return;
|
|
706
|
+
const entries = await readEntriesStrict(file);
|
|
707
|
+
const checkpointResult = await readRetentionCheckpoint(familyBase);
|
|
708
|
+
if (checkpointResult.error) throw new Error(checkpointResult.error);
|
|
709
|
+
const checkpoint = checkpointResult.checkpoint;
|
|
710
|
+
const nextCheckpoint = verifyRetainedPrefix(entries, checkpoint);
|
|
711
|
+
if (!nextCheckpoint) throw new Error("partition does not extend the trusted Chronicle chain");
|
|
712
|
+
if (nextCheckpoint.sequence > (checkpoint?.sequence ?? 0)) {
|
|
713
|
+
await writeRetentionCheckpoint(familyBase, nextCheckpoint);
|
|
714
|
+
}
|
|
715
|
+
deletedBytes = fileStat.size;
|
|
716
|
+
await fs3.unlink(file);
|
|
717
|
+
});
|
|
718
|
+
if (deletedBytes === void 0) {
|
|
719
|
+
sc++;
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
db += deletedBytes;
|
|
723
|
+
dc++;
|
|
724
|
+
} catch (error) {
|
|
725
|
+
errors.push({ file, reason: errorMessage(error) });
|
|
726
|
+
sc++;
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return { deletedCount: dc, deletedBytes: db, skippedCount: sc, errors, ...options.dryRun ? { candidates } : {} };
|
|
732
|
+
}
|
|
733
|
+
async maybeAutoPurge() {
|
|
734
|
+
if (this.retentionDays <= 0) return;
|
|
735
|
+
const n = Date.now();
|
|
736
|
+
if (n - this.lastAutoPurgeAt < this.autoPurgeIntervalMs) return;
|
|
737
|
+
this.lastAutoPurgeAt = n;
|
|
738
|
+
try {
|
|
739
|
+
await this.purge({ retentionDays: this.retentionDays });
|
|
740
|
+
} catch {
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
scheduleDrain() {
|
|
744
|
+
if (this.drainScheduled || this.drainPromise) return;
|
|
745
|
+
this.drainScheduled = true;
|
|
746
|
+
this.drainTimer = setTimeout(() => {
|
|
747
|
+
this.drainTimer = void 0;
|
|
748
|
+
this.drainScheduled = false;
|
|
749
|
+
this.startDrain();
|
|
750
|
+
}, this.batchWindowMs);
|
|
751
|
+
}
|
|
752
|
+
startDrain() {
|
|
753
|
+
if (this.drainPromise || this.pending.length === 0) return;
|
|
754
|
+
const batch = this.pending.splice(0);
|
|
755
|
+
const drain = this.persistBatch(batch);
|
|
756
|
+
this.drainPromise = drain.finally(() => {
|
|
757
|
+
this.drainPromise = void 0;
|
|
758
|
+
if (this.pending.length > 0) this.scheduleDrain();
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
async refreshStateFromDisk() {
|
|
762
|
+
const files = await collectPartitions(this.basePath);
|
|
763
|
+
const latest = files[files.length - 1] ?? this.basePath;
|
|
764
|
+
this.partitionIndex = partitionIndex(latest, this.basePath);
|
|
765
|
+
const entry = await readLastEntry(latest);
|
|
766
|
+
const checkpointResult = await readRetentionCheckpoint(this.basePath);
|
|
767
|
+
if (checkpointResult.error) throw new Error(checkpointResult.error);
|
|
768
|
+
const checkpoint = checkpointResult.checkpoint;
|
|
769
|
+
this.lastSequence = entry?.sequence ?? checkpoint?.sequence ?? 0;
|
|
770
|
+
this.lastHash = entry?.hash ?? checkpoint?.hash ?? GENESIS_HASH;
|
|
771
|
+
try {
|
|
772
|
+
this.partitionStartedAt = (await fs3.stat(latest)).birthtimeMs;
|
|
773
|
+
} catch {
|
|
774
|
+
this.partitionStartedAt = Date.now();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async checkRotation() {
|
|
778
|
+
if (this.partitionIndex === 0 && this.lastSequence === 0) return;
|
|
779
|
+
if (Number.isFinite(this.rotationWindowMs) && Date.now() - this.partitionStartedAt >= this.rotationWindowMs) {
|
|
780
|
+
this.rotate();
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (Number.isFinite(this.maxPartitionSizeBytes)) {
|
|
784
|
+
try {
|
|
785
|
+
if ((await fs3.stat(this.path)).size >= this.maxPartitionSizeBytes) this.rotate();
|
|
786
|
+
} catch {
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
rotate() {
|
|
791
|
+
this.partitionIndex++;
|
|
792
|
+
this.partitionStartedAt = Date.now();
|
|
793
|
+
this.counters.partitionRolls++;
|
|
794
|
+
}
|
|
795
|
+
async persistBatch(batch) {
|
|
796
|
+
const started = performance.now();
|
|
797
|
+
this.counters.batches++;
|
|
798
|
+
this.counters.largestBatch = Math.max(this.counters.largestBatch, batch.length);
|
|
799
|
+
try {
|
|
800
|
+
await ensureDir(path4.dirname(this.basePath));
|
|
801
|
+
let recorded = [];
|
|
802
|
+
await withFileLock(this.basePath, async () => {
|
|
803
|
+
await this.refreshStateFromDisk();
|
|
804
|
+
await this.checkRotation();
|
|
805
|
+
const cp = this.path;
|
|
806
|
+
let prev = this.lastSequence > 0 ? { sequence: this.lastSequence, hash: this.lastHash } : void 0;
|
|
807
|
+
recorded = batch.map(({ input }) => {
|
|
808
|
+
const instant = this.now().toISOString();
|
|
809
|
+
const ni = removeUndefined(input);
|
|
810
|
+
const uh = { ...ni, occurredAt: input.occurredAt ?? instant, monotonicNs: input.monotonicNs ?? this.monotonicNow().toString(), schemaVersion: CHRONICLE_SCHEMA_VERSION, eventId: this.idFactory(), observedAt: instant, persistedAt: instant, sequence: (prev?.sequence ?? 0) + 1, previousHash: prev?.hash ?? GENESIS_HASH };
|
|
811
|
+
const event = { ...uh, hash: hashValue(uh) };
|
|
812
|
+
prev = event;
|
|
813
|
+
return event;
|
|
814
|
+
});
|
|
815
|
+
await fs3.appendFile(cp, recorded.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf8");
|
|
816
|
+
});
|
|
817
|
+
const last = recorded[recorded.length - 1];
|
|
818
|
+
this.lastSequence = last.sequence;
|
|
819
|
+
this.lastHash = last.hash;
|
|
820
|
+
batch.forEach((item, i) => {
|
|
821
|
+
item.resolve(recorded[i]);
|
|
822
|
+
});
|
|
823
|
+
this.counters.persistedEvents += batch.length;
|
|
824
|
+
void this.maybeAutoPurge();
|
|
825
|
+
} catch (error) {
|
|
826
|
+
this.counters.failedEvents += batch.length;
|
|
827
|
+
batch.forEach((item) => {
|
|
828
|
+
item.reject(error);
|
|
829
|
+
});
|
|
830
|
+
} finally {
|
|
831
|
+
this.lastBatchDurationMs = performance.now() - started;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
function rotatedPath(basePath, index) {
|
|
836
|
+
const dir = path4.dirname(basePath);
|
|
837
|
+
const ext = path4.extname(basePath);
|
|
838
|
+
const base = path4.basename(basePath, ext);
|
|
839
|
+
return path4.join(dir, `${base}.${String(index).padStart(5, "0")}${ext}`);
|
|
840
|
+
}
|
|
841
|
+
async function collectPartitions(basePath) {
|
|
842
|
+
const dir = path4.dirname(basePath);
|
|
843
|
+
const ext = path4.extname(basePath);
|
|
844
|
+
const base = path4.basename(basePath, ext);
|
|
845
|
+
const pattern = new RegExp(`^${escapeRegex(base)}(?:\\.\\d{5})?${escapeRegex(ext)}$`);
|
|
846
|
+
const result = [];
|
|
847
|
+
try {
|
|
848
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
849
|
+
for (const entry of entries) if (entry.isFile() && pattern.test(entry.name)) result.push(path4.join(dir, entry.name));
|
|
850
|
+
} catch {
|
|
851
|
+
}
|
|
852
|
+
const baseFile = path4.join(dir, base + ext);
|
|
853
|
+
const rotated = result.filter((file) => file !== baseFile).sort((left, right) => parseIndex(left, base, ext) - parseIndex(right, base, ext));
|
|
854
|
+
return [baseFile, ...rotated];
|
|
855
|
+
}
|
|
856
|
+
async function collectJournalPartitions(basePath) {
|
|
857
|
+
const directory = path4.dirname(basePath);
|
|
858
|
+
const result = [];
|
|
859
|
+
try {
|
|
860
|
+
const entries = await fs3.readdir(directory, { withFileTypes: true });
|
|
861
|
+
for (const entry of entries) {
|
|
862
|
+
const file = path4.join(directory, entry.name);
|
|
863
|
+
if (entry.isFile() && isJournalPartition(file, directory, basePath)) result.push(file);
|
|
864
|
+
}
|
|
865
|
+
} catch {
|
|
866
|
+
}
|
|
867
|
+
return result.sort(compareJournalPartitions);
|
|
868
|
+
}
|
|
869
|
+
function isJournalPartition(filePath, directory, basePath) {
|
|
870
|
+
if (path4.dirname(path4.resolve(filePath)) !== path4.resolve(directory)) return false;
|
|
871
|
+
const fileName = path4.basename(filePath);
|
|
872
|
+
if (!basePath) return false;
|
|
873
|
+
const baseName = path4.basename(basePath);
|
|
874
|
+
const dailyFamily = /^\d{4}-\d{2}-\d{2}\.events(?:\.\d{5})?\.jsonl$/;
|
|
875
|
+
if (/^\d{4}-\d{2}-\d{2}\.events\.jsonl$/.test(baseName)) return dailyFamily.test(fileName);
|
|
876
|
+
return partitionFamilyBase(path4.resolve(filePath)) === partitionFamilyBase(path4.resolve(basePath));
|
|
877
|
+
}
|
|
878
|
+
function compareJournalPartitions(left, right) {
|
|
879
|
+
const pattern = /^(.*\.events)(?:\.(\d{5}))?\.jsonl$/;
|
|
880
|
+
const leftMatch = pattern.exec(path4.basename(left));
|
|
881
|
+
const rightMatch = pattern.exec(path4.basename(right));
|
|
882
|
+
const familyOrder = (leftMatch?.[1] ?? left).localeCompare(rightMatch?.[1] ?? right);
|
|
883
|
+
return familyOrder || Number(leftMatch?.[2] ?? 0) - Number(rightMatch?.[2] ?? 0);
|
|
884
|
+
}
|
|
885
|
+
function groupPartitionsByFamily(files) {
|
|
886
|
+
const groups = /* @__PURE__ */ new Map();
|
|
887
|
+
for (const file of files) {
|
|
888
|
+
const family = partitionFamilyBase(file);
|
|
889
|
+
const group = groups.get(family) ?? [];
|
|
890
|
+
group.push(file);
|
|
891
|
+
groups.set(family, group);
|
|
892
|
+
}
|
|
893
|
+
return groups;
|
|
894
|
+
}
|
|
895
|
+
function partitionFamilyBase(filePath) {
|
|
896
|
+
return filePath.replace(/\.\d{5}(?=\.jsonl$)/, "");
|
|
897
|
+
}
|
|
898
|
+
function retentionCheckpointPath(basePath) {
|
|
899
|
+
return `${partitionFamilyBase(basePath)}.retention.json`;
|
|
900
|
+
}
|
|
901
|
+
function verifyRetainedPrefix(entries, checkpoint) {
|
|
902
|
+
if (entries.length === 0) return void 0;
|
|
903
|
+
let sequence = checkpoint?.sequence ?? 0;
|
|
904
|
+
let hash3 = checkpoint?.hash ?? GENESIS_HASH;
|
|
905
|
+
let advanced = false;
|
|
906
|
+
for (const entry of entries) {
|
|
907
|
+
if (entry.sequence <= sequence) continue;
|
|
908
|
+
if (entry.sequence !== sequence + 1 || entry.previousHash !== hash3) return void 0;
|
|
909
|
+
const { hash: recordedHash, ...content } = entry;
|
|
910
|
+
if (hashValue(content) !== recordedHash) return void 0;
|
|
911
|
+
sequence = entry.sequence;
|
|
912
|
+
hash3 = recordedHash;
|
|
913
|
+
advanced = true;
|
|
914
|
+
}
|
|
915
|
+
if (!advanced) return checkpoint;
|
|
916
|
+
return { version: RETENTION_CHECKPOINT_VERSION, sequence, hash: hash3 };
|
|
917
|
+
}
|
|
918
|
+
async function verifyPartitionFiles(files, checkpoint) {
|
|
919
|
+
const checkpointSequence = checkpoint?.sequence ?? 0;
|
|
920
|
+
let previousHash = checkpoint?.hash ?? GENESIS_HASH;
|
|
921
|
+
let entries = 0;
|
|
922
|
+
let lastSequence = checkpointSequence;
|
|
923
|
+
let coveredPrevious;
|
|
924
|
+
for (const file of files) {
|
|
925
|
+
let chunk;
|
|
926
|
+
try {
|
|
927
|
+
chunk = await readEntriesStrict(file);
|
|
928
|
+
} catch (error) {
|
|
929
|
+
return { ok: false, entries, brokenAt: entries, reason: errorMessage(error) };
|
|
930
|
+
}
|
|
931
|
+
for (const entry of chunk) {
|
|
932
|
+
const { hash: recordedHash, ...content } = entry;
|
|
933
|
+
if (entry.sequence <= checkpointSequence) {
|
|
934
|
+
if (hashValue(content) !== recordedHash) return { ok: false, entries, brokenAt: entries, reason: "entry hash mismatch" };
|
|
935
|
+
if (coveredPrevious && entry.sequence !== coveredPrevious.sequence + 1) {
|
|
936
|
+
return { ok: false, entries, brokenAt: entries, reason: `sequence ${entry.sequence} is not ${coveredPrevious.sequence + 1}` };
|
|
937
|
+
}
|
|
938
|
+
if (coveredPrevious && entry.previousHash !== coveredPrevious.hash) {
|
|
939
|
+
return { ok: false, entries, brokenAt: entries, reason: "previous hash mismatch" };
|
|
940
|
+
}
|
|
941
|
+
if (entry.sequence === checkpointSequence && recordedHash !== checkpoint?.hash) {
|
|
942
|
+
return { ok: false, entries, brokenAt: entries, reason: "retention checkpoint hash mismatch" };
|
|
943
|
+
}
|
|
944
|
+
coveredPrevious = entry;
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const index = entries++;
|
|
948
|
+
if (entry.sequence !== lastSequence + 1) return { ok: false, entries, brokenAt: index, reason: `sequence ${entry.sequence} is not ${lastSequence + 1}` };
|
|
949
|
+
if (entry.previousHash !== previousHash) return { ok: false, entries, brokenAt: index, reason: "previous hash mismatch" };
|
|
950
|
+
if (hashValue(content) !== recordedHash) return { ok: false, entries, brokenAt: index, reason: "entry hash mismatch" };
|
|
951
|
+
previousHash = recordedHash;
|
|
952
|
+
lastSequence = entry.sequence;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (coveredPrevious && (coveredPrevious.sequence !== checkpointSequence || coveredPrevious.hash !== checkpoint?.hash)) {
|
|
956
|
+
return { ok: false, entries, brokenAt: entries, reason: "retention checkpoint hash mismatch" };
|
|
957
|
+
}
|
|
958
|
+
return { ok: true, entries, lastSequence, lastHash: previousHash };
|
|
959
|
+
}
|
|
960
|
+
async function readRetentionCheckpoint(basePath) {
|
|
961
|
+
const checkpointPath = retentionCheckpointPath(basePath);
|
|
962
|
+
let raw;
|
|
963
|
+
try {
|
|
964
|
+
raw = await fs3.readFile(checkpointPath, "utf8");
|
|
965
|
+
} catch (error) {
|
|
966
|
+
return isNotFound(error) ? {} : { error: `cannot read retention checkpoint: ${errorMessage(error)}` };
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
const parsed = JSON.parse(raw);
|
|
970
|
+
if (parsed.version !== RETENTION_CHECKPOINT_VERSION || !Number.isSafeInteger(parsed.sequence) || (parsed.sequence ?? -1) < 0 || !isHash(parsed.hash)) {
|
|
971
|
+
return { error: "invalid Chronicle retention checkpoint" };
|
|
972
|
+
}
|
|
973
|
+
return { checkpoint: parsed };
|
|
974
|
+
} catch {
|
|
975
|
+
return { error: "invalid Chronicle retention checkpoint JSON" };
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
async function writeRetentionCheckpoint(basePath, checkpoint) {
|
|
979
|
+
await atomicWrite(retentionCheckpointPath(basePath), `${JSON.stringify(checkpoint)}
|
|
980
|
+
`, { mode: 384 });
|
|
981
|
+
}
|
|
982
|
+
function isHash(value) {
|
|
983
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
984
|
+
}
|
|
985
|
+
function parseIndex(filePath, base, ext) {
|
|
986
|
+
const suffix = path4.basename(filePath).slice(base.length + 1, -ext.length);
|
|
987
|
+
return suffix ? parseInt(suffix, 10) : 0;
|
|
988
|
+
}
|
|
989
|
+
function partitionIndex(filePath, basePath) {
|
|
990
|
+
const ext = path4.extname(basePath);
|
|
991
|
+
return parseIndex(filePath, path4.basename(basePath, ext), ext);
|
|
992
|
+
}
|
|
993
|
+
function escapeRegex(text2) {
|
|
994
|
+
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
995
|
+
}
|
|
996
|
+
async function readLastEntry(filePath) {
|
|
997
|
+
let handle;
|
|
998
|
+
try {
|
|
999
|
+
handle = await fs3.open(filePath, "r");
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
if (isNotFound(error)) return void 0;
|
|
1002
|
+
throw error;
|
|
1003
|
+
}
|
|
1004
|
+
try {
|
|
1005
|
+
const size = (await handle.stat()).size;
|
|
1006
|
+
let position = size, suffix = "";
|
|
1007
|
+
while (position > 0) {
|
|
1008
|
+
const length = Math.min(65536, position);
|
|
1009
|
+
position -= length;
|
|
1010
|
+
const buf = Buffer.allocUnsafe(length);
|
|
1011
|
+
await handle.read(buf, 0, length, position);
|
|
1012
|
+
suffix = buf.toString("utf8") + suffix;
|
|
1013
|
+
const lines = suffix.split("\n");
|
|
1014
|
+
const start = position === 0 ? 0 : 1;
|
|
1015
|
+
for (let i = lines.length - 1; i >= start; i--) {
|
|
1016
|
+
const trimmed = lines[i].trim();
|
|
1017
|
+
if (!trimmed) continue;
|
|
1018
|
+
try {
|
|
1019
|
+
return JSON.parse(trimmed);
|
|
1020
|
+
} catch {
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
suffix = lines[0] ?? "";
|
|
1024
|
+
}
|
|
1025
|
+
return void 0;
|
|
1026
|
+
} finally {
|
|
1027
|
+
await handle.close();
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
async function readEntriesStrict(filePath) {
|
|
1031
|
+
let raw;
|
|
1032
|
+
try {
|
|
1033
|
+
raw = await fs3.readFile(filePath, "utf8");
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
if (isNotFound(error)) return [];
|
|
1036
|
+
throw error;
|
|
1037
|
+
}
|
|
1038
|
+
const entries = [];
|
|
1039
|
+
const lines = raw.split("\n");
|
|
1040
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1041
|
+
const trimmed = lines[i].trim();
|
|
1042
|
+
if (!trimmed) continue;
|
|
1043
|
+
try {
|
|
1044
|
+
entries.push(JSON.parse(trimmed));
|
|
1045
|
+
} catch {
|
|
1046
|
+
throw new Error(`invalid JSON at line ${i + 1} in ${path4.basename(filePath)}`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return entries;
|
|
1050
|
+
}
|
|
1051
|
+
function hashValue(value) {
|
|
1052
|
+
return createHash3("sha256").update(stableStringify(value), "utf8").digest("hex");
|
|
1053
|
+
}
|
|
1054
|
+
function stableStringify(value) {
|
|
1055
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1056
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1057
|
+
const obj = value;
|
|
1058
|
+
return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
1059
|
+
}
|
|
1060
|
+
function removeUndefined(value) {
|
|
1061
|
+
if (Array.isArray(value)) return value.map((item) => item === void 0 ? null : removeUndefined(item));
|
|
1062
|
+
if (value === null || typeof value !== "object") return value;
|
|
1063
|
+
const result = {};
|
|
1064
|
+
for (const [key, item] of Object.entries(value)) if (item !== void 0) result[key] = removeUndefined(item);
|
|
1065
|
+
return result;
|
|
1066
|
+
}
|
|
1067
|
+
function isNotFound(error) {
|
|
1068
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1069
|
+
}
|
|
1070
|
+
function errorMessage(error) {
|
|
1071
|
+
return error instanceof Error ? error.message : String(error);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/chronicle/provider-adapter.ts
|
|
1075
|
+
function wireProviderAttemptsToChronicle(options) {
|
|
1076
|
+
const unsubs = [
|
|
1077
|
+
options.events.on("provider.attempt.started", (event) => persist(options, event, {
|
|
1078
|
+
eventType: "provider.attempt.started",
|
|
1079
|
+
outcome: "started",
|
|
1080
|
+
occurredAt: event.startedAt
|
|
1081
|
+
})),
|
|
1082
|
+
options.events.on("provider.attempt.completed", (event) => persist(options, event, {
|
|
1083
|
+
eventType: "provider.attempt.completed",
|
|
1084
|
+
outcome: "success",
|
|
1085
|
+
occurredAt: event.endedAt,
|
|
1086
|
+
durationNs: millisecondsToNanoseconds(event.durationMs)
|
|
1087
|
+
})),
|
|
1088
|
+
options.events.on("provider.attempt.failed", (event) => persist(options, event, {
|
|
1089
|
+
eventType: "provider.attempt.failed",
|
|
1090
|
+
outcome: "failure",
|
|
1091
|
+
occurredAt: event.endedAt,
|
|
1092
|
+
durationNs: millisecondsToNanoseconds(event.durationMs)
|
|
1093
|
+
}))
|
|
1094
|
+
];
|
|
1095
|
+
return () => unsubs.forEach((unsubscribe) => {
|
|
1096
|
+
unsubscribe();
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
function persist(options, event, base) {
|
|
1100
|
+
const context = typeof options.context === "function" ? options.context() : options.context;
|
|
1101
|
+
const input = {
|
|
1102
|
+
...base,
|
|
1103
|
+
scope: {
|
|
1104
|
+
...context.scope,
|
|
1105
|
+
sessionId: event.sessionId,
|
|
1106
|
+
...event.agentId ? { agentId: event.agentId } : {}
|
|
1107
|
+
},
|
|
1108
|
+
correlation: {
|
|
1109
|
+
...context.correlation,
|
|
1110
|
+
...event.traceId ? { traceId: event.traceId } : {},
|
|
1111
|
+
logicalRequestId: event.logicalRequestId,
|
|
1112
|
+
attemptId: event.attemptId
|
|
1113
|
+
},
|
|
1114
|
+
runtime: { providerId: event.providerId, modelId: event.model },
|
|
1115
|
+
attributes: providerAttributes(event)
|
|
1116
|
+
};
|
|
1117
|
+
void options.journal.append(input).catch((error) => options.onPersistError?.(error, input));
|
|
1118
|
+
}
|
|
1119
|
+
function providerAttributes(event) {
|
|
1120
|
+
const {
|
|
1121
|
+
sessionId: _sessionId,
|
|
1122
|
+
traceId: _traceId,
|
|
1123
|
+
agentId: _agentId,
|
|
1124
|
+
providerId: _providerId,
|
|
1125
|
+
model: _model,
|
|
1126
|
+
logicalRequestId: _logicalRequestId,
|
|
1127
|
+
attemptId: _attemptId,
|
|
1128
|
+
...attributes
|
|
1129
|
+
} = event;
|
|
1130
|
+
return attributes;
|
|
1131
|
+
}
|
|
1132
|
+
function millisecondsToNanoseconds(durationMs2) {
|
|
1133
|
+
return Math.round(durationMs2 * 1e6).toString();
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/chronicle/tool-adapter.ts
|
|
1137
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1138
|
+
function wireToolsToChronicle(options) {
|
|
1139
|
+
const unsubs = [
|
|
1140
|
+
options.events.on("tool.started", (event) => {
|
|
1141
|
+
const input = scrubValue(options.scrubber, event.input);
|
|
1142
|
+
persist2(options, event, {
|
|
1143
|
+
eventType: "tool.started",
|
|
1144
|
+
outcome: "started",
|
|
1145
|
+
attributes: {
|
|
1146
|
+
toolName: event.name,
|
|
1147
|
+
input,
|
|
1148
|
+
inputHash: hashText(input)
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
}),
|
|
1152
|
+
options.events.on("tool.executed", (event) => {
|
|
1153
|
+
const output = options.scrubber.scrub(event.output ?? "");
|
|
1154
|
+
persist2(options, event, {
|
|
1155
|
+
eventType: "tool.executed",
|
|
1156
|
+
outcome: event.ok ? "success" : "failure",
|
|
1157
|
+
durationNs: millisecondsToNanoseconds2(event.durationMs),
|
|
1158
|
+
attributes: {
|
|
1159
|
+
toolName: event.name,
|
|
1160
|
+
ok: event.ok,
|
|
1161
|
+
outputPreview: output,
|
|
1162
|
+
outputHash: hashText(output),
|
|
1163
|
+
outputBytes: event.outputBytes,
|
|
1164
|
+
outputTokens: event.outputTokens,
|
|
1165
|
+
outputLines: event.outputLines,
|
|
1166
|
+
metadata: event.metadata
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
persistEvidenceEdges(options, event);
|
|
1170
|
+
}),
|
|
1171
|
+
options.events.on("tool.failed", (event) => persist2(options, event, {
|
|
1172
|
+
eventType: "tool.failed",
|
|
1173
|
+
outcome: "failure",
|
|
1174
|
+
durationNs: millisecondsToNanoseconds2(event.durationMs),
|
|
1175
|
+
attributes: {
|
|
1176
|
+
toolName: event.name,
|
|
1177
|
+
category: event.category,
|
|
1178
|
+
retryable: event.retryable,
|
|
1179
|
+
detail: event.detail,
|
|
1180
|
+
errorCode: event.errorCode,
|
|
1181
|
+
errorSubsystem: event.errorSubsystem,
|
|
1182
|
+
errorSeverity: event.errorSeverity
|
|
1183
|
+
}
|
|
1184
|
+
})),
|
|
1185
|
+
options.events.on("tool.progress", (event) => {
|
|
1186
|
+
if (event.event.type !== "file_changed") return;
|
|
1187
|
+
const resource = progressResource(event);
|
|
1188
|
+
persist2(options, event, {
|
|
1189
|
+
eventType: "file.mutation.observed",
|
|
1190
|
+
outcome: "started",
|
|
1191
|
+
...resource ? { resource } : {},
|
|
1192
|
+
attributes: {
|
|
1193
|
+
toolName: event.name,
|
|
1194
|
+
progressType: event.event.type,
|
|
1195
|
+
text: options.scrubber.scrub(event.event.text ?? ""),
|
|
1196
|
+
data: scrubValue(options.scrubber, event.event.data),
|
|
1197
|
+
operation: event.event.operation
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
})
|
|
1201
|
+
];
|
|
1202
|
+
return () => unsubs.forEach((unsubscribe) => {
|
|
1203
|
+
unsubscribe();
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
function persist2(options, event, fields) {
|
|
1207
|
+
const context = typeof options.context === "function" ? options.context() : options.context;
|
|
1208
|
+
const input = {
|
|
1209
|
+
...fields,
|
|
1210
|
+
scope: {
|
|
1211
|
+
...context.scope,
|
|
1212
|
+
...event.sessionId ? { sessionId: event.sessionId } : {},
|
|
1213
|
+
...event.agentId ? { agentId: event.agentId } : {}
|
|
1214
|
+
},
|
|
1215
|
+
correlation: {
|
|
1216
|
+
...context.correlation,
|
|
1217
|
+
...event.traceId ? { traceId: event.traceId } : {},
|
|
1218
|
+
...event.id ? { toolCallId: event.id } : {}
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
void options.journal.append(input).catch((error) => options.onPersistError?.(error, input));
|
|
1222
|
+
}
|
|
1223
|
+
function persistEvidenceEdges(options, event) {
|
|
1224
|
+
const metadata = event.metadata;
|
|
1225
|
+
if (!metadata) return;
|
|
1226
|
+
for (const file of metadata.files) {
|
|
1227
|
+
persist2(options, event, {
|
|
1228
|
+
eventType: "tool.resource.observed",
|
|
1229
|
+
outcome: event.ok ? "success" : "failure",
|
|
1230
|
+
resource: { kind: "file", id: resourceId2("file", file), path: file },
|
|
1231
|
+
attributes: { relation: "observed", toolName: event.name, evidenceStatus: metadata.status }
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
for (const symbol of metadata.symbols) {
|
|
1235
|
+
persist2(options, event, {
|
|
1236
|
+
eventType: "tool.resource.observed",
|
|
1237
|
+
outcome: event.ok ? "success" : "failure",
|
|
1238
|
+
resource: { kind: "symbol", id: resourceId2("symbol", symbol) },
|
|
1239
|
+
attributes: { relation: "observed", toolName: event.name, symbol }
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
for (const command of metadata.commands) {
|
|
1243
|
+
persist2(options, event, {
|
|
1244
|
+
eventType: "tool.resource.observed",
|
|
1245
|
+
outcome: event.ok ? "success" : "failure",
|
|
1246
|
+
resource: { kind: "process", id: resourceId2("command", command) },
|
|
1247
|
+
attributes: { relation: "invoked", toolName: event.name, command: options.scrubber.scrub(command) }
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
function progressResource(event) {
|
|
1252
|
+
if (event.event.type !== "file_changed" || !event.event.path) return void 0;
|
|
1253
|
+
return {
|
|
1254
|
+
kind: "file",
|
|
1255
|
+
id: resourceId2("file", event.event.path),
|
|
1256
|
+
path: event.event.path,
|
|
1257
|
+
...event.event.line !== void 0 ? { lineStart: event.event.line } : {},
|
|
1258
|
+
...event.event.endLine !== void 0 ? { lineEnd: event.event.endLine } : {}
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
function scrubValue(scrubber, value) {
|
|
1262
|
+
if (value === void 0) return "";
|
|
1263
|
+
try {
|
|
1264
|
+
return scrubber.scrub(JSON.stringify(value));
|
|
1265
|
+
} catch {
|
|
1266
|
+
return scrubber.scrub(String(value));
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
function resourceId2(kind, value) {
|
|
1270
|
+
return `${kind}_${hashText(value).slice(0, 24)}`;
|
|
1271
|
+
}
|
|
1272
|
+
function hashText(value) {
|
|
1273
|
+
return createHash4("sha256").update(value).digest("hex");
|
|
1274
|
+
}
|
|
1275
|
+
function millisecondsToNanoseconds2(durationMs2) {
|
|
1276
|
+
return Math.round(durationMs2 * 1e6).toString();
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/chronicle/process-adapter.ts
|
|
1280
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
1281
|
+
function wireProcessesToChronicle(options) {
|
|
1282
|
+
const unsubs = [
|
|
1283
|
+
options.events.on("process.started", (event) => persist3(options, event, {
|
|
1284
|
+
eventType: "process.started",
|
|
1285
|
+
outcome: "started",
|
|
1286
|
+
occurredAt: event.startedAt,
|
|
1287
|
+
attributes: {
|
|
1288
|
+
command: options.scrubber.scrub(event.command),
|
|
1289
|
+
args: event.args.map((arg) => options.scrubber.scrub(arg)),
|
|
1290
|
+
cwd: event.cwd,
|
|
1291
|
+
parentPid: event.parentPid,
|
|
1292
|
+
background: event.background
|
|
1293
|
+
}
|
|
1294
|
+
})),
|
|
1295
|
+
options.events.on("process.completed", (event) => persist3(options, event, {
|
|
1296
|
+
eventType: "process.completed",
|
|
1297
|
+
outcome: event.exitCode === 0 ? "success" : event.timedOut ? "cancelled" : "failure",
|
|
1298
|
+
occurredAt: event.endedAt,
|
|
1299
|
+
durationNs: Math.round(event.durationMs * 1e6).toString(),
|
|
1300
|
+
attributes: {
|
|
1301
|
+
exitCode: event.exitCode,
|
|
1302
|
+
signal: event.signal,
|
|
1303
|
+
stdoutBytes: event.stdoutBytes,
|
|
1304
|
+
stderrBytes: event.stderrBytes,
|
|
1305
|
+
timedOut: event.timedOut
|
|
1306
|
+
}
|
|
1307
|
+
}))
|
|
1308
|
+
];
|
|
1309
|
+
return () => unsubs.forEach((unsubscribe) => {
|
|
1310
|
+
unsubscribe();
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
function persist3(options, event, fields) {
|
|
1314
|
+
const context = typeof options.context === "function" ? options.context() : options.context;
|
|
1315
|
+
const processKey = `${event.sessionId}\0${event.pid ?? "unknown"}\0${event.toolCallId}`;
|
|
1316
|
+
const input = {
|
|
1317
|
+
...fields,
|
|
1318
|
+
scope: {
|
|
1319
|
+
...context.scope,
|
|
1320
|
+
sessionId: event.sessionId,
|
|
1321
|
+
...event.agentId ? { agentId: event.agentId } : {}
|
|
1322
|
+
},
|
|
1323
|
+
correlation: {
|
|
1324
|
+
...context.correlation,
|
|
1325
|
+
...event.traceId ? { traceId: event.traceId } : {},
|
|
1326
|
+
toolCallId: event.toolCallId
|
|
1327
|
+
},
|
|
1328
|
+
runtime: {
|
|
1329
|
+
...event.pid !== void 0 ? { processId: event.pid } : {},
|
|
1330
|
+
..."parentPid" in event ? { parentProcessId: event.parentPid } : {}
|
|
1331
|
+
},
|
|
1332
|
+
resource: {
|
|
1333
|
+
kind: "process",
|
|
1334
|
+
id: `process_${createHash5("sha256").update(processKey).digest("hex").slice(0, 24)}`
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
void options.journal.append(input).catch((error) => options.onPersistError?.(error, input));
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/chronicle/health-monitor.ts
|
|
1341
|
+
import { monitorEventLoopDelay, performance as performance2 } from "node:perf_hooks";
|
|
1342
|
+
function startChronicleHealthMonitor(options) {
|
|
1343
|
+
const intervalMs = Math.max(5e3, options.intervalMs ?? 3e4);
|
|
1344
|
+
const delay = monitorEventLoopDelay({ resolution: 20 });
|
|
1345
|
+
delay.enable();
|
|
1346
|
+
let previousCpu = process.cpuUsage();
|
|
1347
|
+
let previousElu = performance2.eventLoopUtilization();
|
|
1348
|
+
const sample = () => {
|
|
1349
|
+
const context = typeof options.context === "function" ? options.context() : options.context;
|
|
1350
|
+
const memory = process.memoryUsage();
|
|
1351
|
+
const cpu = process.cpuUsage(previousCpu);
|
|
1352
|
+
previousCpu = process.cpuUsage();
|
|
1353
|
+
const elu = performance2.eventLoopUtilization(previousElu);
|
|
1354
|
+
previousElu = performance2.eventLoopUtilization();
|
|
1355
|
+
const journalBeforeSample = options.journal.stats();
|
|
1356
|
+
void options.journal.append({
|
|
1357
|
+
eventType: "runtime.health.sampled",
|
|
1358
|
+
scope: context.scope,
|
|
1359
|
+
correlation: context.correlation,
|
|
1360
|
+
runtime: { processId: process.pid, parentProcessId: process.ppid },
|
|
1361
|
+
outcome: "success",
|
|
1362
|
+
resource: { kind: "process", id: `process:${process.pid}` },
|
|
1363
|
+
attributes: {
|
|
1364
|
+
uptimeSeconds: process.uptime(),
|
|
1365
|
+
eventLoop: {
|
|
1366
|
+
utilization: elu.utilization,
|
|
1367
|
+
activeMs: elu.active,
|
|
1368
|
+
idleMs: elu.idle,
|
|
1369
|
+
delayMeanMs: Number(delay.mean) / 1e6,
|
|
1370
|
+
delayP95Ms: Number(delay.percentile(95)) / 1e6,
|
|
1371
|
+
delayMaxMs: Number(delay.max) / 1e6
|
|
1372
|
+
},
|
|
1373
|
+
cpu: { userMicros: cpu.user, systemMicros: cpu.system },
|
|
1374
|
+
memory: {
|
|
1375
|
+
rssBytes: memory.rss,
|
|
1376
|
+
heapTotalBytes: memory.heapTotal,
|
|
1377
|
+
heapUsedBytes: memory.heapUsed,
|
|
1378
|
+
externalBytes: memory.external,
|
|
1379
|
+
arrayBuffersBytes: memory.arrayBuffers
|
|
1380
|
+
},
|
|
1381
|
+
chronicle: journalBeforeSample
|
|
1382
|
+
}
|
|
1383
|
+
}).catch((error) => options.onPersistError?.(error));
|
|
1384
|
+
delay.reset();
|
|
1385
|
+
};
|
|
1386
|
+
const timer = setInterval(sample, intervalMs);
|
|
1387
|
+
timer.unref?.();
|
|
1388
|
+
return () => {
|
|
1389
|
+
clearInterval(timer);
|
|
1390
|
+
delay.disable();
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// src/chronicle/decision-adapter.ts
|
|
1395
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
1396
|
+
function wireDecisionsToChronicle(options) {
|
|
1397
|
+
const write = (eventType, at, sessionId, requestId, attributes, outcome) => {
|
|
1398
|
+
const context = typeof options.context === "function" ? options.context() : options.context;
|
|
1399
|
+
const input = {
|
|
1400
|
+
eventType,
|
|
1401
|
+
occurredAt: new Date(at).toISOString(),
|
|
1402
|
+
outcome,
|
|
1403
|
+
scope: { ...context.scope, ...sessionId ? { sessionId } : {} },
|
|
1404
|
+
correlation: context.correlation,
|
|
1405
|
+
resource: { kind: "other", id: `decision:${requestId}` },
|
|
1406
|
+
attributes: { decisionId: requestId, ...attributes }
|
|
1407
|
+
};
|
|
1408
|
+
void options.journal.append(input).catch((error) => options.onPersistError?.(error, input));
|
|
1409
|
+
};
|
|
1410
|
+
const requestAttrs = (request) => ({
|
|
1411
|
+
source: request.source,
|
|
1412
|
+
risk: request.risk,
|
|
1413
|
+
fallback: request.fallback,
|
|
1414
|
+
questionHash: hash(request.question),
|
|
1415
|
+
contextHash: hash(request.context),
|
|
1416
|
+
optionCount: request.options?.length ?? 0,
|
|
1417
|
+
options: request.options?.map((option) => ({
|
|
1418
|
+
id: option.id,
|
|
1419
|
+
risk: option.risk,
|
|
1420
|
+
recommended: option.recommended ?? false,
|
|
1421
|
+
labelHash: hash(option.label),
|
|
1422
|
+
consequenceHash: hash(option.consequence)
|
|
1423
|
+
}))
|
|
1424
|
+
});
|
|
1425
|
+
const decisionAttrs = (decision) => ({
|
|
1426
|
+
type: decision.type,
|
|
1427
|
+
..."optionId" in decision && decision.optionId ? { optionId: decision.optionId } : {},
|
|
1428
|
+
contentHash: hash("text" in decision ? decision.text : "prompt" in decision ? decision.prompt : decision.reason),
|
|
1429
|
+
rationaleHash: hash("rationale" in decision ? decision.rationale : void 0)
|
|
1430
|
+
});
|
|
1431
|
+
const offs = [
|
|
1432
|
+
options.events.on("brain.decision_requested", (e) => write("decision.requested", e.at, e.sessionId, e.request.id, requestAttrs(e.request), "started")),
|
|
1433
|
+
options.events.on("brain.decision_answered", (e) => write("decision.resolved", e.at, e.sessionId, e.request.id, { ...requestAttrs(e.request), ...decisionAttrs(e.decision), resolver: "brain" }, "success")),
|
|
1434
|
+
options.events.on("brain.decision_ask_human", (e) => write("decision.escalated", e.at, e.sessionId, e.request.id, { ...requestAttrs(e.request), ...decisionAttrs(e.decision) }, "started")),
|
|
1435
|
+
options.events.on("brain.decision_denied", (e) => write("decision.denied", e.at, e.sessionId, e.request.id, { ...requestAttrs(e.request), ...decisionAttrs(e.decision) }, "denied")),
|
|
1436
|
+
options.events.on("brain.human_answered", (e) => write("decision.human_answered", e.at, e.sessionId, e.id, { resolver: "human", optionId: e.optionId, denied: e.deny ?? false, answerHash: hash(e.text) }, e.deny ? "denied" : "success")),
|
|
1437
|
+
options.events.on("brain.outcome", (e) => write("decision.outcome_observed", e.at, e.sessionId, e.requestId, { observedOutcome: e.outcome, detailHash: hash(e.detail) }, e.outcome))
|
|
1438
|
+
];
|
|
1439
|
+
return () => offs.forEach((off) => {
|
|
1440
|
+
off();
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
function hash(value) {
|
|
1444
|
+
return value ? createHash6("sha256").update(value).digest("hex") : void 0;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// src/chronicle/domain-adapter.ts
|
|
1448
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
1449
|
+
var SPECIALIZED = [
|
|
1450
|
+
/^provider\.attempt\./,
|
|
1451
|
+
/^tool\./,
|
|
1452
|
+
/^process\./,
|
|
1453
|
+
/^brain\.decision_/,
|
|
1454
|
+
/^brain\.human_answered$/,
|
|
1455
|
+
/^brain\.outcome$/,
|
|
1456
|
+
/^file\.activity$/,
|
|
1457
|
+
/^provider\.(?:text_delta|thinking_delta)$/,
|
|
1458
|
+
/^(?:ctx\.pct|subagent\.ctx_pct|countdown\.tick|coordinator\.stats)$/
|
|
1459
|
+
];
|
|
1460
|
+
var CODING_SIGNAL = [
|
|
1461
|
+
/^(?:agent|subagent|delegate|fleet)\./,
|
|
1462
|
+
/^(?:session|iteration|context|compaction|checkpoint|in_flight)\./,
|
|
1463
|
+
/^(?:memory|storage|trust)\./,
|
|
1464
|
+
/^(?:sdd|worktree)\./,
|
|
1465
|
+
/^(?:brain|token|budget|concurrency)\./,
|
|
1466
|
+
/^(?:provider|mcp|network)\./,
|
|
1467
|
+
/^error$/
|
|
1468
|
+
];
|
|
1469
|
+
var SENSITIVE_KEY = /(content|text|prompt|question|rationale|reason|detail|summary|description|context|input|output|error|message|secret|token|password|key)$/i;
|
|
1470
|
+
var PRESERVE_STRING_KEY = /(^|_)(id|status|state|kind|type|source|model|provider|phase|risk|fallback|path|name|role|sha|branch|mode)$/i;
|
|
1471
|
+
var TRUNCATED_ARRAYS = /* @__PURE__ */ new Set(["recentMail", "recentTools", "recentCommands"]);
|
|
1472
|
+
var TRUNCATED_ARRAY_MAX = 5;
|
|
1473
|
+
var DEFAULT_ARRAY_MAX = 20;
|
|
1474
|
+
function wireDomainEventsToChronicle(options) {
|
|
1475
|
+
return options.events.onAny((eventName, payload) => {
|
|
1476
|
+
if (SPECIALIZED.some((pattern) => pattern.test(eventName))) return;
|
|
1477
|
+
if (!CODING_SIGNAL.some((pattern) => pattern.test(eventName))) return;
|
|
1478
|
+
const context = typeof options.context === "function" ? options.context() : options.context;
|
|
1479
|
+
const record = objectPayload(payload);
|
|
1480
|
+
const sessionId = stringField(record, "sessionId");
|
|
1481
|
+
const agentId = stringField(record, "agentId") ?? stringField(record, "subagentId");
|
|
1482
|
+
const taskId = stringField(record, "taskId");
|
|
1483
|
+
const input = {
|
|
1484
|
+
eventType: eventName,
|
|
1485
|
+
occurredAt: eventTime(record),
|
|
1486
|
+
scope: { ...context.scope, ...sessionId ? { sessionId } : {}, ...agentId ? { agentId } : {}, ...taskId ? { taskId } : {} },
|
|
1487
|
+
correlation: {
|
|
1488
|
+
...context.correlation,
|
|
1489
|
+
...stringField(record, "traceId") ? { traceId: stringField(record, "traceId") } : {},
|
|
1490
|
+
...stringField(record, "toolCallId") ? { toolCallId: stringField(record, "toolCallId") } : {},
|
|
1491
|
+
...stringField(record, "attemptId") ? { attemptId: stringField(record, "attemptId") } : {},
|
|
1492
|
+
...stringField(record, "logicalRequestId") ? { logicalRequestId: stringField(record, "logicalRequestId") } : {}
|
|
1493
|
+
},
|
|
1494
|
+
outcome: inferOutcome(eventName, record),
|
|
1495
|
+
runtime: {
|
|
1496
|
+
...stringField(record, "providerId") ?? stringField(record, "provider") ? { providerId: stringField(record, "providerId") ?? stringField(record, "provider") } : {},
|
|
1497
|
+
...stringField(record, "modelId") ?? stringField(record, "model") ? { modelId: stringField(record, "modelId") ?? stringField(record, "model") } : {}
|
|
1498
|
+
},
|
|
1499
|
+
resource: inferResource(record),
|
|
1500
|
+
attributes: sanitize(record),
|
|
1501
|
+
tags: { collector: "eventbus-domain", family: eventName.split(".")[0] ?? "unknown" }
|
|
1502
|
+
};
|
|
1503
|
+
void options.journal.append(input).catch((error) => options.onPersistError?.(error, input));
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
function objectPayload(value) {
|
|
1507
|
+
return value && typeof value === "object" ? value : { value };
|
|
1508
|
+
}
|
|
1509
|
+
function stringField(value, key) {
|
|
1510
|
+
return typeof value[key] === "string" ? value[key] : void 0;
|
|
1511
|
+
}
|
|
1512
|
+
function eventTime(value) {
|
|
1513
|
+
const raw = value.at ?? value.ts ?? value.timestamp;
|
|
1514
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return new Date(raw).toISOString();
|
|
1515
|
+
if (typeof raw === "string" && Number.isFinite(Date.parse(raw))) return new Date(raw).toISOString();
|
|
1516
|
+
return void 0;
|
|
1517
|
+
}
|
|
1518
|
+
function inferOutcome(name, payload) {
|
|
1519
|
+
if (payload.ok === false || /(?:failed|error|damaged|conflict|deadlock|denied|rejected|blocked)$/.test(name)) return "failure";
|
|
1520
|
+
if (/(?:started|starting|retrying|threshold_reached)$/.test(name)) return "started";
|
|
1521
|
+
if (/(?:cancelled|aborted)$/.test(name)) return "cancelled";
|
|
1522
|
+
if (/(?:completed|finished|committed|merged|written|persisted|accepted|recovered|verified|connected|success)$/.test(name) || payload.ok === true) return "success";
|
|
1523
|
+
return "unknown";
|
|
1524
|
+
}
|
|
1525
|
+
function inferResource(payload) {
|
|
1526
|
+
const candidates = [
|
|
1527
|
+
["memory", "memoryId", payload.memoryId],
|
|
1528
|
+
["task", "taskId", payload.taskId],
|
|
1529
|
+
["kanban", "boardId", payload.boardId ?? payload.runId],
|
|
1530
|
+
["artifact", "worktreeId", payload.worktreeId ?? payload.handleId],
|
|
1531
|
+
["file", "path", payload.filePath ?? payload.path],
|
|
1532
|
+
["other", "agentId", payload.agentId ?? payload.subagentId],
|
|
1533
|
+
["network", "serverAddress", payload.serverAddress],
|
|
1534
|
+
["other", "sessionId", payload.sessionId]
|
|
1535
|
+
];
|
|
1536
|
+
const found = candidates.find(([, , value2]) => typeof value2 === "string" && value2.length > 0);
|
|
1537
|
+
if (!found) return void 0;
|
|
1538
|
+
const [kind, label, value] = found;
|
|
1539
|
+
return { kind, id: `${label}:${value}`, ...kind === "file" ? { path: value } : {} };
|
|
1540
|
+
}
|
|
1541
|
+
function sanitize(value, key = "", depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
1542
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") return value;
|
|
1543
|
+
if (typeof value === "bigint") return value.toString();
|
|
1544
|
+
if (typeof value === "function") return { type: "function" };
|
|
1545
|
+
if (typeof value === "string") {
|
|
1546
|
+
if (PRESERVE_STRING_KEY.test(key) && !SENSITIVE_KEY.test(key)) return value.slice(0, 512);
|
|
1547
|
+
if (SENSITIVE_KEY.test(key)) return { hash: digest(value), length: value.length, redacted: true };
|
|
1548
|
+
return value.length <= 256 ? value : { hash: digest(value), length: value.length, truncated: true };
|
|
1549
|
+
}
|
|
1550
|
+
if (typeof value !== "object") return String(value);
|
|
1551
|
+
if (seen.has(value)) return { circular: true };
|
|
1552
|
+
if (depth >= 5) return { hash: digest(safeString(value)), depthLimited: true };
|
|
1553
|
+
seen.add(value);
|
|
1554
|
+
if (Array.isArray(value)) {
|
|
1555
|
+
const cap = TRUNCATED_ARRAYS.has(key) ? TRUNCATED_ARRAY_MAX : DEFAULT_ARRAY_MAX;
|
|
1556
|
+
const items = value.slice(0, cap).map((item) => sanitize(item, key, depth + 1, seen));
|
|
1557
|
+
return value.length > cap ? { items, total: value.length, truncated: true } : items;
|
|
1558
|
+
}
|
|
1559
|
+
const output = {};
|
|
1560
|
+
const entries = Object.entries(value);
|
|
1561
|
+
for (const [childKey, child] of entries.slice(0, 100)) {
|
|
1562
|
+
if (childKey === "ctx" || childKey === "provider" || childKey === "resolve" || childKey === "extend" || childKey === "deny") continue;
|
|
1563
|
+
output[childKey] = sanitize(child, childKey, depth + 1, seen);
|
|
1564
|
+
}
|
|
1565
|
+
if (entries.length > 100) output._truncatedKeys = entries.length - 100;
|
|
1566
|
+
return output;
|
|
1567
|
+
}
|
|
1568
|
+
function safeString(value) {
|
|
1569
|
+
try {
|
|
1570
|
+
return JSON.stringify(value) ?? String(value);
|
|
1571
|
+
} catch {
|
|
1572
|
+
return String(value);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
function digest(value) {
|
|
1576
|
+
return createHash7("sha256").update(value).digest("hex");
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/chronicle/stream-adapter.ts
|
|
1580
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
1581
|
+
function wireProviderStreamsToChronicle(options) {
|
|
1582
|
+
const states = /* @__PURE__ */ new Map();
|
|
1583
|
+
const key = (sessionId, agentId) => `${sessionId ?? "__default__"}\0${agentId ?? "__leader__"}`;
|
|
1584
|
+
const update = (sessionId, agentId, text2, thinking) => {
|
|
1585
|
+
const state = states.get(key(sessionId, agentId));
|
|
1586
|
+
if (!state) return;
|
|
1587
|
+
const now = Date.now();
|
|
1588
|
+
const bytes = Buffer.byteLength(text2);
|
|
1589
|
+
state.firstChunkAtMs ??= now;
|
|
1590
|
+
state.lastChunkAtMs = now;
|
|
1591
|
+
if (thinking) {
|
|
1592
|
+
state.thinkingChunks++;
|
|
1593
|
+
state.thinkingBytes += bytes;
|
|
1594
|
+
state.thinkingHash.update(text2);
|
|
1595
|
+
} else {
|
|
1596
|
+
state.textChunks++;
|
|
1597
|
+
state.textBytes += bytes;
|
|
1598
|
+
state.textHash.update(text2);
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
const flush = (sessionId, agentId, outcome) => {
|
|
1602
|
+
const state = states.get(key(sessionId, agentId));
|
|
1603
|
+
if (!state) return;
|
|
1604
|
+
states.delete(key(sessionId, agentId));
|
|
1605
|
+
const context = typeof options.context === "function" ? options.context() : options.context;
|
|
1606
|
+
const endedAtMs = Date.now();
|
|
1607
|
+
const input = {
|
|
1608
|
+
eventType: "provider.stream.summarized",
|
|
1609
|
+
outcome,
|
|
1610
|
+
scope: { ...context.scope, ...state.sessionId ? { sessionId: state.sessionId } : {}, ...state.agentId ? { agentId: state.agentId } : {} },
|
|
1611
|
+
correlation: { ...context.correlation, attemptId: state.attemptId, logicalRequestId: state.logicalRequestId },
|
|
1612
|
+
runtime: { providerId: state.providerId, modelId: state.model },
|
|
1613
|
+
durationNs: String(Math.max(0, endedAtMs - state.startedAtMs) * 1e6),
|
|
1614
|
+
attributes: {
|
|
1615
|
+
textChunks: state.textChunks,
|
|
1616
|
+
textBytes: state.textBytes,
|
|
1617
|
+
thinkingChunks: state.thinkingChunks,
|
|
1618
|
+
thinkingBytes: state.thinkingBytes,
|
|
1619
|
+
textHash: state.textHash.digest("hex"),
|
|
1620
|
+
thinkingHash: state.thinkingHash.digest("hex"),
|
|
1621
|
+
firstChunkLatencyMs: state.firstChunkAtMs === void 0 ? void 0 : state.firstChunkAtMs - state.startedAtMs,
|
|
1622
|
+
streamActiveMs: state.firstChunkAtMs === void 0 || state.lastChunkAtMs === void 0 ? 0 : state.lastChunkAtMs - state.firstChunkAtMs
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
void options.journal.append(input).catch((error) => options.onPersistError?.(error, input));
|
|
1626
|
+
};
|
|
1627
|
+
const offs = [
|
|
1628
|
+
options.events.on("provider.attempt.started", (event) => states.set(key(event.sessionId, event.agentId), {
|
|
1629
|
+
sessionId: event.sessionId,
|
|
1630
|
+
...event.agentId ? { agentId: event.agentId } : {},
|
|
1631
|
+
attemptId: event.attemptId,
|
|
1632
|
+
logicalRequestId: event.logicalRequestId,
|
|
1633
|
+
providerId: event.providerId,
|
|
1634
|
+
model: event.model,
|
|
1635
|
+
startedAtMs: Date.parse(event.startedAt),
|
|
1636
|
+
textChunks: 0,
|
|
1637
|
+
textBytes: 0,
|
|
1638
|
+
thinkingChunks: 0,
|
|
1639
|
+
thinkingBytes: 0,
|
|
1640
|
+
textHash: createHash8("sha256"),
|
|
1641
|
+
thinkingHash: createHash8("sha256")
|
|
1642
|
+
})),
|
|
1643
|
+
options.events.on("provider.text_delta", (event) => update(event.sessionId, event.ctx.agentId, event.text, false)),
|
|
1644
|
+
options.events.on("provider.thinking_delta", (event) => update(event.sessionId, event.ctx.agentId, event.text, true)),
|
|
1645
|
+
options.events.on("provider.attempt.completed", (event) => flush(event.sessionId, event.agentId, "success")),
|
|
1646
|
+
options.events.on("provider.attempt.failed", (event) => flush(event.sessionId, event.agentId, "failure"))
|
|
1647
|
+
];
|
|
1648
|
+
return () => {
|
|
1649
|
+
for (const state of [...states.values()]) flush(state.sessionId, state.agentId, "failure");
|
|
1650
|
+
offs.forEach((off) => {
|
|
1651
|
+
off();
|
|
1652
|
+
});
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/chronicle/prompt-manifest.ts
|
|
1657
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
1658
|
+
function createChroniclePromptManifest(request) {
|
|
1659
|
+
const roleCounts = {}, blockCounts = {};
|
|
1660
|
+
const messages = request.messages.map((message, index) => messageSummary(message, index, roleCounts, blockCounts));
|
|
1661
|
+
const systemText = (request.system ?? []).map((block) => block.text).join("\n");
|
|
1662
|
+
const toolRecords = (request.tools ?? []).map((tool) => ({
|
|
1663
|
+
name: tool.name,
|
|
1664
|
+
schemaHash: hash2(stable(tool.inputSchema)),
|
|
1665
|
+
permission: tool.permission,
|
|
1666
|
+
mutating: tool.mutating,
|
|
1667
|
+
riskTier: tool.riskTier,
|
|
1668
|
+
capabilities: tool.capabilities,
|
|
1669
|
+
estimatedTokens: tool._estDefTokens ?? 0
|
|
1670
|
+
}));
|
|
1671
|
+
const core = {
|
|
1672
|
+
systemHash: hash2(systemText),
|
|
1673
|
+
messages: messages.map(({ hash: contentHash, ...rest }) => ({ ...rest, contentHash })),
|
|
1674
|
+
tools: toolRecords,
|
|
1675
|
+
model: request.model,
|
|
1676
|
+
maxTokens: request.maxTokens,
|
|
1677
|
+
temperature: request.temperature,
|
|
1678
|
+
topP: request.topP,
|
|
1679
|
+
topK: request.topK,
|
|
1680
|
+
seed: request.seed,
|
|
1681
|
+
toolChoice: request.toolChoice,
|
|
1682
|
+
reasoning: request.reasoning,
|
|
1683
|
+
cache: request.cache,
|
|
1684
|
+
responseFormat: request.responseFormat?.type
|
|
1685
|
+
};
|
|
1686
|
+
return {
|
|
1687
|
+
manifestId: `prompt_${hash2(stable(core))}`,
|
|
1688
|
+
messageCount: messages.length,
|
|
1689
|
+
estimatedMessageTokens: messages.reduce((sum, message) => sum + (message.estimatedTokens ?? 0), 0),
|
|
1690
|
+
contentBytes: messages.reduce((sum, message) => sum + message.bytes, 0) + Buffer.byteLength(systemText),
|
|
1691
|
+
roleCounts,
|
|
1692
|
+
blockCounts,
|
|
1693
|
+
system: { blockCount: request.system?.length ?? 0, bytes: Buffer.byteLength(systemText), hash: hash2(systemText) },
|
|
1694
|
+
messages,
|
|
1695
|
+
tools: {
|
|
1696
|
+
count: toolRecords.length,
|
|
1697
|
+
estimatedDefinitionTokens: toolRecords.reduce((sum, tool) => sum + tool.estimatedTokens, 0),
|
|
1698
|
+
manifestHash: hash2(stable(toolRecords)),
|
|
1699
|
+
names: toolRecords.map((tool) => tool.name),
|
|
1700
|
+
mutating: toolRecords.filter((tool) => tool.mutating).length,
|
|
1701
|
+
destructive: toolRecords.filter((tool) => tool.riskTier === "destructive").length
|
|
1702
|
+
},
|
|
1703
|
+
request: {
|
|
1704
|
+
maxTokens: request.maxTokens,
|
|
1705
|
+
temperature: request.temperature,
|
|
1706
|
+
topP: request.topP,
|
|
1707
|
+
topK: request.topK,
|
|
1708
|
+
frequencyPenalty: request.frequencyPenalty,
|
|
1709
|
+
presencePenalty: request.presencePenalty,
|
|
1710
|
+
seed: request.seed,
|
|
1711
|
+
candidateCount: request.candidateCount,
|
|
1712
|
+
logprobs: request.logprobs,
|
|
1713
|
+
topLogprobs: request.topLogprobs,
|
|
1714
|
+
stopSequenceCount: request.stopSequences?.length ?? 0,
|
|
1715
|
+
toolChoice: request.toolChoice,
|
|
1716
|
+
reasoning: request.reasoning,
|
|
1717
|
+
cache: request.cache,
|
|
1718
|
+
responseFormat: request.responseFormat?.type,
|
|
1719
|
+
safetySettingCount: request.safetySettings?.length ?? 0,
|
|
1720
|
+
userHash: request.user ? hash2(request.user) : void 0
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
function messageSummary(message, index, roles, totals) {
|
|
1725
|
+
roles[message.role] = (roles[message.role] ?? 0) + 1;
|
|
1726
|
+
const blocks = typeof message.content === "string" ? { text: 1 } : countBlocks(message.content);
|
|
1727
|
+
for (const [type, count] of Object.entries(blocks)) totals[type] = (totals[type] ?? 0) + count;
|
|
1728
|
+
const content = contentIdentity(message.content);
|
|
1729
|
+
return {
|
|
1730
|
+
index,
|
|
1731
|
+
role: message.role,
|
|
1732
|
+
bytes: content.bytes,
|
|
1733
|
+
...message._estTokens !== void 0 ? { estimatedTokens: message._estTokens } : {},
|
|
1734
|
+
hash: content.hash,
|
|
1735
|
+
blocks
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function countBlocks(blocks) {
|
|
1739
|
+
const counts = {};
|
|
1740
|
+
for (const block of blocks) counts[block.type] = (counts[block.type] ?? 0) + 1;
|
|
1741
|
+
return counts;
|
|
1742
|
+
}
|
|
1743
|
+
function hash2(value) {
|
|
1744
|
+
return createHash9("sha256").update(value).digest("hex");
|
|
1745
|
+
}
|
|
1746
|
+
function contentIdentity(content) {
|
|
1747
|
+
if (typeof content === "string") return { hash: hash2(content), bytes: Buffer.byteLength(content) };
|
|
1748
|
+
const digest2 = createHash9("sha256");
|
|
1749
|
+
let bytes = 0;
|
|
1750
|
+
const add = (value) => {
|
|
1751
|
+
if (!value) return;
|
|
1752
|
+
digest2.update(value);
|
|
1753
|
+
bytes += Buffer.byteLength(value);
|
|
1754
|
+
};
|
|
1755
|
+
for (const block of content) {
|
|
1756
|
+
add(block.type);
|
|
1757
|
+
if (block.type === "text") add(block.text);
|
|
1758
|
+
else if (block.type === "thinking") {
|
|
1759
|
+
add(block.thinking);
|
|
1760
|
+
add(block.signature);
|
|
1761
|
+
} else if (block.type === "tool_use") {
|
|
1762
|
+
add(block.id);
|
|
1763
|
+
add(block.name);
|
|
1764
|
+
add(stable(block.input));
|
|
1765
|
+
} else if (block.type === "tool_result") {
|
|
1766
|
+
add(block.tool_use_id);
|
|
1767
|
+
add(block.name);
|
|
1768
|
+
add(block.content);
|
|
1769
|
+
add(String(block.is_error ?? false));
|
|
1770
|
+
} else if (block.type === "image") {
|
|
1771
|
+
add(block.source.type);
|
|
1772
|
+
add(block.source.media_type);
|
|
1773
|
+
add(block.source.url);
|
|
1774
|
+
add(block.source.data);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return { hash: digest2.digest("hex"), bytes };
|
|
1778
|
+
}
|
|
1779
|
+
function stable(value) {
|
|
1780
|
+
if (value === void 0) return "undefined";
|
|
1781
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1782
|
+
if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
|
|
1783
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => `${JSON.stringify(key)}:${stable(child)}`).join(",")}}`;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// src/chronicle/rollup-adapter.ts
|
|
1787
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
1788
|
+
function wireRollupsToChronicle(options) {
|
|
1789
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1790
|
+
const windowMs = Math.max(1e3, options.windowMs ?? 1e4);
|
|
1791
|
+
const bucket = (key, seed) => {
|
|
1792
|
+
let value = buckets.get(key);
|
|
1793
|
+
if (!value) {
|
|
1794
|
+
const now = Date.now();
|
|
1795
|
+
value = {
|
|
1796
|
+
...seed,
|
|
1797
|
+
startedAt: now,
|
|
1798
|
+
updatedAt: now,
|
|
1799
|
+
count: 0,
|
|
1800
|
+
metrics: {},
|
|
1801
|
+
categories: {},
|
|
1802
|
+
digest: createHash10("sha256")
|
|
1803
|
+
};
|
|
1804
|
+
buckets.set(key, value);
|
|
1805
|
+
}
|
|
1806
|
+
return value;
|
|
1807
|
+
};
|
|
1808
|
+
const sample = (target, values, category, digest2) => {
|
|
1809
|
+
target.count++;
|
|
1810
|
+
target.updatedAt = Date.now();
|
|
1811
|
+
for (const [name, value] of Object.entries(values)) {
|
|
1812
|
+
const metric = target.metrics[name];
|
|
1813
|
+
target.metrics[name] = metric ? { sum: metric.sum + value, min: Math.min(metric.min, value), max: Math.max(metric.max, value), last: value } : { sum: value, min: value, max: value, last: value };
|
|
1814
|
+
}
|
|
1815
|
+
if (category) target.categories[category] = (target.categories[category] ?? 0) + 1;
|
|
1816
|
+
if (digest2) target.digest.update(digest2);
|
|
1817
|
+
};
|
|
1818
|
+
const flush = (key) => {
|
|
1819
|
+
const value = buckets.get(key);
|
|
1820
|
+
if (!value || value.count === 0) return;
|
|
1821
|
+
buckets.delete(key);
|
|
1822
|
+
const context = typeof options.context === "function" ? options.context() : options.context;
|
|
1823
|
+
const stats = Object.fromEntries(Object.entries(value.metrics).map(([name, metric]) => [name, { ...metric, avg: metric.sum / value.count }]));
|
|
1824
|
+
const input = {
|
|
1825
|
+
eventType: "metrics.rollup",
|
|
1826
|
+
outcome: "success",
|
|
1827
|
+
occurredAt: new Date(value.updatedAt).toISOString(),
|
|
1828
|
+
scope: { ...context.scope, ...value.sessionId ? { sessionId: value.sessionId } : {}, ...value.agentId ? { agentId: value.agentId } : {} },
|
|
1829
|
+
correlation: { ...context.correlation, ...value.toolCallId ? { toolCallId: value.toolCallId } : {} },
|
|
1830
|
+
durationNs: String(Math.max(0, value.updatedAt - value.startedAt) * 1e6),
|
|
1831
|
+
attributes: {
|
|
1832
|
+
signal: value.signal,
|
|
1833
|
+
windowStart: new Date(value.startedAt).toISOString(),
|
|
1834
|
+
windowEnd: new Date(value.updatedAt).toISOString(),
|
|
1835
|
+
samples: value.count,
|
|
1836
|
+
dimensions: value.dimensions,
|
|
1837
|
+
stats,
|
|
1838
|
+
categories: value.categories,
|
|
1839
|
+
digest: value.digest.digest("hex"),
|
|
1840
|
+
rawEventsRetained: false
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
void options.journal.append(input).catch((error) => options.onPersistError?.(error, input));
|
|
1844
|
+
};
|
|
1845
|
+
const gauge = (signal, event, dimension) => {
|
|
1846
|
+
const sessionId = text(event.sessionId);
|
|
1847
|
+
const dimensionValue = dimension ? text(event[dimension]) : void 0;
|
|
1848
|
+
const key = `${signal}\0${sessionId ?? ""}\0${dimensionValue ?? ""}`;
|
|
1849
|
+
const target = bucket(key, { signal, ...sessionId ? { sessionId } : {}, ...dimensionValue ? { agentId: dimensionValue } : {}, dimensions: dimensionValue && dimension ? { [dimension]: dimensionValue } : {} });
|
|
1850
|
+
sample(target, Object.fromEntries(Object.entries(event).filter(([, value]) => typeof value === "number")));
|
|
1851
|
+
};
|
|
1852
|
+
const offs = [
|
|
1853
|
+
options.events.on("process.output", (event) => {
|
|
1854
|
+
const key = `process.output\0${event.sessionId}\0${event.toolCallId}\0${event.pid ?? ""}\0${event.stream}`;
|
|
1855
|
+
const target = bucket(key, {
|
|
1856
|
+
signal: "process.output",
|
|
1857
|
+
sessionId: event.sessionId,
|
|
1858
|
+
...event.agentId ? { agentId: event.agentId } : {},
|
|
1859
|
+
toolCallId: event.toolCallId,
|
|
1860
|
+
dimensions: { stream: event.stream, toolName: event.toolName, pid: String(event.pid ?? "") }
|
|
1861
|
+
});
|
|
1862
|
+
sample(target, { bytes: event.bytes }, event.stream, event.chunkHash);
|
|
1863
|
+
}),
|
|
1864
|
+
options.events.on("process.completed", (event) => {
|
|
1865
|
+
for (const key of [...buckets.keys()]) if (key.startsWith(`process.output\0${event.sessionId}\0${event.toolCallId}\0`)) flush(key);
|
|
1866
|
+
}),
|
|
1867
|
+
options.events.on("tool.progress", (event) => {
|
|
1868
|
+
if (event.event.type === "file_changed") return;
|
|
1869
|
+
const key = `tool.progress\0${event.sessionId ?? ""}\0${event.id}`;
|
|
1870
|
+
const target = bucket(key, { signal: "tool.progress", ...event.sessionId ? { sessionId: event.sessionId } : {}, ...event.agentId ? { agentId: event.agentId } : {}, toolCallId: event.id, dimensions: { toolName: event.name } });
|
|
1871
|
+
sample(target, { textBytes: Buffer.byteLength(event.event.text ?? "") }, event.event.type, safeDigest(event.event));
|
|
1872
|
+
}),
|
|
1873
|
+
options.events.on("tool.executed", (event) => flush(`tool.progress\0${event.sessionId ?? ""}\0${event.id ?? ""}`)),
|
|
1874
|
+
options.events.on("tool.failed", (event) => flush(`tool.progress\0${event.sessionId}\0${event.id}`)),
|
|
1875
|
+
options.events.on("ctx.pct", (event) => gauge("ctx.pct", event)),
|
|
1876
|
+
options.events.on("subagent.ctx_pct", (event) => gauge("subagent.ctx_pct", event, "subagentId")),
|
|
1877
|
+
options.events.on("countdown.tick", (event) => gauge("countdown.tick", event)),
|
|
1878
|
+
options.events.on("coordinator.stats", (event) => gauge("coordinator.stats", event))
|
|
1879
|
+
];
|
|
1880
|
+
const timer = setInterval(() => {
|
|
1881
|
+
const cutoff = Date.now() - windowMs;
|
|
1882
|
+
for (const [key, value] of buckets) if (value.updatedAt <= cutoff) flush(key);
|
|
1883
|
+
}, windowMs);
|
|
1884
|
+
timer.unref?.();
|
|
1885
|
+
return () => {
|
|
1886
|
+
clearInterval(timer);
|
|
1887
|
+
for (const key of [...buckets.keys()]) flush(key);
|
|
1888
|
+
offs.forEach((off) => {
|
|
1889
|
+
off();
|
|
1890
|
+
});
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
function text(value) {
|
|
1894
|
+
return typeof value === "string" ? value : void 0;
|
|
1895
|
+
}
|
|
1896
|
+
function safeDigest(value) {
|
|
1897
|
+
try {
|
|
1898
|
+
return createHash10("sha256").update(JSON.stringify(value)).digest("hex");
|
|
1899
|
+
} catch {
|
|
1900
|
+
return "unhashable";
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// src/chronicle/query.ts
|
|
1905
|
+
import { createHash as createHash11 } from "node:crypto";
|
|
1906
|
+
import { createReadStream } from "node:fs";
|
|
1907
|
+
import * as fs4 from "node:fs/promises";
|
|
1908
|
+
import * as path5 from "node:path";
|
|
1909
|
+
import { createInterface } from "node:readline";
|
|
1910
|
+
var MAX_CURSOR_SNAPSHOT_ENTRIES = 1e4;
|
|
1911
|
+
function streamLines(filePath, maxBytes) {
|
|
1912
|
+
const stream = createReadStream(filePath, {
|
|
1913
|
+
encoding: "utf8",
|
|
1914
|
+
highWaterMark: 256 * 1024,
|
|
1915
|
+
...maxBytes !== void 0 ? { end: maxBytes - 1 } : {}
|
|
1916
|
+
});
|
|
1917
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
1918
|
+
return rl[Symbol.asyncIterator]();
|
|
1919
|
+
}
|
|
1920
|
+
var ChronicleQueryEngine = class _ChronicleQueryEngine {
|
|
1921
|
+
constructor(files, diagnostics) {
|
|
1922
|
+
this.diagnostics = diagnostics;
|
|
1923
|
+
this.partitionFiles = files;
|
|
1924
|
+
}
|
|
1925
|
+
diagnostics;
|
|
1926
|
+
partitionFiles;
|
|
1927
|
+
static async fromDirectory(directory) {
|
|
1928
|
+
const files = await findPartitions(path5.resolve(directory));
|
|
1929
|
+
return new _ChronicleQueryEngine(files, { sourceFiles: files.length, invalidLines: 0 });
|
|
1930
|
+
}
|
|
1931
|
+
static async fromFiles(files) {
|
|
1932
|
+
return new _ChronicleQueryEngine(files, { sourceFiles: files.length, invalidLines: 0 });
|
|
1933
|
+
}
|
|
1934
|
+
/** Stream all partitions, filter, and return the requested page + summary. */
|
|
1935
|
+
async query(query = {}) {
|
|
1936
|
+
const order = query.order ?? "desc";
|
|
1937
|
+
const limit = Math.max(1, Math.min(query.limit ?? 100, 1e4));
|
|
1938
|
+
const queryHash = hashQuery(query);
|
|
1939
|
+
const cursor = decodeCursor(query.cursor, order, queryHash);
|
|
1940
|
+
const snapshotFiles = cursor ? await resolveSnapshotFiles(this.partitionFiles, cursor.snapshot) : await captureSnapshot(this.partitionFiles);
|
|
1941
|
+
const files = order === "asc" ? snapshotFiles : snapshotFiles.slice().reverse();
|
|
1942
|
+
const summaryAcc = createSummaryAccumulator();
|
|
1943
|
+
const orderedCandidates = [];
|
|
1944
|
+
const pageOrder = (left, right) => compareEvents(left, right) * (order === "asc" ? 1 : -1);
|
|
1945
|
+
let totalCount = 0;
|
|
1946
|
+
let remainingCount = 0;
|
|
1947
|
+
let scannedEvents = 0;
|
|
1948
|
+
let invalidLines = 0;
|
|
1949
|
+
for (const snapshotFile of files) {
|
|
1950
|
+
try {
|
|
1951
|
+
if (snapshotFile.size === 0) continue;
|
|
1952
|
+
const lines = order === "asc" ? streamLines(snapshotFile.file, snapshotFile.size) : reverseLines(snapshotFile.file, snapshotFile.size);
|
|
1953
|
+
for await (const line of lines) {
|
|
1954
|
+
if (!line.trim()) continue;
|
|
1955
|
+
let event;
|
|
1956
|
+
try {
|
|
1957
|
+
event = JSON.parse(line);
|
|
1958
|
+
if (!isChronicleEvent(event)) {
|
|
1959
|
+
invalidLines++;
|
|
1960
|
+
continue;
|
|
1961
|
+
}
|
|
1962
|
+
} catch {
|
|
1963
|
+
invalidLines++;
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
scannedEvents++;
|
|
1967
|
+
if (!matches(event, query)) continue;
|
|
1968
|
+
totalCount++;
|
|
1969
|
+
updateSummary(summaryAcc, event);
|
|
1970
|
+
if (cursor && compareEventToKey(event, cursor.after) * (order === "asc" ? 1 : -1) <= 0) {
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
remainingCount++;
|
|
1974
|
+
const insertionIndex = findInsertionIndex(orderedCandidates, event, pageOrder);
|
|
1975
|
+
if (insertionIndex < limit) {
|
|
1976
|
+
orderedCandidates.splice(insertionIndex, 0, event);
|
|
1977
|
+
if (orderedCandidates.length > limit) orderedCandidates.pop();
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
} catch {
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
const pageEvents = orderedCandidates;
|
|
1984
|
+
const lastEvent = pageEvents.at(-1);
|
|
1985
|
+
return {
|
|
1986
|
+
events: pageEvents,
|
|
1987
|
+
total: totalCount,
|
|
1988
|
+
summary: finalizeSummary(summaryAcc),
|
|
1989
|
+
...lastEvent && pageEvents.length < remainingCount ? { nextCursor: encodeCursor({
|
|
1990
|
+
version: 1,
|
|
1991
|
+
order,
|
|
1992
|
+
queryHash,
|
|
1993
|
+
after: orderKey(lastEvent),
|
|
1994
|
+
snapshot: snapshotFiles.map(({ id, size }) => ({ id, size }))
|
|
1995
|
+
}) } : {},
|
|
1996
|
+
scannedEvents,
|
|
1997
|
+
sourceFiles: snapshotFiles.length,
|
|
1998
|
+
invalidLines
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
/** Stream all partitions and compute facet value counts. */
|
|
2002
|
+
async facet(field, query = {}, limit = 100) {
|
|
2003
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2004
|
+
let invalidLines = 0;
|
|
2005
|
+
for (const file of this.partitionFiles) {
|
|
2006
|
+
try {
|
|
2007
|
+
for await (const line of streamLines(file)) {
|
|
2008
|
+
if (!line.trim()) continue;
|
|
2009
|
+
let event;
|
|
2010
|
+
try {
|
|
2011
|
+
event = JSON.parse(line);
|
|
2012
|
+
if (!isChronicleEvent(event)) {
|
|
2013
|
+
invalidLines++;
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
} catch {
|
|
2017
|
+
invalidLines++;
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
if (!matches(event, query)) continue;
|
|
2021
|
+
const value = facetValue(event, field);
|
|
2022
|
+
if (value !== void 0) counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
2023
|
+
}
|
|
2024
|
+
} catch {
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
this.diagnostics.invalidLines = invalidLines;
|
|
2028
|
+
return [...counts].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value)).slice(0, Math.max(0, limit));
|
|
2029
|
+
}
|
|
2030
|
+
/** Expand explicit and typed correlation edges; temporal proximity alone never creates causality. */
|
|
2031
|
+
async graph(seed = {}, hops = 2, maxNodes = 1e3) {
|
|
2032
|
+
const nodeLimit = Math.max(0, Math.floor(maxNodes));
|
|
2033
|
+
const selected = /* @__PURE__ */ new Map();
|
|
2034
|
+
let seedCount = 0;
|
|
2035
|
+
for await (const event of streamEvents(this.partitionFiles)) {
|
|
2036
|
+
if (!matches(event, seed)) continue;
|
|
2037
|
+
seedCount++;
|
|
2038
|
+
if (selected.size < nodeLimit) selected.set(event.eventId, event);
|
|
2039
|
+
}
|
|
2040
|
+
let frontier = [...selected.values()];
|
|
2041
|
+
const depthLimit = Math.max(0, Math.min(hops, 10));
|
|
2042
|
+
for (let depth = 0; depth < depthLimit && frontier.length > 0 && selected.size < nodeLimit; depth++) {
|
|
2043
|
+
const frontierKeys = new Set(frontier.flatMap((event) => relationKeys(event).map((relation) => relation.key)));
|
|
2044
|
+
const next = [];
|
|
2045
|
+
for await (const event of streamEvents(this.partitionFiles)) {
|
|
2046
|
+
if (selected.has(event.eventId)) continue;
|
|
2047
|
+
if (!relationKeys(event).some((relation) => frontierKeys.has(relation.key))) continue;
|
|
2048
|
+
selected.set(event.eventId, event);
|
|
2049
|
+
next.push(event);
|
|
2050
|
+
if (selected.size >= nodeLimit) break;
|
|
2051
|
+
}
|
|
2052
|
+
frontier = next;
|
|
2053
|
+
}
|
|
2054
|
+
const nodes = [...selected.values()].sort(compareEvents);
|
|
2055
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2056
|
+
for (const node of nodes) for (const relation of relationKeys(node)) {
|
|
2057
|
+
const related = byKey.get(relation.key) ?? [];
|
|
2058
|
+
related.push(node);
|
|
2059
|
+
byKey.set(relation.key, related);
|
|
2060
|
+
}
|
|
2061
|
+
const edges = [];
|
|
2062
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2063
|
+
for (const node of nodes) for (const relation of relationKeys(node)) for (const candidate of byKey.get(relation.key) ?? []) {
|
|
2064
|
+
if (candidate.eventId === node.eventId) continue;
|
|
2065
|
+
const [from, to] = compareEvents(node, candidate) <= 0 ? [node, candidate] : [candidate, node];
|
|
2066
|
+
const id = `${from.eventId}:${to.eventId}:${relation.kind}`;
|
|
2067
|
+
if (!seen.has(id)) {
|
|
2068
|
+
seen.add(id);
|
|
2069
|
+
edges.push({ from: from.eventId, to: to.eventId, kind: relation.kind, confidence: relation.confidence });
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
return { nodes, edges, truncated: seedCount > nodeLimit || selected.size >= nodeLimit };
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
async function* streamEvents(files) {
|
|
2076
|
+
for (const file of files) {
|
|
2077
|
+
try {
|
|
2078
|
+
for await (const line of streamLines(file)) {
|
|
2079
|
+
if (!line.trim()) continue;
|
|
2080
|
+
try {
|
|
2081
|
+
const event = JSON.parse(line);
|
|
2082
|
+
if (isChronicleEvent(event)) yield event;
|
|
2083
|
+
} catch {
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
} catch {
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
async function* reverseLines(filePath, maxBytes) {
|
|
2091
|
+
const CHUNK = 64 * 1024;
|
|
2092
|
+
const NEWLINE = 10;
|
|
2093
|
+
let handle;
|
|
2094
|
+
try {
|
|
2095
|
+
handle = await fs4.open(filePath, "r");
|
|
2096
|
+
} catch {
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
try {
|
|
2100
|
+
const fileSize = (await handle.stat()).size;
|
|
2101
|
+
const size = Math.min(fileSize, maxBytes ?? fileSize);
|
|
2102
|
+
let position = size;
|
|
2103
|
+
let suffix = Buffer.alloc(0);
|
|
2104
|
+
while (position > 0) {
|
|
2105
|
+
const length = Math.min(CHUNK, position);
|
|
2106
|
+
position -= length;
|
|
2107
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
2108
|
+
await handle.read(buffer, 0, length, position);
|
|
2109
|
+
const data = suffix.length === 0 ? buffer : Buffer.concat([buffer, suffix]);
|
|
2110
|
+
let lineEnd = data.length;
|
|
2111
|
+
let firstNewline = -1;
|
|
2112
|
+
for (let index = data.length - 1; index >= 0; index--) {
|
|
2113
|
+
if (data[index] !== NEWLINE) continue;
|
|
2114
|
+
const trimmed2 = data.subarray(index + 1, lineEnd).toString("utf8").trim();
|
|
2115
|
+
if (trimmed2) yield trimmed2;
|
|
2116
|
+
lineEnd = index;
|
|
2117
|
+
firstNewline = index;
|
|
2118
|
+
}
|
|
2119
|
+
suffix = firstNewline >= 0 ? Buffer.from(data.subarray(0, firstNewline)) : data;
|
|
2120
|
+
}
|
|
2121
|
+
const trimmed = suffix.toString("utf8").trim();
|
|
2122
|
+
if (trimmed) yield trimmed;
|
|
2123
|
+
} finally {
|
|
2124
|
+
await handle.close();
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
function createSummaryAccumulator() {
|
|
2128
|
+
return {
|
|
2129
|
+
logicalRequestIds: /* @__PURE__ */ new Set(),
|
|
2130
|
+
modelAttempts: 0,
|
|
2131
|
+
completedAttempts: 0,
|
|
2132
|
+
failedAttempts: 0,
|
|
2133
|
+
scheduledRetries: 0,
|
|
2134
|
+
fallbacks: 0,
|
|
2135
|
+
providers: /* @__PURE__ */ new Set(),
|
|
2136
|
+
models: /* @__PURE__ */ new Set(),
|
|
2137
|
+
inputTokens: 0,
|
|
2138
|
+
outputTokens: 0,
|
|
2139
|
+
cacheReadTokens: 0,
|
|
2140
|
+
cacheWriteTokens: 0,
|
|
2141
|
+
costByScope: /* @__PURE__ */ new Map(),
|
|
2142
|
+
providerDurations: [],
|
|
2143
|
+
toolCalls: 0,
|
|
2144
|
+
completedTools: 0,
|
|
2145
|
+
failedTools: 0,
|
|
2146
|
+
toolDurations: [],
|
|
2147
|
+
processes: 0,
|
|
2148
|
+
failedProcesses: 0,
|
|
2149
|
+
fileEvents: 0,
|
|
2150
|
+
uniqueFiles: /* @__PURE__ */ new Set(),
|
|
2151
|
+
agentEvents: 0,
|
|
2152
|
+
uniqueAgents: /* @__PURE__ */ new Set(),
|
|
2153
|
+
decisions: 0,
|
|
2154
|
+
escalations: 0,
|
|
2155
|
+
failures: 0,
|
|
2156
|
+
cancellations: 0,
|
|
2157
|
+
families: { llm: 0, agent: 0, tool: 0, file: 0, memory: 0, task: 0, decision: 0, runtime: 0 },
|
|
2158
|
+
failuresByFamily: { llm: 0, agent: 0, tool: 0, file: 0, memory: 0, task: 0, decision: 0, runtime: 0 }
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
function updateSummary(acc, event) {
|
|
2162
|
+
const family = signalFamily(event);
|
|
2163
|
+
acc.families[family]++;
|
|
2164
|
+
if (isTerminalFailure(event)) acc.failuresByFamily[family]++;
|
|
2165
|
+
if (event.correlation.logicalRequestId) acc.logicalRequestIds.add(event.correlation.logicalRequestId);
|
|
2166
|
+
if (event.runtime?.providerId) acc.providers.add(event.runtime.providerId);
|
|
2167
|
+
if (event.runtime?.modelId) acc.models.add(event.runtime.modelId);
|
|
2168
|
+
if (event.eventType === "provider.attempt.started") acc.modelAttempts++;
|
|
2169
|
+
else if (event.eventType === "provider.attempt.completed") {
|
|
2170
|
+
acc.completedAttempts++;
|
|
2171
|
+
acc.inputTokens += numberAt(event, "usage.input");
|
|
2172
|
+
acc.outputTokens += numberAt(event, "usage.output");
|
|
2173
|
+
acc.cacheReadTokens += numberAt(event, "usage.cacheRead");
|
|
2174
|
+
acc.cacheWriteTokens += numberAt(event, "usage.cacheWrite");
|
|
2175
|
+
const dur = durationMs(event);
|
|
2176
|
+
if (dur > 0) acc.providerDurations.push(dur);
|
|
2177
|
+
} else if (event.eventType === "provider.attempt.failed") {
|
|
2178
|
+
acc.failedAttempts++;
|
|
2179
|
+
if (event.attributes?.retryScheduled === true) acc.scheduledRetries++;
|
|
2180
|
+
const dur = durationMs(event);
|
|
2181
|
+
if (dur > 0) acc.providerDurations.push(dur);
|
|
2182
|
+
} else if (event.eventType === "provider.fallback") acc.fallbacks++;
|
|
2183
|
+
else if (event.eventType === "tool.started") acc.toolCalls++;
|
|
2184
|
+
else if (event.eventType === "tool.executed") {
|
|
2185
|
+
acc.completedTools++;
|
|
2186
|
+
const dur = durationMs(event);
|
|
2187
|
+
if (dur > 0) acc.toolDurations.push(dur);
|
|
2188
|
+
} else if (event.eventType === "tool.failed") {
|
|
2189
|
+
acc.failedTools++;
|
|
2190
|
+
const dur = durationMs(event);
|
|
2191
|
+
if (dur > 0) acc.toolDurations.push(dur);
|
|
2192
|
+
} else if (event.eventType === "process.started") acc.processes++;
|
|
2193
|
+
else if (event.eventType === "process.completed" && event.outcome === "failure") acc.failedProcesses++;
|
|
2194
|
+
else if (event.eventType === "decision.requested") acc.decisions++;
|
|
2195
|
+
else if (event.eventType === "decision.escalated") acc.escalations++;
|
|
2196
|
+
if (event.eventType === "token.accounted") {
|
|
2197
|
+
const cost = readPath(event.attributes ?? {}, "cost.total");
|
|
2198
|
+
if (typeof cost === "number" && Number.isFinite(cost)) {
|
|
2199
|
+
const key = scopeKey(event);
|
|
2200
|
+
const existing = acc.costByScope.get(key);
|
|
2201
|
+
if (!existing || compareEvents(event, existing.event) > 0) {
|
|
2202
|
+
acc.costByScope.set(key, { cost, event });
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
if (event.resource?.kind === "file" || event.eventType.startsWith("file.")) {
|
|
2207
|
+
acc.fileEvents++;
|
|
2208
|
+
if (event.resource?.path) acc.uniqueFiles.add(event.resource.path);
|
|
2209
|
+
}
|
|
2210
|
+
if (family === "agent") acc.agentEvents++;
|
|
2211
|
+
if (event.scope.agentId) acc.uniqueAgents.add(event.scope.agentId);
|
|
2212
|
+
if (isTerminalFailure(event)) acc.failures++;
|
|
2213
|
+
if (event.outcome === "cancelled" || event.outcome === "abandoned") acc.cancellations++;
|
|
2214
|
+
}
|
|
2215
|
+
function finalizeSummary(acc) {
|
|
2216
|
+
const sortedProviderDurations = acc.providerDurations.slice().sort((a, b) => a - b);
|
|
2217
|
+
const sortedToolDurations = acc.toolDurations.slice().sort((a, b) => a - b);
|
|
2218
|
+
const totalCost = [...acc.costByScope.values()].reduce((sum, entry) => sum + entry.cost, 0);
|
|
2219
|
+
return {
|
|
2220
|
+
logicalRequests: acc.logicalRequestIds.size,
|
|
2221
|
+
modelAttempts: acc.modelAttempts,
|
|
2222
|
+
completedAttempts: acc.completedAttempts,
|
|
2223
|
+
failedAttempts: acc.failedAttempts,
|
|
2224
|
+
scheduledRetries: acc.scheduledRetries,
|
|
2225
|
+
fallbacks: acc.fallbacks,
|
|
2226
|
+
providers: acc.providers.size,
|
|
2227
|
+
models: acc.models.size,
|
|
2228
|
+
inputTokens: acc.inputTokens,
|
|
2229
|
+
outputTokens: acc.outputTokens,
|
|
2230
|
+
cacheReadTokens: acc.cacheReadTokens,
|
|
2231
|
+
cacheWriteTokens: acc.cacheWriteTokens,
|
|
2232
|
+
estimatedCostUsd: totalCost,
|
|
2233
|
+
providerAvgDurationMs: average(sortedProviderDurations),
|
|
2234
|
+
providerP95DurationMs: percentile(sortedProviderDurations, 0.95),
|
|
2235
|
+
toolCalls: acc.toolCalls,
|
|
2236
|
+
completedTools: acc.completedTools,
|
|
2237
|
+
failedTools: acc.failedTools,
|
|
2238
|
+
toolAvgDurationMs: average(sortedToolDurations),
|
|
2239
|
+
processes: acc.processes,
|
|
2240
|
+
failedProcesses: acc.failedProcesses,
|
|
2241
|
+
fileEvents: acc.fileEvents,
|
|
2242
|
+
uniqueFiles: acc.uniqueFiles.size,
|
|
2243
|
+
agentEvents: acc.agentEvents,
|
|
2244
|
+
uniqueAgents: acc.uniqueAgents.size,
|
|
2245
|
+
decisions: acc.decisions,
|
|
2246
|
+
escalations: acc.escalations,
|
|
2247
|
+
failures: acc.failures,
|
|
2248
|
+
cancellations: acc.cancellations,
|
|
2249
|
+
families: acc.families,
|
|
2250
|
+
failuresByFamily: acc.failuresByFamily
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
Object.defineProperty(ChronicleQueryEngine.prototype, "partitionFiles", {
|
|
2254
|
+
get() {
|
|
2255
|
+
throw new Error("ChronicleQueryEngine no longer loads events on construction. Use async query().");
|
|
2256
|
+
},
|
|
2257
|
+
set(_val) {
|
|
2258
|
+
Object.defineProperty(this, "partitionFiles", { value: _val, writable: false, configurable: false });
|
|
2259
|
+
}
|
|
2260
|
+
});
|
|
2261
|
+
async function findPartitions(root) {
|
|
2262
|
+
const result = [];
|
|
2263
|
+
const partitionName = /^(.*\.events)(?:\.(\d{5}))?\.jsonl$/;
|
|
2264
|
+
const visit = async (directory) => {
|
|
2265
|
+
let entries;
|
|
2266
|
+
try {
|
|
2267
|
+
entries = await fs4.readdir(directory, { withFileTypes: true });
|
|
2268
|
+
} catch {
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
for (const entry of entries) {
|
|
2272
|
+
const full = path5.join(directory, entry.name);
|
|
2273
|
+
if (entry.isDirectory()) await visit(full);
|
|
2274
|
+
else if (entry.isFile() && partitionName.test(entry.name)) result.push(full);
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
await visit(root);
|
|
2278
|
+
return result.sort((left, right) => {
|
|
2279
|
+
const leftMatch = partitionName.exec(path5.basename(left));
|
|
2280
|
+
const rightMatch = partitionName.exec(path5.basename(right));
|
|
2281
|
+
const leftGroup = path5.join(path5.dirname(left), leftMatch?.[1] ?? left);
|
|
2282
|
+
const rightGroup = path5.join(path5.dirname(right), rightMatch?.[1] ?? right);
|
|
2283
|
+
const groupOrder = leftGroup.localeCompare(rightGroup);
|
|
2284
|
+
if (groupOrder !== 0) return groupOrder;
|
|
2285
|
+
return Number(leftMatch?.[2] ?? 0) - Number(rightMatch?.[2] ?? 0);
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
function numberAt(event, dotPath) {
|
|
2289
|
+
const value = readPath(event.attributes ?? {}, dotPath);
|
|
2290
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
2291
|
+
}
|
|
2292
|
+
function durationMs(event) {
|
|
2293
|
+
const value = Number(event.durationNs ?? 0) / 1e6;
|
|
2294
|
+
return Number.isFinite(value) ? value : 0;
|
|
2295
|
+
}
|
|
2296
|
+
function average(values) {
|
|
2297
|
+
return values.length ? values.reduce((sum, v) => sum + v, 0) / values.length : 0;
|
|
2298
|
+
}
|
|
2299
|
+
function percentile(sorted, quantile) {
|
|
2300
|
+
return sorted.length ? sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * quantile) - 1))] : 0;
|
|
2301
|
+
}
|
|
2302
|
+
function scopeKey(event) {
|
|
2303
|
+
return `${event.scope.projectId ?? ""}\0${event.scope.sessionId ?? ""}\0${event.scope.agentId ?? ""}`;
|
|
2304
|
+
}
|
|
2305
|
+
function signalFamily(event) {
|
|
2306
|
+
if (event.eventType.startsWith("decision.") || event.eventType.startsWith("brain.")) return "decision";
|
|
2307
|
+
if (event.resource?.kind === "file" || event.resource?.kind === "symbol" || /^(?:file|worktree)\./.test(event.eventType)) return "file";
|
|
2308
|
+
if (/^(?:provider|token|context|ctx|compaction)\./.test(event.eventType)) return "llm";
|
|
2309
|
+
if (/^(?:agent|subagent|delegate|fleet|concurrency)\./.test(event.eventType)) return "agent";
|
|
2310
|
+
if (/^(?:tool|process|mcp|network)\./.test(event.eventType)) return "tool";
|
|
2311
|
+
if (/^(?:memory|storage|trust)\./.test(event.eventType)) return "memory";
|
|
2312
|
+
if (/^(?:sdd|task|kanban|checkpoint|session|iteration|in_flight)\./.test(event.eventType)) return "task";
|
|
2313
|
+
return "runtime";
|
|
2314
|
+
}
|
|
2315
|
+
function isTerminalFailure(event) {
|
|
2316
|
+
if (event.eventType === "provider.attempt.failed") return event.attributes?.retryScheduled !== true;
|
|
2317
|
+
return event.eventType === "tool.failed" || event.eventType === "process.completed" && event.outcome === "failure" || /^(?:agent\.run\.error|sdd\.task\.failed|compaction\.failed|network\.request\.failed)$/.test(event.eventType);
|
|
2318
|
+
}
|
|
2319
|
+
function relationKeys(event) {
|
|
2320
|
+
const result = [];
|
|
2321
|
+
const add = (kind, value, confidence) => {
|
|
2322
|
+
if (typeof value === "string" && value) result.push({ key: `${kind}:${value}`, kind, confidence });
|
|
2323
|
+
};
|
|
2324
|
+
add("trace", event.correlation.traceId, "correlated");
|
|
2325
|
+
add("tool_call", event.correlation.toolCallId, "explicit");
|
|
2326
|
+
add("logical_request", event.correlation.logicalRequestId, "explicit");
|
|
2327
|
+
add("attempt", event.correlation.attemptId, "explicit");
|
|
2328
|
+
add("decision", event.attributes?.decisionId, "explicit");
|
|
2329
|
+
add("network_request", event.attributes?.requestId, "explicit");
|
|
2330
|
+
add("prompt_manifest", event.attributes?.promptManifest?.manifestId, "explicit");
|
|
2331
|
+
add("resource_lineage", event.resource?.id, "inferred");
|
|
2332
|
+
if (event.correlation.parentSpanId) add("parent_span", event.correlation.parentSpanId, "explicit");
|
|
2333
|
+
add("parent_span", event.correlation.spanId, "explicit");
|
|
2334
|
+
return result;
|
|
2335
|
+
}
|
|
2336
|
+
function matches(event, query) {
|
|
2337
|
+
if (query.eventId && event.eventId !== query.eventId) return false;
|
|
2338
|
+
if (query.eventTypes && !query.eventTypes.includes(event.eventType)) return false;
|
|
2339
|
+
if (query.outcomes && (!event.outcome || !query.outcomes.includes(event.outcome))) return false;
|
|
2340
|
+
const occurredAt = event.occurredAt ?? event.observedAt;
|
|
2341
|
+
if (query.from && occurredAt < query.from || query.to && occurredAt > query.to) return false;
|
|
2342
|
+
if (!equal(query.projectId, event.scope.projectId) || !equal(query.sessionId, event.scope.sessionId)) return false;
|
|
2343
|
+
if (!equal(query.agentId, event.scope.agentId) || !equal(query.taskId, event.scope.taskId)) return false;
|
|
2344
|
+
if (!equal(query.providerId, event.runtime?.providerId) || !equal(query.modelId, event.runtime?.modelId)) return false;
|
|
2345
|
+
if (!equal(query.traceId, event.correlation.traceId) || !equal(query.logicalRequestId, event.correlation.logicalRequestId)) return false;
|
|
2346
|
+
if (!equal(query.attemptId, event.correlation.attemptId) || !equal(query.toolCallId, event.correlation.toolCallId)) return false;
|
|
2347
|
+
if (!equal(query.resourceKind, event.resource?.kind) || !equal(query.resourceId, event.resource?.id)) return false;
|
|
2348
|
+
if (query.path && normalize2(event.resource?.path) !== normalize2(query.path)) return false;
|
|
2349
|
+
if (query.line !== void 0 && !lineContains(event, query.line)) return false;
|
|
2350
|
+
if (query.tags && !objectContains(event.tags, query.tags)) return false;
|
|
2351
|
+
if (query.attributes && !objectContains(event.attributes, query.attributes)) return false;
|
|
2352
|
+
if (query.text && !JSON.stringify(event).toLocaleLowerCase().includes(query.text.toLocaleLowerCase())) return false;
|
|
2353
|
+
return true;
|
|
2354
|
+
}
|
|
2355
|
+
function equal(expected, actual) {
|
|
2356
|
+
return expected === void 0 || expected === actual;
|
|
2357
|
+
}
|
|
2358
|
+
function normalize2(value) {
|
|
2359
|
+
return value?.replaceAll("\\", "/").toLocaleLowerCase();
|
|
2360
|
+
}
|
|
2361
|
+
function lineContains(event, line) {
|
|
2362
|
+
const start = event.resource?.lineStart;
|
|
2363
|
+
const end = event.resource?.lineEnd ?? start;
|
|
2364
|
+
return start !== void 0 && end !== void 0 && line >= start && line <= end;
|
|
2365
|
+
}
|
|
2366
|
+
function objectContains(actual, expected) {
|
|
2367
|
+
return Boolean(actual && Object.entries(expected).every(([key, value]) => deepEqual(readPath(actual, key), value)));
|
|
2368
|
+
}
|
|
2369
|
+
function readPath(value, key) {
|
|
2370
|
+
return key.split(".").reduce((current, part) => current && typeof current === "object" ? current[part] : void 0, value);
|
|
2371
|
+
}
|
|
2372
|
+
function deepEqual(left, right) {
|
|
2373
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
2374
|
+
}
|
|
2375
|
+
function findInsertionIndex(events, event, compare) {
|
|
2376
|
+
let low = 0;
|
|
2377
|
+
let high = events.length;
|
|
2378
|
+
while (low < high) {
|
|
2379
|
+
const middle = low + high >>> 1;
|
|
2380
|
+
if (compare(events[middle], event) <= 0) low = middle + 1;
|
|
2381
|
+
else high = middle;
|
|
2382
|
+
}
|
|
2383
|
+
return low;
|
|
2384
|
+
}
|
|
2385
|
+
function compareEvents(a, b) {
|
|
2386
|
+
return compareEventToKey(a, orderKey(b));
|
|
2387
|
+
}
|
|
2388
|
+
function compareEventToKey(event, key) {
|
|
2389
|
+
return (event.occurredAt ?? event.observedAt).localeCompare(key.occurredAt) || event.persistedAt.localeCompare(key.persistedAt) || event.sequence - key.sequence || event.eventId.localeCompare(key.eventId);
|
|
2390
|
+
}
|
|
2391
|
+
function orderKey(event) {
|
|
2392
|
+
return {
|
|
2393
|
+
occurredAt: event.occurredAt ?? event.observedAt,
|
|
2394
|
+
persistedAt: event.persistedAt,
|
|
2395
|
+
sequence: event.sequence,
|
|
2396
|
+
eventId: event.eventId
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
function hashQuery(query) {
|
|
2400
|
+
const { cursor: _cursor, limit: _limit, order: _order, ...filters } = query;
|
|
2401
|
+
return createHash11("sha256").update(stableStringify2(filters), "utf8").digest("base64url");
|
|
2402
|
+
}
|
|
2403
|
+
function encodeCursor(cursor) {
|
|
2404
|
+
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
2405
|
+
}
|
|
2406
|
+
function decodeCursor(encoded, order, queryHash) {
|
|
2407
|
+
if (!encoded) return void 0;
|
|
2408
|
+
if (encoded.length > 1e6) throw new Error("Invalid Chronicle cursor");
|
|
2409
|
+
try {
|
|
2410
|
+
const parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
2411
|
+
if (!isCursor(parsed) || parsed.order !== order || parsed.queryHash !== queryHash) {
|
|
2412
|
+
throw new Error("cursor does not match the query");
|
|
2413
|
+
}
|
|
2414
|
+
return parsed;
|
|
2415
|
+
} catch (error) {
|
|
2416
|
+
throw new Error(`Invalid Chronicle cursor: ${error instanceof Error ? error.message : String(error)}`);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
function isCursor(value) {
|
|
2420
|
+
if (!value || typeof value !== "object") return false;
|
|
2421
|
+
const cursor = value;
|
|
2422
|
+
const after = cursor.after;
|
|
2423
|
+
if (cursor.version !== 1 || cursor.order !== "asc" && cursor.order !== "desc" || typeof cursor.queryHash !== "string" || !after || typeof after.occurredAt !== "string" || typeof after.persistedAt !== "string" || !Number.isSafeInteger(after.sequence) || typeof after.eventId !== "string" || !Array.isArray(cursor.snapshot) || cursor.snapshot.length > MAX_CURSOR_SNAPSHOT_ENTRIES) return false;
|
|
2424
|
+
const snapshotIds = /* @__PURE__ */ new Set();
|
|
2425
|
+
for (const entry of cursor.snapshot) {
|
|
2426
|
+
if (!entry || typeof entry.id !== "string" || !entry.id || !Number.isSafeInteger(entry.size) || entry.size < 0 || snapshotIds.has(entry.id)) return false;
|
|
2427
|
+
snapshotIds.add(entry.id);
|
|
2428
|
+
}
|
|
2429
|
+
return true;
|
|
2430
|
+
}
|
|
2431
|
+
async function captureSnapshot(files) {
|
|
2432
|
+
if (files.length > MAX_CURSOR_SNAPSHOT_ENTRIES) {
|
|
2433
|
+
throw new Error("Chronicle snapshot contains too many partitions");
|
|
2434
|
+
}
|
|
2435
|
+
return Promise.all(files.map(async (file) => {
|
|
2436
|
+
let size = 0;
|
|
2437
|
+
try {
|
|
2438
|
+
size = (await fs4.stat(file)).size;
|
|
2439
|
+
} catch {
|
|
2440
|
+
}
|
|
2441
|
+
return { file, id: fileId(file), size };
|
|
2442
|
+
}));
|
|
2443
|
+
}
|
|
2444
|
+
async function resolveSnapshotFiles(files, snapshot) {
|
|
2445
|
+
const currentFiles = new Map(files.map((file) => [fileId(file), file]));
|
|
2446
|
+
return Promise.all(snapshot.map(async (entry) => {
|
|
2447
|
+
const file = currentFiles.get(entry.id);
|
|
2448
|
+
if (!file) throw new Error("Chronicle cursor snapshot has expired");
|
|
2449
|
+
let currentSize;
|
|
2450
|
+
try {
|
|
2451
|
+
currentSize = (await fs4.stat(file)).size;
|
|
2452
|
+
} catch {
|
|
2453
|
+
throw new Error("Chronicle cursor snapshot has expired");
|
|
2454
|
+
}
|
|
2455
|
+
if (currentSize < entry.size) throw new Error("Chronicle cursor snapshot has expired");
|
|
2456
|
+
return { file, ...entry };
|
|
2457
|
+
}));
|
|
2458
|
+
}
|
|
2459
|
+
function fileId(file) {
|
|
2460
|
+
return createHash11("sha256").update(path5.resolve(file), "utf8").digest("base64url");
|
|
2461
|
+
}
|
|
2462
|
+
function stableStringify2(value) {
|
|
2463
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
2464
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify2).join(",")}]`;
|
|
2465
|
+
const object = value;
|
|
2466
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
|
|
2467
|
+
}
|
|
2468
|
+
function isChronicleEvent(value) {
|
|
2469
|
+
if (!isRecord(value) || !isRecord(value.scope) || !isRecord(value.correlation)) return false;
|
|
2470
|
+
return value.schemaVersion === 1 && typeof value.eventId === "string" && typeof value.eventType === "string" && typeof value.occurredAt === "string" && typeof value.observedAt === "string" && typeof value.persistedAt === "string" && Number.isSafeInteger(value.sequence) && value.sequence >= 0 && typeof value.previousHash === "string" && typeof value.hash === "string" && typeof value.scope.installationId === "string" && typeof value.scope.machineId === "string" && optionalStrings(value.scope, [
|
|
2471
|
+
"projectId",
|
|
2472
|
+
"repositoryId",
|
|
2473
|
+
"workspaceId",
|
|
2474
|
+
"worktreeId",
|
|
2475
|
+
"sessionId",
|
|
2476
|
+
"turnId",
|
|
2477
|
+
"iterationId",
|
|
2478
|
+
"agentId",
|
|
2479
|
+
"goalId",
|
|
2480
|
+
"planId",
|
|
2481
|
+
"taskId",
|
|
2482
|
+
"kanbanBoardId"
|
|
2483
|
+
]) && typeof value.correlation.traceId === "string" && typeof value.correlation.spanId === "string" && optionalStrings(value.correlation, [
|
|
2484
|
+
"parentSpanId",
|
|
2485
|
+
"logicalRequestId",
|
|
2486
|
+
"attemptId",
|
|
2487
|
+
"toolCallId"
|
|
2488
|
+
]) && isRuntime(value.runtime) && isResource(value.resource) && (value.attributes === void 0 || isRecord(value.attributes)) && (value.tags === void 0 || isStringRecord(value.tags));
|
|
2489
|
+
}
|
|
2490
|
+
function isRuntime(value) {
|
|
2491
|
+
return value === void 0 || isRecord(value) && optionalStrings(value, ["providerId", "modelId", "modelRevision"]) && optionalFiniteNumbers(value, ["processId", "parentProcessId"]);
|
|
2492
|
+
}
|
|
2493
|
+
function isResource(value) {
|
|
2494
|
+
if (value === void 0) return true;
|
|
2495
|
+
if (!isRecord(value) || ![
|
|
2496
|
+
"file",
|
|
2497
|
+
"symbol",
|
|
2498
|
+
"memory",
|
|
2499
|
+
"task",
|
|
2500
|
+
"kanban",
|
|
2501
|
+
"process",
|
|
2502
|
+
"network",
|
|
2503
|
+
"artifact",
|
|
2504
|
+
"other"
|
|
2505
|
+
].includes(String(value.kind)) || typeof value.id !== "string") return false;
|
|
2506
|
+
return optionalStrings(value, ["path", "contentHashBefore", "contentHashAfter"]) && optionalFiniteNumbers(value, ["lineStart", "lineEnd"]);
|
|
2507
|
+
}
|
|
2508
|
+
function optionalStrings(value, keys) {
|
|
2509
|
+
return keys.every((key) => value[key] === void 0 || typeof value[key] === "string");
|
|
2510
|
+
}
|
|
2511
|
+
function optionalFiniteNumbers(value, keys) {
|
|
2512
|
+
return keys.every((key) => value[key] === void 0 || typeof value[key] === "number" && Number.isFinite(value[key]));
|
|
2513
|
+
}
|
|
2514
|
+
function isStringRecord(value) {
|
|
2515
|
+
return isRecord(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
2516
|
+
}
|
|
2517
|
+
function isRecord(value) {
|
|
2518
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
2519
|
+
}
|
|
2520
|
+
function facetValue(event, field) {
|
|
2521
|
+
const values = {
|
|
2522
|
+
eventType: event.eventType,
|
|
2523
|
+
outcome: event.outcome,
|
|
2524
|
+
projectId: event.scope.projectId,
|
|
2525
|
+
sessionId: event.scope.sessionId,
|
|
2526
|
+
agentId: event.scope.agentId,
|
|
2527
|
+
taskId: event.scope.taskId,
|
|
2528
|
+
providerId: event.runtime?.providerId,
|
|
2529
|
+
modelId: event.runtime?.modelId,
|
|
2530
|
+
resourceKind: event.resource?.kind,
|
|
2531
|
+
resourcePath: event.resource?.path,
|
|
2532
|
+
toolCallId: event.correlation.toolCallId
|
|
2533
|
+
};
|
|
2534
|
+
return values[field];
|
|
2535
|
+
}
|
|
2536
|
+
export {
|
|
2537
|
+
CHRONICLE_SCHEMA_VERSION,
|
|
2538
|
+
ChronicleJournal,
|
|
2539
|
+
ChronicleQueryEngine,
|
|
2540
|
+
GENESIS_HASH,
|
|
2541
|
+
childChronicleContext,
|
|
2542
|
+
createChronicleContext,
|
|
2543
|
+
createChroniclePromptManifest,
|
|
2544
|
+
resolveChronicleRuntimeLocation,
|
|
2545
|
+
startChronicleFileObserver,
|
|
2546
|
+
startChronicleHealthMonitor,
|
|
2547
|
+
wireDecisionsToChronicle,
|
|
2548
|
+
wireDomainEventsToChronicle,
|
|
2549
|
+
wireProcessesToChronicle,
|
|
2550
|
+
wireProviderAttemptsToChronicle,
|
|
2551
|
+
wireProviderStreamsToChronicle,
|
|
2552
|
+
wireRollupsToChronicle,
|
|
2553
|
+
wireToolsToChronicle
|
|
2554
|
+
};
|
|
2555
|
+
//# sourceMappingURL=index.js.map
|