@ijfw/memory-server 1.4.3 → 1.5.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/fixtures/truncation-corpus/_generate-corpus.js +367 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
- package/package.json +1 -1
- package/src/active-extension-writer.js +144 -64
- package/src/api-client.js +43 -5
- package/src/audit-roster.js +80 -5
- package/src/blackboard.js +298 -6
- package/src/cli-run.js +33 -5
- package/src/codex-agents.js +96 -5
- package/src/cost/aggregator.js +39 -9
- package/src/cost/pricing.js +57 -0
- package/src/cost/readers/gemini.js +1 -1
- package/src/cross-audit-chunker.js +189 -0
- package/src/cross-dispatcher.js +124 -21
- package/src/cross-orchestrator-cli.js +550 -14
- package/src/cross-orchestrator.js +1171 -10
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-planning.html +273 -0
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +17 -2
- package/src/dashboard-server.js +152 -0
- package/src/deploy-alerts.js +150 -0
- package/src/design/iframe-bridge.js +242 -0
- package/src/design-companion.js +144 -0
- package/src/dispatch/checkpoint-cli.js +97 -0
- package/src/dispatch/colon-syntax.js +81 -1
- package/src/dispatch/extension.js +27 -1
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +323 -0
- package/src/dispatch/worktree-cli.js +40 -0
- package/src/dispatch-planner.js +97 -2
- package/src/dream/runner.mjs +47 -11
- package/src/dream/stage-runner.js +40 -0
- package/src/dream/state-file.js +102 -0
- package/src/extension-installer.js +70 -24
- package/src/extension-quota-tracker.js +4 -2
- package/src/extension-registry.js +289 -35
- package/src/feedback-detector.js +26 -0
- package/src/fs-lock.js +259 -7
- package/src/gate-result.js +95 -1
- package/src/hero-line.js +86 -5
- package/src/intent-router.js +35 -0
- package/src/lib/a11y-contract.js +117 -0
- package/src/lib/atomic-io.js +29 -8
- package/src/lib/cache-keepalive.js +150 -0
- package/src/lib/jsonl-rotation.js +104 -0
- package/src/lib/lighthouse-pillar.js +121 -0
- package/src/lib/llm-call.js +121 -0
- package/src/lib/playwright-baseline.js +205 -0
- package/src/lib/rekor-bridge.js +221 -0
- package/src/lib/repo-map.js +392 -0
- package/src/lib/shasum-verify.js +164 -0
- package/src/lib/sketches-gc.js +132 -0
- package/src/lib/tmp-suffix.js +62 -0
- package/src/lib/ui-review-runner.js +554 -0
- package/src/lib/uispec-drift.js +301 -0
- package/src/lib/uispec-intake.js +381 -0
- package/src/lib/worktree-guards.js +118 -0
- package/src/lib/worktree-recovery.js +100 -0
- package/src/memory/auto-linker.js +152 -0
- package/src/memory/benchmark.js +498 -0
- package/src/memory/dedup.js +126 -0
- package/src/memory/embedding-cache.js +136 -0
- package/src/memory/fact-extractor.js +168 -0
- package/src/memory/fts5.js +65 -1
- package/src/memory/migrations/004-bitemporal.js +91 -0
- package/src/memory/migrations/005-vector-cache.js +61 -0
- package/src/memory/migrations/006-obsidian-graph.js +46 -0
- package/src/memory/migrations/007-skill-telemetry.js +24 -0
- package/src/memory/migrations/008-write-provenance.js +41 -0
- package/src/memory/obsidian-parser.js +91 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +10 -0
- package/src/memory/temporal.js +529 -0
- package/src/memory/tokenize.js +10 -0
- package/src/memory-facts-handler.js +37 -0
- package/src/memory-feedback.js +260 -2
- package/src/model-refresh.js +292 -0
- package/src/observability/cost-anomaly.js +166 -0
- package/src/observability/evaluator-checkpoint-contract.js +117 -0
- package/src/observability/trace-id.js +163 -0
- package/src/orchestrator/agents-md-blackboard.js +152 -0
- package/src/orchestrator/checkpoint-contract.md +140 -0
- package/src/orchestrator/debug-trident.js +570 -0
- package/src/orchestrator/merge-block-aware.js +350 -0
- package/src/orchestrator/plan-checker.js +475 -0
- package/src/orchestrator/post-done-runner.js +249 -0
- package/src/orchestrator/review.js +136 -0
- package/src/orchestrator/runtime-loop.js +430 -0
- package/src/orchestrator/skill-telemetry-sink.js +29 -0
- package/src/orchestrator/skill-telemetry.js +37 -0
- package/src/orchestrator/state-events.js +459 -0
- package/src/orchestrator/state-sdk.js +1764 -0
- package/src/orchestrator/status-protocol.js +235 -0
- package/src/orchestrator/subagent-telemetry.js +452 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +281 -0
- package/src/orchestrator/wave-state.js +564 -0
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +656 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -0
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +603 -59
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +113 -12
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +41 -0
- package/src/team/domain-templates/content.json +50 -0
- package/src/team/domain-templates/design.json +44 -0
- package/src/team/domain-templates/research.json +41 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +278 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* debug-trident.js — v1.5.0 T29 (W2): Trident-powered debug loop.
|
|
3
|
+
*
|
|
4
|
+
* The ijfw-debug stack (claude/agents/ijfw-debug-session-manager.md +
|
|
5
|
+
* claude/agents/ijfw-debugger.md) drives a multi-cycle scientific-method
|
|
6
|
+
* investigation: reproduce → hypothesise → falsify → resolve. Until W2 the
|
|
7
|
+
* loop was *single-lens*: when the active hypothesis stalled (either
|
|
8
|
+
* INVESTIGATION_INCONCLUSIVE or a byte-identical hypothesis tree two cycles
|
|
9
|
+
* running), the only escape was to surface NEEDS_CONTEXT and ask the user
|
|
10
|
+
* for more evidence. That collapses the debug strength of cross-AI
|
|
11
|
+
* disagreement — a stuck Claude hypothesis tree is exactly where Codex's
|
|
12
|
+
* dependency-call-graph reading or Gemini's broader-context cross-file
|
|
13
|
+
* search would have produced a competing hypothesis the single lens missed.
|
|
14
|
+
*
|
|
15
|
+
* Moat #3 — "Trident-powered debug" — closes the gap by dispatching codex
|
|
16
|
+
* and gemini lenses, in parallel, to generate competing hypotheses against
|
|
17
|
+
* the same evidence pack. The lens responses are appended to the
|
|
18
|
+
* hypothesis tree as new rows (status:'open') and the next cycle resumes
|
|
19
|
+
* with a refreshed candidate set. A campaign exit telemetry record is
|
|
20
|
+
* written via the state-SDK telemetry.record verb (kind:'debug-campaign').
|
|
21
|
+
*
|
|
22
|
+
* --------------------------------------------------------------------------
|
|
23
|
+
* SCOPE — what this module is
|
|
24
|
+
* --------------------------------------------------------------------------
|
|
25
|
+
*
|
|
26
|
+
* * A pluggable orchestration shell (`runDebugCampaign`) that wraps an
|
|
27
|
+
* opaque per-cycle `dispatch` function — the same dependency-injection
|
|
28
|
+
* contract that runPhaseEConverge uses (cross-orchestrator.js §1190).
|
|
29
|
+
* The dispatch function returns one of the structured terminators the
|
|
30
|
+
* ijfw-debugger agent emits: ROOT_CAUSE_FOUND, INVESTIGATION_INCONCLUSIVE,
|
|
31
|
+
* CHECKPOINT_REACHED, TDD_CHECKPOINT, DEBUG_COMPLETE.
|
|
32
|
+
*
|
|
33
|
+
* * Stall detection — two adjacent cycles whose hypothesis-tree signature
|
|
34
|
+
* is byte-identical OR an INVESTIGATION_INCONCLUSIVE terminator => the
|
|
35
|
+
* loop escalates to a Trident hypothesis-gen sub-step.
|
|
36
|
+
*
|
|
37
|
+
* * Cross-lens hypothesis generation — calls `tridentDispatch({ lens,
|
|
38
|
+
* evidencePack, currentHypotheses })` for each non-stalled lens
|
|
39
|
+
* (default: codex + gemini, claude excluded because it owns the stall).
|
|
40
|
+
* The dispatcher is injected; production wires it through the existing
|
|
41
|
+
* cross-dispatcher.js MCP CLI surface, tests stub it.
|
|
42
|
+
*
|
|
43
|
+
* * Hypothesis-tree merge — new candidates are appended with provenance
|
|
44
|
+
* `from:'trident:codex'` / `from:'trident:gemini'` so the receipt
|
|
45
|
+
* captures which lens contributed which hypothesis.
|
|
46
|
+
*
|
|
47
|
+
* * Telemetry — `telemetry.record({ kind:'debug-campaign', metrics })`
|
|
48
|
+
* where metrics ⊇ { cycles, stalls, tridentInvocations, hypothesesAdded,
|
|
49
|
+
* hypothesesCompetingCount, resolved, resolutionLens }.
|
|
50
|
+
*
|
|
51
|
+
* --------------------------------------------------------------------------
|
|
52
|
+
* NON-SCOPE — what this module is NOT
|
|
53
|
+
* --------------------------------------------------------------------------
|
|
54
|
+
*
|
|
55
|
+
* * The investigator. ijfw-debugger.md owns scientific method; this
|
|
56
|
+
* module orchestrates lens dispatch around the existing investigator
|
|
57
|
+
* contract.
|
|
58
|
+
*
|
|
59
|
+
* * The state writer for checkpoint files. The session-manager agent
|
|
60
|
+
* persists `<session_id>.state.json` and `HYPOTHESES.md`; this module
|
|
61
|
+
* accepts them as opaque structured input.
|
|
62
|
+
*
|
|
63
|
+
* * A consumer of state-sdk write-locks beyond `telemetry.record`. The
|
|
64
|
+
* campaign itself is in-memory; only the exit telemetry is persisted.
|
|
65
|
+
*
|
|
66
|
+
* Zero new prod deps. ESM. Node ≥18. No emoji.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
import { query as stateQuery } from './state-sdk.js';
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Reasons the campaign loop terminates. Surfaced on the return value as
|
|
73
|
+
* `outcome` so callers can branch and the receipt can render a one-line
|
|
74
|
+
* narrative without re-deriving from cycle history.
|
|
75
|
+
*/
|
|
76
|
+
export const DEBUG_OUTCOMES = Object.freeze({
|
|
77
|
+
RESOLVED: 'resolved',
|
|
78
|
+
ROOT_CAUSE: 'root_cause_found',
|
|
79
|
+
CHECKPOINT: 'awaiting_context',
|
|
80
|
+
EXHAUSTED: 'cycles_exhausted',
|
|
81
|
+
TRIDENT_DRY: 'trident_no_new_hypotheses',
|
|
82
|
+
FAILED: 'campaign_failed',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Stall signature — a stable, order-independent serialization of the
|
|
87
|
+
* current open hypothesis tree. Used to detect "no new ground covered" two
|
|
88
|
+
* cycles in a row. Sorting both the row list and the per-row key set means
|
|
89
|
+
* two trees with identical content but different insertion order produce
|
|
90
|
+
* the same signature.
|
|
91
|
+
*
|
|
92
|
+
* Input shape (matches HYPOTHESES.md table):
|
|
93
|
+
* [{ id:'H1', hypothesis:'…', status:'open|testing|confirmed|refuted',
|
|
94
|
+
* evidence:'…', refuted_by:'…' }, …]
|
|
95
|
+
*/
|
|
96
|
+
export function hypothesisTreeSignature(hypotheses) {
|
|
97
|
+
if (!Array.isArray(hypotheses) || hypotheses.length === 0) return '';
|
|
98
|
+
const rows = hypotheses.map((row) => {
|
|
99
|
+
if (!row || typeof row !== 'object') return '';
|
|
100
|
+
const keys = Object.keys(row).sort();
|
|
101
|
+
const parts = keys.map((k) => `${k}=${JSON.stringify(row[k] ?? '')}`);
|
|
102
|
+
return parts.join('|');
|
|
103
|
+
});
|
|
104
|
+
rows.sort();
|
|
105
|
+
return rows.join('||');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Decide whether the most recent cycle qualifies as a STALL — the trigger
|
|
110
|
+
* for Trident hypothesis generation.
|
|
111
|
+
*
|
|
112
|
+
* Stall conditions (any one):
|
|
113
|
+
* (a) The just-returned terminator was INVESTIGATION_INCONCLUSIVE.
|
|
114
|
+
* (b) The hypothesis tree signature is byte-identical to the prior
|
|
115
|
+
* cycle's signature AND no terminal status (DEBUG_COMPLETE /
|
|
116
|
+
* ROOT_CAUSE_FOUND) was returned.
|
|
117
|
+
* (c) Caller forced via `forceTrident:true` (test hook).
|
|
118
|
+
*
|
|
119
|
+
* Returns a small object `{ stalled, reason }` so the receipt can record
|
|
120
|
+
* which condition fired.
|
|
121
|
+
*/
|
|
122
|
+
export function detectStall({ terminator, signature, priorSignature, forceTrident = false } = {}) {
|
|
123
|
+
if (forceTrident) {
|
|
124
|
+
return { stalled: true, reason: 'forced' };
|
|
125
|
+
}
|
|
126
|
+
const term = String(terminator || '').toUpperCase();
|
|
127
|
+
if (term === 'INVESTIGATION_INCONCLUSIVE') {
|
|
128
|
+
return { stalled: true, reason: 'inconclusive_terminator' };
|
|
129
|
+
}
|
|
130
|
+
if (term === 'DEBUG_COMPLETE' || term === 'ROOT_CAUSE_FOUND') {
|
|
131
|
+
return { stalled: false, reason: 'progress' };
|
|
132
|
+
}
|
|
133
|
+
if (priorSignature && signature && priorSignature === signature) {
|
|
134
|
+
return { stalled: true, reason: 'byte_identical_tree' };
|
|
135
|
+
}
|
|
136
|
+
return { stalled: false, reason: 'progress' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Validate a Trident-lens hypothesis-gen response. Each lens returns:
|
|
141
|
+
* { lens, hypotheses: [ { hypothesis: string, rationale?: string } ] }
|
|
142
|
+
*
|
|
143
|
+
* A malformed response is treated as zero contributions (defensive: a
|
|
144
|
+
* single bad lens never crashes the campaign).
|
|
145
|
+
*/
|
|
146
|
+
export function normaliseLensResponse(raw, lens) {
|
|
147
|
+
if (!raw || typeof raw !== 'object') {
|
|
148
|
+
return { lens, hypotheses: [], ok: false, reason: 'non-object' };
|
|
149
|
+
}
|
|
150
|
+
const list = Array.isArray(raw.hypotheses) ? raw.hypotheses : [];
|
|
151
|
+
const cleaned = [];
|
|
152
|
+
for (const h of list) {
|
|
153
|
+
if (!h || typeof h !== 'object') continue;
|
|
154
|
+
const text = typeof h.hypothesis === 'string' ? h.hypothesis.trim() : '';
|
|
155
|
+
if (!text) continue;
|
|
156
|
+
cleaned.push({
|
|
157
|
+
hypothesis: text,
|
|
158
|
+
rationale: typeof h.rationale === 'string' ? h.rationale.trim() : '',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return { lens, hypotheses: cleaned, ok: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Run the Trident hypothesis-generation sub-step.
|
|
166
|
+
*
|
|
167
|
+
* The DI hook `tridentDispatch` is invoked once per lens IN PARALLEL with
|
|
168
|
+
* an evidence pack + the current hypothesis tree (so the lens can avoid
|
|
169
|
+
* proposing duplicates of what's already been refuted). A lens that
|
|
170
|
+
* throws is captured as `ok:false`; a lens that returns malformed JSON is
|
|
171
|
+
* normalised to zero hypotheses — neither crashes the campaign.
|
|
172
|
+
*
|
|
173
|
+
* Returns `{ perLens, totalAdded, novelHypotheses }`:
|
|
174
|
+
* - perLens: array of { lens, hypotheses, ok, reason? }
|
|
175
|
+
* - totalAdded: count of novel hypothesis rows after dedup
|
|
176
|
+
* - novelHypotheses: the merged-in rows (with provenance)
|
|
177
|
+
*
|
|
178
|
+
* "Novel" = the hypothesis text (case-insensitive, whitespace-collapsed)
|
|
179
|
+
* does not match any existing row's hypothesis. Refuted rows still count
|
|
180
|
+
* as "existing" — a lens proposing the same refuted theory does NOT get
|
|
181
|
+
* to re-raise it; the orchestrator's job is to break out of stuck space,
|
|
182
|
+
* not loop.
|
|
183
|
+
*/
|
|
184
|
+
export async function generateCompetingHypotheses({
|
|
185
|
+
evidencePack,
|
|
186
|
+
currentHypotheses,
|
|
187
|
+
lenses,
|
|
188
|
+
tridentDispatch,
|
|
189
|
+
abortSignal,
|
|
190
|
+
} = {}) {
|
|
191
|
+
if (typeof tridentDispatch !== 'function') {
|
|
192
|
+
throw new Error('generateCompetingHypotheses: tridentDispatch is required');
|
|
193
|
+
}
|
|
194
|
+
if (!Array.isArray(lenses) || lenses.length === 0) {
|
|
195
|
+
throw new Error('generateCompetingHypotheses: lenses must be non-empty');
|
|
196
|
+
}
|
|
197
|
+
// Normalise the existing-hypothesis text set for dedup.
|
|
198
|
+
const existingNorm = new Set();
|
|
199
|
+
for (const row of currentHypotheses || []) {
|
|
200
|
+
if (row && typeof row.hypothesis === 'string') {
|
|
201
|
+
existingNorm.add(row.hypothesis.trim().toLowerCase().replace(/\s+/g, ' '));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Dispatch lenses in parallel — defensive Promise.all that converts a
|
|
206
|
+
// thrown dispatch into { ok:false, error } rather than rejecting the
|
|
207
|
+
// whole batch (one stuck lens can't kill the campaign).
|
|
208
|
+
const perLens = await Promise.all(
|
|
209
|
+
lenses.map(async (lens) => {
|
|
210
|
+
try {
|
|
211
|
+
const raw = await tridentDispatch({
|
|
212
|
+
lens,
|
|
213
|
+
evidencePack,
|
|
214
|
+
currentHypotheses: Array.isArray(currentHypotheses) ? currentHypotheses : [],
|
|
215
|
+
signal: abortSignal,
|
|
216
|
+
});
|
|
217
|
+
return normaliseLensResponse(raw, lens);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
return {
|
|
220
|
+
lens,
|
|
221
|
+
hypotheses: [],
|
|
222
|
+
ok: false,
|
|
223
|
+
reason: err && err.message ? err.message : String(err),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Merge — append novel rows with provenance. Two lenses can independently
|
|
230
|
+
// converge on the same hypothesis text; the first lens wins the row, the
|
|
231
|
+
// second is dropped (recorded in the receipt as a consensus signal).
|
|
232
|
+
const novelHypotheses = [];
|
|
233
|
+
// Determine the next id by scanning existing row ids of form H<N>.
|
|
234
|
+
const idsTaken = (currentHypotheses || [])
|
|
235
|
+
.map((r) => (r && typeof r.id === 'string' ? r.id : ''))
|
|
236
|
+
.filter((s) => /^H\d+$/.test(s))
|
|
237
|
+
.map((s) => parseInt(s.slice(1), 10));
|
|
238
|
+
let nextId = (idsTaken.length === 0 ? 0 : Math.max(...idsTaken)) + 1;
|
|
239
|
+
const seenThisRound = new Set();
|
|
240
|
+
for (const lensRow of perLens) {
|
|
241
|
+
for (const h of lensRow.hypotheses) {
|
|
242
|
+
const norm = h.hypothesis.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
243
|
+
if (existingNorm.has(norm)) continue;
|
|
244
|
+
if (seenThisRound.has(norm)) continue;
|
|
245
|
+
seenThisRound.add(norm);
|
|
246
|
+
novelHypotheses.push({
|
|
247
|
+
id: `H${nextId++}`,
|
|
248
|
+
hypothesis: h.hypothesis.trim(),
|
|
249
|
+
status: 'open',
|
|
250
|
+
evidence: '',
|
|
251
|
+
refuted_by: '',
|
|
252
|
+
from: `trident:${lensRow.lens}`,
|
|
253
|
+
rationale: h.rationale || '',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
perLens,
|
|
259
|
+
totalAdded: novelHypotheses.length,
|
|
260
|
+
novelHypotheses,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Run a Trident-powered debug campaign.
|
|
266
|
+
*
|
|
267
|
+
* @param {object} opts
|
|
268
|
+
* @param {string} opts.sessionId slug — e.g. 'auth-redirect-loop'
|
|
269
|
+
* @param {string} opts.symptoms one-line expected vs actual
|
|
270
|
+
* @param {Array<object>} [opts.hypotheses] initial hypothesis tree (rows)
|
|
271
|
+
* @param {Function} opts.dispatch async ({ cycle, hypotheses, evidencePack }) =>
|
|
272
|
+
* { terminator, hypothesesPatch?, fix?, rootCause? }
|
|
273
|
+
* terminator ∈ ROOT_CAUSE_FOUND | DEBUG_COMPLETE |
|
|
274
|
+
* INVESTIGATION_INCONCLUSIVE | CHECKPOINT_REACHED |
|
|
275
|
+
* TDD_CHECKPOINT
|
|
276
|
+
* @param {Function} opts.tridentDispatch async ({ lens, evidencePack, currentHypotheses }) =>
|
|
277
|
+
* { lens, hypotheses: [ { hypothesis, rationale? } ] }
|
|
278
|
+
* @param {Array<string>} [opts.tridentLenses] default ['codex','gemini']; claude
|
|
279
|
+
* omitted because it owns the stall
|
|
280
|
+
* @param {number} [opts.maxCycles] default 6
|
|
281
|
+
* @param {string} [opts.evidencePack] opaque blob forwarded to dispatches
|
|
282
|
+
* @param {string} [opts.projectRoot] forwarded to telemetry.record
|
|
283
|
+
* @param {string} [opts.runStamp] ISO timestamp seed (deterministic in tests)
|
|
284
|
+
* @param {AbortSignal} [opts.abortSignal] propagates into dispatch calls
|
|
285
|
+
* @param {boolean} [opts.recordTelemetry] default true; off in unit tests that
|
|
286
|
+
* don't supply a projectRoot
|
|
287
|
+
* @returns {Promise<object>} campaign record (see CAMPAIGN_RECORD_SHAPE)
|
|
288
|
+
*
|
|
289
|
+
* CAMPAIGN_RECORD_SHAPE:
|
|
290
|
+
* {
|
|
291
|
+
* sessionId, symptoms, outcome (DEBUG_OUTCOMES), cycles, stalls,
|
|
292
|
+
* tridentInvocations, hypothesesAdded, resolutionLens, rootCause,
|
|
293
|
+
* fix, cyclesLog: [...], hypothesesFinal: [...], duration_ms
|
|
294
|
+
* }
|
|
295
|
+
*/
|
|
296
|
+
export async function runDebugCampaign({
|
|
297
|
+
sessionId,
|
|
298
|
+
symptoms,
|
|
299
|
+
hypotheses = [],
|
|
300
|
+
dispatch,
|
|
301
|
+
tridentDispatch,
|
|
302
|
+
tridentLenses = ['codex', 'gemini'],
|
|
303
|
+
maxCycles = 6,
|
|
304
|
+
evidencePack = '',
|
|
305
|
+
projectRoot,
|
|
306
|
+
runStamp,
|
|
307
|
+
abortSignal,
|
|
308
|
+
recordTelemetry = true,
|
|
309
|
+
} = {}) {
|
|
310
|
+
if (typeof sessionId !== 'string' || !sessionId.trim()) {
|
|
311
|
+
throw new Error('runDebugCampaign: sessionId is required');
|
|
312
|
+
}
|
|
313
|
+
if (typeof symptoms !== 'string' || !symptoms.trim()) {
|
|
314
|
+
throw new Error('runDebugCampaign: symptoms is required');
|
|
315
|
+
}
|
|
316
|
+
if (typeof dispatch !== 'function') {
|
|
317
|
+
throw new Error('runDebugCampaign: dispatch is required');
|
|
318
|
+
}
|
|
319
|
+
if (typeof tridentDispatch !== 'function') {
|
|
320
|
+
throw new Error('runDebugCampaign: tridentDispatch is required');
|
|
321
|
+
}
|
|
322
|
+
if (!Array.isArray(tridentLenses) || tridentLenses.length === 0) {
|
|
323
|
+
throw new Error('runDebugCampaign: tridentLenses must be a non-empty array');
|
|
324
|
+
}
|
|
325
|
+
if (!Number.isFinite(maxCycles) || maxCycles < 1) {
|
|
326
|
+
throw new Error('runDebugCampaign: maxCycles must be a positive integer');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const t0 = Date.now();
|
|
330
|
+
const _runStamp = runStamp || new Date().toISOString();
|
|
331
|
+
|
|
332
|
+
// Mutable campaign state. The hypotheses array is the source of truth —
|
|
333
|
+
// dispatch may return a `hypothesesPatch` (replacement or delta) that the
|
|
334
|
+
// orchestrator merges; Trident escalation also mutates it.
|
|
335
|
+
let workingHypotheses = Array.isArray(hypotheses) ? [...hypotheses] : [];
|
|
336
|
+
let priorSignature = hypothesisTreeSignature(workingHypotheses);
|
|
337
|
+
let outcome = DEBUG_OUTCOMES.EXHAUSTED;
|
|
338
|
+
let resolutionLens = null;
|
|
339
|
+
let rootCause = null;
|
|
340
|
+
let fix = null;
|
|
341
|
+
let stalls = 0;
|
|
342
|
+
let tridentInvocations = 0;
|
|
343
|
+
let hypothesesAdded = 0;
|
|
344
|
+
let lastError = null;
|
|
345
|
+
const cyclesLog = [];
|
|
346
|
+
|
|
347
|
+
// The main investigation loop.
|
|
348
|
+
for (let cycle = 1; cycle <= maxCycles; cycle++) {
|
|
349
|
+
if (abortSignal && abortSignal.aborted) {
|
|
350
|
+
outcome = DEBUG_OUTCOMES.FAILED;
|
|
351
|
+
lastError = 'aborted';
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let dispatchResult;
|
|
356
|
+
try {
|
|
357
|
+
dispatchResult = await dispatch({
|
|
358
|
+
cycle,
|
|
359
|
+
hypotheses: workingHypotheses,
|
|
360
|
+
evidencePack,
|
|
361
|
+
sessionId,
|
|
362
|
+
symptoms,
|
|
363
|
+
signal: abortSignal,
|
|
364
|
+
});
|
|
365
|
+
} catch (err) {
|
|
366
|
+
outcome = DEBUG_OUTCOMES.FAILED;
|
|
367
|
+
lastError = err && err.message ? err.message : String(err);
|
|
368
|
+
cyclesLog.push({
|
|
369
|
+
cycle,
|
|
370
|
+
terminator: 'DISPATCH_THREW',
|
|
371
|
+
error: lastError,
|
|
372
|
+
});
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const terminator = String(dispatchResult?.terminator || '').toUpperCase();
|
|
377
|
+
|
|
378
|
+
// Merge dispatcher-supplied hypothesis updates BEFORE stall detection so
|
|
379
|
+
// we evaluate the post-cycle tree (mirrors what HYPOTHESES.md would
|
|
380
|
+
// look like on disk after the investigator wrote its row updates).
|
|
381
|
+
if (Array.isArray(dispatchResult?.hypothesesPatch)) {
|
|
382
|
+
workingHypotheses = mergeHypothesesPatch(workingHypotheses, dispatchResult.hypothesesPatch);
|
|
383
|
+
} else if (Array.isArray(dispatchResult?.hypothesesReplacement)) {
|
|
384
|
+
workingHypotheses = [...dispatchResult.hypothesesReplacement];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const signature = hypothesisTreeSignature(workingHypotheses);
|
|
388
|
+
const cycleEntry = {
|
|
389
|
+
cycle,
|
|
390
|
+
terminator,
|
|
391
|
+
hypothesisCount: workingHypotheses.length,
|
|
392
|
+
signature: signature.slice(0, 64),
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// Terminal happy paths.
|
|
396
|
+
if (terminator === 'DEBUG_COMPLETE') {
|
|
397
|
+
outcome = DEBUG_OUTCOMES.RESOLVED;
|
|
398
|
+
fix = typeof dispatchResult.fix === 'string' ? dispatchResult.fix : null;
|
|
399
|
+
rootCause = typeof dispatchResult.rootCause === 'string' ? dispatchResult.rootCause : null;
|
|
400
|
+
resolutionLens = dispatchResult.resolutionLens || 'claude';
|
|
401
|
+
cyclesLog.push(cycleEntry);
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
if (terminator === 'ROOT_CAUSE_FOUND') {
|
|
405
|
+
outcome = DEBUG_OUTCOMES.ROOT_CAUSE;
|
|
406
|
+
rootCause = typeof dispatchResult.rootCause === 'string' ? dispatchResult.rootCause : null;
|
|
407
|
+
resolutionLens = dispatchResult.resolutionLens || 'claude';
|
|
408
|
+
cyclesLog.push(cycleEntry);
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
if (terminator === 'CHECKPOINT_REACHED') {
|
|
412
|
+
outcome = DEBUG_OUTCOMES.CHECKPOINT;
|
|
413
|
+
cyclesLog.push(cycleEntry);
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Stall detection.
|
|
418
|
+
const stall = detectStall({
|
|
419
|
+
terminator,
|
|
420
|
+
signature,
|
|
421
|
+
priorSignature,
|
|
422
|
+
forceTrident: !!dispatchResult?.forceTrident,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
if (stall.stalled) {
|
|
426
|
+
stalls += 1;
|
|
427
|
+
cycleEntry.stalled = true;
|
|
428
|
+
cycleEntry.stallReason = stall.reason;
|
|
429
|
+
|
|
430
|
+
// Trident escalation — generate competing cross-lens hypotheses.
|
|
431
|
+
tridentInvocations += 1;
|
|
432
|
+
let tridentResult;
|
|
433
|
+
try {
|
|
434
|
+
tridentResult = await generateCompetingHypotheses({
|
|
435
|
+
evidencePack,
|
|
436
|
+
currentHypotheses: workingHypotheses,
|
|
437
|
+
lenses: tridentLenses,
|
|
438
|
+
tridentDispatch,
|
|
439
|
+
abortSignal,
|
|
440
|
+
});
|
|
441
|
+
} catch (err) {
|
|
442
|
+
// A throw from the Trident sub-step itself (NOT from a per-lens
|
|
443
|
+
// dispatch — those are already caught above) is a campaign-level
|
|
444
|
+
// failure: we cannot recover the stall without competing hypotheses
|
|
445
|
+
// and continuing would just loop on the same signature.
|
|
446
|
+
outcome = DEBUG_OUTCOMES.FAILED;
|
|
447
|
+
lastError = err && err.message ? err.message : String(err);
|
|
448
|
+
cycleEntry.tridentError = lastError;
|
|
449
|
+
cyclesLog.push(cycleEntry);
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
cycleEntry.trident = {
|
|
453
|
+
lensesInvoked: tridentResult.perLens.map((p) => p.lens),
|
|
454
|
+
lensesOk: tridentResult.perLens.filter((p) => p.ok).map((p) => p.lens),
|
|
455
|
+
lensesFailed: tridentResult.perLens.filter((p) => !p.ok).map((p) => ({
|
|
456
|
+
lens: p.lens,
|
|
457
|
+
reason: p.reason,
|
|
458
|
+
})),
|
|
459
|
+
hypothesesAdded: tridentResult.totalAdded,
|
|
460
|
+
novelHypothesesPreview: tridentResult.novelHypotheses
|
|
461
|
+
.slice(0, 3)
|
|
462
|
+
.map((h) => ({ id: h.id, from: h.from, hypothesis: h.hypothesis })),
|
|
463
|
+
};
|
|
464
|
+
hypothesesAdded += tridentResult.totalAdded;
|
|
465
|
+
|
|
466
|
+
if (tridentResult.totalAdded === 0) {
|
|
467
|
+
// No new ground from Trident either — terminate as exhausted (the
|
|
468
|
+
// single-lens AND the cross-lens swarm both stalled on the same
|
|
469
|
+
// hypothesis space).
|
|
470
|
+
outcome = DEBUG_OUTCOMES.TRIDENT_DRY;
|
|
471
|
+
cyclesLog.push(cycleEntry);
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Append novel hypotheses; refresh the signature so the NEXT cycle
|
|
476
|
+
// doesn't immediately re-trigger stall detection on the merged tree.
|
|
477
|
+
workingHypotheses = workingHypotheses.concat(tridentResult.novelHypotheses);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
cyclesLog.push(cycleEntry);
|
|
481
|
+
priorSignature = hypothesisTreeSignature(workingHypotheses);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// -------- Telemetry: write campaign exit metrics via state-SDK.
|
|
485
|
+
// OFF the critical path — failure must NEVER alter the returned outcome
|
|
486
|
+
// (mirrors cross-orchestrator.js T21 convergence-telemetry discipline).
|
|
487
|
+
if (recordTelemetry && projectRoot) {
|
|
488
|
+
const metrics = {
|
|
489
|
+
sessionId,
|
|
490
|
+
outcome,
|
|
491
|
+
cycles: cyclesLog.length,
|
|
492
|
+
stalls,
|
|
493
|
+
tridentInvocations,
|
|
494
|
+
hypothesesAdded,
|
|
495
|
+
hypothesesCompetingCount: workingHypotheses.filter((h) => typeof h?.from === 'string' && h.from.startsWith('trident:')).length,
|
|
496
|
+
resolved: outcome === DEBUG_OUTCOMES.RESOLVED,
|
|
497
|
+
resolutionLens,
|
|
498
|
+
durationMs: Date.now() - t0,
|
|
499
|
+
runStamp: _runStamp,
|
|
500
|
+
};
|
|
501
|
+
const dedupKey = `debug-campaign:${sessionId}:${_runStamp}`;
|
|
502
|
+
try {
|
|
503
|
+
await stateQuery('telemetry.record', {
|
|
504
|
+
kind: 'debug-campaign',
|
|
505
|
+
metrics,
|
|
506
|
+
dedupKey,
|
|
507
|
+
}, { projectRoot });
|
|
508
|
+
} catch {
|
|
509
|
+
// Swallow — telemetry never alters campaign verdict.
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
sessionId,
|
|
515
|
+
symptoms,
|
|
516
|
+
outcome,
|
|
517
|
+
cycles: cyclesLog.length,
|
|
518
|
+
stalls,
|
|
519
|
+
tridentInvocations,
|
|
520
|
+
hypothesesAdded,
|
|
521
|
+
resolutionLens,
|
|
522
|
+
rootCause,
|
|
523
|
+
fix,
|
|
524
|
+
cyclesLog,
|
|
525
|
+
hypothesesFinal: workingHypotheses,
|
|
526
|
+
duration_ms: Date.now() - t0,
|
|
527
|
+
runStamp: _runStamp,
|
|
528
|
+
lastError,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Merge a hypothesis-tree patch from the investigator. The patch is a list
|
|
534
|
+
* of rows: rows whose id matches an existing row REPLACE that row; rows
|
|
535
|
+
* whose id is new are APPENDED. A row with no id is appended with a fresh
|
|
536
|
+
* H<N> id. Stable, no-throw — a malformed patch entry is silently dropped.
|
|
537
|
+
*/
|
|
538
|
+
export function mergeHypothesesPatch(existing, patch) {
|
|
539
|
+
const out = Array.isArray(existing) ? [...existing] : [];
|
|
540
|
+
if (!Array.isArray(patch)) return out;
|
|
541
|
+
const byId = new Map();
|
|
542
|
+
out.forEach((row, idx) => {
|
|
543
|
+
if (row && typeof row.id === 'string') byId.set(row.id, idx);
|
|
544
|
+
});
|
|
545
|
+
const idsTaken = out
|
|
546
|
+
.map((r) => (r && typeof r.id === 'string' ? r.id : ''))
|
|
547
|
+
.filter((s) => /^H\d+$/.test(s))
|
|
548
|
+
.map((s) => parseInt(s.slice(1), 10));
|
|
549
|
+
let nextId = (idsTaken.length === 0 ? 0 : Math.max(...idsTaken)) + 1;
|
|
550
|
+
for (const row of patch) {
|
|
551
|
+
if (!row || typeof row !== 'object') continue;
|
|
552
|
+
if (typeof row.id === 'string' && byId.has(row.id)) {
|
|
553
|
+
out[byId.get(row.id)] = { ...out[byId.get(row.id)], ...row };
|
|
554
|
+
} else if (typeof row.id === 'string') {
|
|
555
|
+
out.push({ ...row });
|
|
556
|
+
byId.set(row.id, out.length - 1);
|
|
557
|
+
// Advance nextId past any explicitly-appended H<N> id so that
|
|
558
|
+
// subsequent auto-id rows don't collide with it.
|
|
559
|
+
if (/^H\d+$/.test(row.id)) {
|
|
560
|
+
const n = parseInt(row.id.slice(1), 10);
|
|
561
|
+
if (n >= nextId) nextId = n + 1;
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
const id = `H${nextId++}`;
|
|
565
|
+
out.push({ id, ...row });
|
|
566
|
+
byId.set(id, out.length - 1);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return out;
|
|
570
|
+
}
|