@ijfw/memory-server 1.4.4 → 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 +1016 -17
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +5 -1
- package/src/dashboard-server.js +73 -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 +26 -2
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +201 -6
- 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 +38 -3
- 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 +84 -17
- package/src/orchestrator/subagent-telemetry.js +452 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +200 -16
- package/src/orchestrator/wave-state.js +332 -23
- 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 +94 -17
- 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,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime-loop.js — v1.5.0-major S02: callable runtime wrapper for the
|
|
3
|
+
* v1.4.4 N2 status protocol. Closes the "discipline-in-markdown" gap by
|
|
4
|
+
* giving the orchestrator-LLM an MCP tool to invoke per subagent report,
|
|
5
|
+
* instead of reading SKILL.md and hoping it remembers to call the right
|
|
6
|
+
* thing in the right order.
|
|
7
|
+
*
|
|
8
|
+
* Pipeline (per subagent message):
|
|
9
|
+
* reviewSubagentReport(reportText, ctx)
|
|
10
|
+
* 1. parseAgentReport(reportText) -- status-protocol.js N2
|
|
11
|
+
* 2. handleStatus(parsed, dispatchTs, ctx)
|
|
12
|
+
* 3. return { action, ...decision, parsed? }
|
|
13
|
+
*
|
|
14
|
+
* Failure modes are explicit, never thrown:
|
|
15
|
+
* - ProtocolViolation -> { action: 'redispatch_needs_context',
|
|
16
|
+
* missing: 'protocol-violation', error, raw }
|
|
17
|
+
* - Stale commit -> { action: 'redispatch_needs_context',
|
|
18
|
+
* missing: 'commit-before-report' } (from handleStatus)
|
|
19
|
+
*
|
|
20
|
+
* The MCP tool wrapper in server.js passes projectRoot from cwd.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { parseAgentReport, handleStatus, ProtocolViolation } from './status-protocol.js';
|
|
24
|
+
// v1.5.0 N4.obs M1: ensure a trace_id is minted at the start of the orchestrator
|
|
25
|
+
// loop so every downstream checkpoint/receipt/session row can be rolled up by
|
|
26
|
+
// session in the dashboard.
|
|
27
|
+
import { ensureTraceId } from '../observability/trace-id.js';
|
|
28
|
+
// v1.5.0 audit-MED-work-M1 / M3: resume_preference config loader + composable
|
|
29
|
+
// termination conditions.
|
|
30
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
import { defaultTermination } from './termination.js';
|
|
33
|
+
// v1.5.0 wire-W1.A: opt-in repo-map prefix for subagent dispatch / redispatch.
|
|
34
|
+
// Folds the importance-ranked file summary in front of the brief when
|
|
35
|
+
// IJFW_REPO_MAP=1 is set. Default off so existing flows are byte-identical.
|
|
36
|
+
import { buildRepoMap, compactBriefForSubagent } from '../lib/repo-map.js';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Review a subagent's report through the v1.4.4 4-value protocol.
|
|
40
|
+
* Returns a route decision object the orchestrator should act on.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} reportText - the subagent's final message
|
|
43
|
+
* @param {object} ctx
|
|
44
|
+
* @param {number} ctx.dispatchTimestamp - Unix seconds at dispatch
|
|
45
|
+
* @param {string} [ctx.branch] - dispatched branch (for branch-tuple freshness check)
|
|
46
|
+
* @param {string} ctx.projectRoot - absolute path of project root for git commands
|
|
47
|
+
* @returns {{ action: string, parsed?: object, missing?: string, error?: string, raw?: string, [k: string]: unknown }}
|
|
48
|
+
*/
|
|
49
|
+
export function reviewSubagentReport(reportText, ctx) {
|
|
50
|
+
// v1.5.0 N4.obs M1: mint (or adopt) a session trace_id on the first
|
|
51
|
+
// orchestrator call. Idempotent -- subsequent calls reuse the cached id, and
|
|
52
|
+
// a subagent inheriting via IJFW_TRACE_ID env keeps the parent's id.
|
|
53
|
+
ensureTraceId();
|
|
54
|
+
if (typeof reportText !== 'string' || reportText.length === 0) {
|
|
55
|
+
return {
|
|
56
|
+
action: 'redispatch_needs_context',
|
|
57
|
+
missing: 'protocol-violation',
|
|
58
|
+
error: 'empty or non-string reportText',
|
|
59
|
+
raw: String(reportText ?? ''),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (!ctx || typeof ctx !== 'object') {
|
|
63
|
+
throw new TypeError('reviewSubagentReport: ctx is required');
|
|
64
|
+
}
|
|
65
|
+
if (typeof ctx.dispatchTimestamp !== 'number' || !Number.isFinite(ctx.dispatchTimestamp)) {
|
|
66
|
+
throw new TypeError('reviewSubagentReport: ctx.dispatchTimestamp must be a finite number (unix seconds)');
|
|
67
|
+
}
|
|
68
|
+
if (typeof ctx.projectRoot !== 'string' || ctx.projectRoot.length === 0) {
|
|
69
|
+
throw new TypeError('reviewSubagentReport: ctx.projectRoot is required');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = parseAgentReport(reportText);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err instanceof ProtocolViolation) {
|
|
77
|
+
return {
|
|
78
|
+
action: 'redispatch_needs_context',
|
|
79
|
+
missing: 'protocol-violation',
|
|
80
|
+
error: err.reason,
|
|
81
|
+
raw: err.raw,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If the parsed branch contradicts the dispatched branch, prefer the parsed
|
|
88
|
+
// (agent-reported) value -- handleStatus uses it for the branch-tuple check.
|
|
89
|
+
// The ctx.branch is informational; we don't override what the agent said.
|
|
90
|
+
const decision = handleStatus(parsed, ctx.dispatchTimestamp, { projectRoot: ctx.projectRoot });
|
|
91
|
+
return { ...decision, parsed };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Cross-AI resume routing (v1.5.0 W12-C N02).
|
|
96
|
+
*
|
|
97
|
+
* When a subagent truncates mid-task, the orchestrator can resume the same
|
|
98
|
+
* checkpoint on a *different* AI to avoid the same context-window/format
|
|
99
|
+
* failure mode. These helpers encode the routing matrix + brief composition
|
|
100
|
+
* so the orchestrator-LLM doesn't have to remember it.
|
|
101
|
+
*
|
|
102
|
+
* The matrix is deliberately small and hand-tuned: each entry picks an
|
|
103
|
+
* alternate AI whose context window / output style differs from the one
|
|
104
|
+
* that truncated. We never reselect the truncated AI itself.
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
// Built-in defaults. v1.5.0 audit-MED-work-M1 promoted this to a config-
|
|
108
|
+
// readable field on `.ijfw/swarm.json::resume_preference` so rosters that
|
|
109
|
+
// include `opencode`, `aider`, `copilot`, etc. get cross-AI resume too
|
|
110
|
+
// instead of falling through to escalate_to_user.
|
|
111
|
+
const DEFAULT_RESUME_PREFERENCE = {
|
|
112
|
+
claude: ['gemini', 'codex'],
|
|
113
|
+
gemini: ['claude', 'codex'],
|
|
114
|
+
codex: ['claude', 'gemini'],
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Cache one read per projectRoot per process — `selectResumeAI` is hot on the
|
|
118
|
+
// orchestrator critical path and re-reading swarm.json every call wastes I/O.
|
|
119
|
+
const _resumePrefCache = new Map();
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Load `resume_preference` from `<projectRoot>/.ijfw/swarm.json` if present,
|
|
123
|
+
* shallow-merge over the built-in defaults. Missing file / malformed JSON /
|
|
124
|
+
* missing field → defaults silently (this is advisory routing, never throws).
|
|
125
|
+
*
|
|
126
|
+
* Result shape: `{ [truncatedAI]: string[] }`. Keys NOT in defaults are kept
|
|
127
|
+
* (so a project with `opencode: ['claude','gemini']` can have its entry
|
|
128
|
+
* looked up by selectResumeAI), and entries in defaults stay unless the
|
|
129
|
+
* config explicitly overrides them with an array.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} [projectRoot]
|
|
132
|
+
* @returns {Record<string, string[]>}
|
|
133
|
+
*/
|
|
134
|
+
export function loadResumePreference(projectRoot) {
|
|
135
|
+
if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
|
|
136
|
+
return { ...DEFAULT_RESUME_PREFERENCE };
|
|
137
|
+
}
|
|
138
|
+
if (_resumePrefCache.has(projectRoot)) {
|
|
139
|
+
return _resumePrefCache.get(projectRoot);
|
|
140
|
+
}
|
|
141
|
+
const merged = { ...DEFAULT_RESUME_PREFERENCE };
|
|
142
|
+
try {
|
|
143
|
+
const swarmPath = join(projectRoot, '.ijfw', 'swarm.json');
|
|
144
|
+
if (existsSync(swarmPath)) {
|
|
145
|
+
const raw = JSON.parse(readFileSync(swarmPath, 'utf8'));
|
|
146
|
+
const cfg = raw && typeof raw === 'object' ? raw.resume_preference : null;
|
|
147
|
+
if (cfg && typeof cfg === 'object' && !Array.isArray(cfg)) {
|
|
148
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
149
|
+
if (typeof k !== 'string' || k.length === 0) continue;
|
|
150
|
+
if (!Array.isArray(v)) continue;
|
|
151
|
+
const list = v.filter((x) => typeof x === 'string' && x.length > 0);
|
|
152
|
+
if (list.length > 0) merged[k] = list;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch { /* advisory — fall back to defaults */ }
|
|
157
|
+
_resumePrefCache.set(projectRoot, merged);
|
|
158
|
+
return merged;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Test-only helper to clear the resume-preference cache. Used by tests that
|
|
163
|
+
* mutate `.ijfw/swarm.json` and re-call `selectResumeAI` in the same process.
|
|
164
|
+
* @internal
|
|
165
|
+
*/
|
|
166
|
+
export function _resetResumePrefCache() {
|
|
167
|
+
_resumePrefCache.clear();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Kept for backwards compat with any direct importers.
|
|
171
|
+
// eslint-disable-next-line no-unused-vars -- exported binding read by external consumers; keep for backcompat
|
|
172
|
+
const RESUME_PREFERENCE = DEFAULT_RESUME_PREFERENCE;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Pick a resume AI for a truncated subagent.
|
|
176
|
+
*
|
|
177
|
+
* @param {object} args
|
|
178
|
+
* @param {string} args.truncatedAI - AI that truncated ('claude' | 'gemini' | 'codex')
|
|
179
|
+
* @param {string[]} [args.available] - AIs the orchestrator can dispatch to
|
|
180
|
+
* @param {string} [args.lastFailureReason] - e.g. 'context_window'
|
|
181
|
+
* @returns {string|null} - resume target, or null when blocked
|
|
182
|
+
*/
|
|
183
|
+
export function selectResumeAI({
|
|
184
|
+
truncatedAI,
|
|
185
|
+
available = ['claude', 'gemini', 'codex'],
|
|
186
|
+
lastFailureReason,
|
|
187
|
+
projectRoot,
|
|
188
|
+
} = {}) {
|
|
189
|
+
if (typeof truncatedAI !== 'string' || truncatedAI.length === 0) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// gemini already has the largest practical context window of the three.
|
|
194
|
+
// If it truncated *because of* context-window pressure, no alternative
|
|
195
|
+
// gives us a larger window -- escalate instead of pretending to resume.
|
|
196
|
+
if (lastFailureReason === 'context_window' && truncatedAI === 'gemini') {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// v1.5.0 audit-MED-work-M1: read resume_preference from swarm.json with a
|
|
201
|
+
// fall-through to DEFAULT_RESUME_PREFERENCE. Projects with `opencode` /
|
|
202
|
+
// `aider` / `copilot` in their roster get cross-AI resume routing instead
|
|
203
|
+
// of an escalate_to_user black hole.
|
|
204
|
+
const prefMap = loadResumePreference(projectRoot);
|
|
205
|
+
const preferred = prefMap[truncatedAI] || [];
|
|
206
|
+
for (const candidate of preferred) {
|
|
207
|
+
if (candidate === truncatedAI) continue;
|
|
208
|
+
if (available.includes(candidate)) {
|
|
209
|
+
return candidate;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Build a resume brief composing the original spec + checkpoint state.
|
|
217
|
+
* Intentionally omits the Step 0 workspace-setup boilerplate: the resume
|
|
218
|
+
* agent inherits the branch + worktree from the first attempt and skips it.
|
|
219
|
+
*
|
|
220
|
+
* @param {object} args
|
|
221
|
+
* @param {string} args.originalSpec - original task brief, verbatim
|
|
222
|
+
* @param {object} args.checkpoint - { filesWritten?, commitSha?, partialProgress? }
|
|
223
|
+
* @param {string} args.fromAI - AI that truncated
|
|
224
|
+
* @param {string} args.toAI - AI taking over
|
|
225
|
+
* @returns {string}
|
|
226
|
+
*/
|
|
227
|
+
export function buildResumeBrief({ originalSpec, checkpoint = {}, fromAI, toAI } = {}) {
|
|
228
|
+
const spec = typeof originalSpec === 'string' ? originalSpec : '';
|
|
229
|
+
const files = Array.isArray(checkpoint.filesWritten) ? checkpoint.filesWritten : [];
|
|
230
|
+
const sha = typeof checkpoint.commitSha === 'string' ? checkpoint.commitSha : '';
|
|
231
|
+
const partial = typeof checkpoint.partialProgress === 'string' ? checkpoint.partialProgress : '';
|
|
232
|
+
|
|
233
|
+
const filesLine = files.length > 0 ? ` Files written: ${files.join(', ')}` : ' Files written: (none recorded)';
|
|
234
|
+
const shaLine = sha ? ` Commit: ${sha}` : ' Commit: (none yet)';
|
|
235
|
+
const partialLine = partial ? ` Partial progress: ${partial}` : ' Partial progress: (none recorded)';
|
|
236
|
+
|
|
237
|
+
// r15-M6: estimate brief size and surface a context-window advisory. The
|
|
238
|
+
// receiving model may have a SMALLER window than the one that truncated
|
|
239
|
+
// (selectResumeAI refuses gemini→larger when reason is context_window, but
|
|
240
|
+
// it can't know the receiver's exact window from here). Tell the receiver
|
|
241
|
+
// to summarise rather than re-quote if the prior context approaches its cap.
|
|
242
|
+
const approxTokens = Math.ceil((spec.length + filesLine.length + shaLine.length + partialLine.length) / 4);
|
|
243
|
+
const budgetLine = `Approx prior-context tokens: ~${approxTokens}. If this brief plus your reply would exceed your context window, summarise the prior agent's "Files written" + "Partial progress" lines instead of quoting verbatim and proceed.`;
|
|
244
|
+
|
|
245
|
+
return [
|
|
246
|
+
spec,
|
|
247
|
+
'',
|
|
248
|
+
'---',
|
|
249
|
+
`RESUME CONTEXT — Prior agent (${fromAI}) truncated. Already done:`,
|
|
250
|
+
filesLine,
|
|
251
|
+
shaLine,
|
|
252
|
+
partialLine,
|
|
253
|
+
'',
|
|
254
|
+
budgetLine,
|
|
255
|
+
'',
|
|
256
|
+
`You are ${toAI}. Continue from here. Do NOT redo completed work.`,
|
|
257
|
+
'Skip workspace setup (Step 0) -- branch + worktree already exist.',
|
|
258
|
+
].join('\n');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Decide what to do when a subagent report indicates truncation.
|
|
263
|
+
*
|
|
264
|
+
* @param {object} args
|
|
265
|
+
* @param {object} args.parsed - parsed report (must carry ai + reason if known)
|
|
266
|
+
* @param {object} args.ctx - orchestrator context; ctx.checkpoint may be present
|
|
267
|
+
* @param {string[]} [args.available] - AIs available to dispatch
|
|
268
|
+
* @returns {{action:'resume_with_alt_ai', toAI:string, brief:string}
|
|
269
|
+
* | {action:'escalate_to_user', reason:string}}
|
|
270
|
+
*/
|
|
271
|
+
export function handleTruncation({ parsed = {}, ctx = {}, available = ['claude', 'gemini', 'codex'] } = {}) {
|
|
272
|
+
const truncatedAI = typeof parsed.ai === 'string' && parsed.ai.length > 0 ? parsed.ai : 'claude';
|
|
273
|
+
const lastFailureReason = parsed.reason;
|
|
274
|
+
const projectRoot = typeof ctx.projectRoot === 'string' ? ctx.projectRoot : undefined;
|
|
275
|
+
const toAI = selectResumeAI({ truncatedAI, available, lastFailureReason, projectRoot });
|
|
276
|
+
|
|
277
|
+
if (!toAI) {
|
|
278
|
+
return {
|
|
279
|
+
action: 'escalate_to_user',
|
|
280
|
+
reason: lastFailureReason === 'context_window' && truncatedAI === 'gemini'
|
|
281
|
+
? 'context_window_exceeded_on_largest_ai'
|
|
282
|
+
: 'no_alternate_ai_available',
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const checkpoint = ctx.checkpoint && typeof ctx.checkpoint === 'object' ? ctx.checkpoint : {};
|
|
287
|
+
const originalSpec = typeof ctx.originalSpec === 'string' ? ctx.originalSpec : '';
|
|
288
|
+
const brief = buildResumeBrief({ originalSpec, checkpoint, fromAI: truncatedAI, toAI });
|
|
289
|
+
|
|
290
|
+
return { action: 'resume_with_alt_ai', toAI, brief };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// v1.5.0 wire-W1.A — opt-in repo-map prefix for subagent (re)dispatch briefs
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
//
|
|
297
|
+
// Production wire-up for `mcp-server/src/lib/repo-map.js`. The orchestrator-LLM
|
|
298
|
+
// calls the `ijfw_state` MCP tool with `verb: 'subagent.post-done'` after every
|
|
299
|
+
// subagent turn (v1.5.0 T13 absorbed the retired `ijfw_subagent_post_done`
|
|
300
|
+
// tool). When the route decision is a redispatch, the LLM composes a NEW brief
|
|
301
|
+
// for the next subagent.
|
|
302
|
+
// With this wire active, the response payload carries `repoMapPrefix` — a
|
|
303
|
+
// pre-built importance-ranked file summary the LLM prepends to the redispatch
|
|
304
|
+
// brief, so the downstream subagent doesn't have to crawl the tree.
|
|
305
|
+
//
|
|
306
|
+
// Default OFF. Activates only when `IJFW_REPO_MAP=1` is set in the calling
|
|
307
|
+
// process env AND a valid `projectRoot` is supplied. Pure no-op on miss.
|
|
308
|
+
//
|
|
309
|
+
// Failure modes are explicit: any throw inside buildRepoMap (permissions,
|
|
310
|
+
// unreadable dirs, etc.) is swallowed and yields an empty prefix. This lets
|
|
311
|
+
// the orchestrator-LLM remain on the existing redispatch path without
|
|
312
|
+
// degrading.
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Build an opt-in repo-map prefix block for a subagent dispatch brief.
|
|
316
|
+
* Returns '' when env opt-in is missing OR projectRoot is invalid OR the
|
|
317
|
+
* map build throws. Never throws back to the caller.
|
|
318
|
+
*
|
|
319
|
+
* @param {object} args
|
|
320
|
+
* @param {string} [args.projectRoot]
|
|
321
|
+
* @param {object} [args.env] defaults to process.env
|
|
322
|
+
* @param {number} [args.budgetTokens] default 1000
|
|
323
|
+
* @returns {Promise<string>} empty string when disabled or on error
|
|
324
|
+
*/
|
|
325
|
+
export async function buildSubagentRepoMapPrefix({ projectRoot, env = process.env, budgetTokens } = {}) {
|
|
326
|
+
if (!env || env.IJFW_REPO_MAP !== '1') return '';
|
|
327
|
+
if (typeof projectRoot !== 'string' || projectRoot.length === 0) return '';
|
|
328
|
+
try {
|
|
329
|
+
const budget = (typeof budgetTokens === 'number' && budgetTokens > 0) ? budgetTokens : 1000;
|
|
330
|
+
const repoMap = buildRepoMap({ rootDir: projectRoot, budgetTokens: budget });
|
|
331
|
+
// compactBriefForSubagent with an empty baseBrief returns just the
|
|
332
|
+
// header/footer-wrapped prefix block. We surface that as a standalone
|
|
333
|
+
// string the orchestrator-LLM splices in front of its own brief.
|
|
334
|
+
const { brief } = compactBriefForSubagent({ baseBrief: '', repoMap, maxPrefixTokens: budget });
|
|
335
|
+
return typeof brief === 'string' ? brief : '';
|
|
336
|
+
} catch {
|
|
337
|
+
// buildRepoMap throws on missing/inaccessible rootDir; on any error we
|
|
338
|
+
// fall back to "no prefix" rather than corrupt the redispatch payload.
|
|
339
|
+
return '';
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Async wrapper around `reviewSubagentReport`. Returns the same decision shape
|
|
345
|
+
* but attaches `repoMapPrefix` (string) to any redispatch action when the
|
|
346
|
+
* env opt-in is on. The orchestrator-LLM consumes the prefix as the leading
|
|
347
|
+
* block of the next subagent's brief.
|
|
348
|
+
*
|
|
349
|
+
* @param {string} reportText
|
|
350
|
+
* @param {object} ctx - same shape as reviewSubagentReport
|
|
351
|
+
* @param {object} [env] - defaults to process.env
|
|
352
|
+
* @returns {Promise<object>} - decision (sync output + optional prefix)
|
|
353
|
+
*/
|
|
354
|
+
export async function reviewSubagentReportWithRepoMap(reportText, ctx, env = process.env) {
|
|
355
|
+
const decision = reviewSubagentReport(reportText, ctx);
|
|
356
|
+
if (decision && (decision.action === 'redispatch_needs_context' || decision.action === 'redispatch_with_context')) {
|
|
357
|
+
decision.repoMapPrefix = await buildSubagentRepoMapPrefix({
|
|
358
|
+
projectRoot: ctx?.projectRoot,
|
|
359
|
+
env,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return decision;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Async variant of `handleTruncation` that, when the env opt-in is set,
|
|
367
|
+
* prepends the repo-map prefix to the resume brief. Falls back byte-identical
|
|
368
|
+
* to sync handleTruncation when the prefix is empty.
|
|
369
|
+
*
|
|
370
|
+
* @param {object} args - same shape as handleTruncation, plus env
|
|
371
|
+
* @returns {Promise<object>}
|
|
372
|
+
*/
|
|
373
|
+
export async function handleTruncationWithRepoMap({ parsed = {}, ctx = {}, available, env = process.env } = {}) {
|
|
374
|
+
const decision = handleTruncation({ parsed, ctx, available });
|
|
375
|
+
if (decision && decision.action === 'resume_with_alt_ai' && typeof decision.brief === 'string') {
|
|
376
|
+
const prefix = await buildSubagentRepoMapPrefix({ projectRoot: ctx?.projectRoot, env });
|
|
377
|
+
if (prefix.length > 0) {
|
|
378
|
+
decision.brief = prefix + decision.brief;
|
|
379
|
+
decision.repoMapApplied = true;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return decision;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Generic iterative loop runner with a composable termination predicate
|
|
387
|
+
* (v1.5.0 audit-MED-work-M3). Closes the "MaxAttempts is the only stop
|
|
388
|
+
* rule" gap by letting callers compose WallClockTimeout, TokenBudget,
|
|
389
|
+
* FindingSeverity, etc. via the `or` / `and` combinators in
|
|
390
|
+
* `./termination.js`.
|
|
391
|
+
*
|
|
392
|
+
* The loop calls `step(iter, state)` on each iteration, expecting either:
|
|
393
|
+
* - `{ done: true, result }` — natural completion; loop returns `{result}`
|
|
394
|
+
* - `{ done: false, state?: object }` — keep iterating; new state merged in
|
|
395
|
+
*
|
|
396
|
+
* If the termination predicate fires before `done: true`, the loop returns
|
|
397
|
+
* `{ terminated: true, reason: 'termination', iter, state }`.
|
|
398
|
+
*
|
|
399
|
+
* The default predicate is MaxAttempts(3), matching the v1.4.4 N3 cap.
|
|
400
|
+
*
|
|
401
|
+
* @param {object} args
|
|
402
|
+
* @param {(iter: number, state: object) => Promise<{done:boolean, result?:unknown, state?:object}>} args.step
|
|
403
|
+
* @param {object} [args.initialState] starting state object (shallow-merged with step output)
|
|
404
|
+
* @param {(iter:number, state:object) => boolean} [args.termination]
|
|
405
|
+
* @returns {Promise<{result?:unknown, terminated?:boolean, reason?:string, iter:number, state:object}>}
|
|
406
|
+
*/
|
|
407
|
+
export async function runLoop({ step, initialState = {}, termination } = {}) {
|
|
408
|
+
if (typeof step !== 'function') {
|
|
409
|
+
throw new TypeError('runLoop: step is required');
|
|
410
|
+
}
|
|
411
|
+
const stop = typeof termination === 'function' ? termination : defaultTermination();
|
|
412
|
+
let state = { ...initialState };
|
|
413
|
+
let iter = 0;
|
|
414
|
+
// Sentinel cap so a broken `termination` predicate can't pin the loop.
|
|
415
|
+
const HARD_CAP = 10_000;
|
|
416
|
+
while (iter < HARD_CAP) {
|
|
417
|
+
const out = await step(iter, state);
|
|
418
|
+
if (out && out.state && typeof out.state === 'object') {
|
|
419
|
+
state = { ...state, ...out.state };
|
|
420
|
+
}
|
|
421
|
+
if (out && out.done) {
|
|
422
|
+
return { result: out.result, iter, state };
|
|
423
|
+
}
|
|
424
|
+
if (stop(iter, state)) {
|
|
425
|
+
return { terminated: true, reason: 'termination', iter, state };
|
|
426
|
+
}
|
|
427
|
+
iter += 1;
|
|
428
|
+
}
|
|
429
|
+
return { terminated: true, reason: 'hard-cap', iter, state };
|
|
430
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// mcp-server/src/orchestrator/skill-telemetry-sink.js
|
|
2
|
+
// IJFW v1.5.0 -- state-SDK telemetry.record -> skill_telemetry shim.
|
|
3
|
+
//
|
|
4
|
+
// Maps the existing state-SDK telemetry.record payload shape into the
|
|
5
|
+
// skill_telemetry table. Payload shape (per state-sdk.js telemetry.record):
|
|
6
|
+
// { kind, dedupKey, metrics }
|
|
7
|
+
// When kind === 'skill.execution' we expect metrics to carry:
|
|
8
|
+
// { skill_id, session_id?, outcome, latency_ms?, created_at? }
|
|
9
|
+
// Anything else is a clean skip — the generic telemetry.record verb keeps
|
|
10
|
+
// its existing append-to-telemetry-file behavior regardless of this sink.
|
|
11
|
+
|
|
12
|
+
import { recordSkillExecution } from './skill-telemetry.js';
|
|
13
|
+
|
|
14
|
+
export function sinkSkillTelemetry(db, payload) {
|
|
15
|
+
if (!payload || payload.kind !== 'skill.execution') return { skipped: true };
|
|
16
|
+
const m = payload.metrics || {};
|
|
17
|
+
const skill_id = m.skill_id;
|
|
18
|
+
if (!skill_id) return { skipped: true, reason: 'no_skill_id' };
|
|
19
|
+
recordSkillExecution(db, {
|
|
20
|
+
skill_id,
|
|
21
|
+
session_id: m.session_id || null,
|
|
22
|
+
outcome: m.outcome || 'success',
|
|
23
|
+
latency_ms: typeof m.latency_ms === 'number' ? m.latency_ms : null,
|
|
24
|
+
created_at: m.created_at || Date.now(),
|
|
25
|
+
});
|
|
26
|
+
return { skipped: false };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default { sinkSkillTelemetry };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// mcp-server/src/orchestrator/skill-telemetry.js
|
|
2
|
+
// IJFW v1.5.0 -- skills telemetry recorder + top-K reader.
|
|
3
|
+
|
|
4
|
+
export function recordSkillExecution(db, {
|
|
5
|
+
skill_id,
|
|
6
|
+
session_id = null,
|
|
7
|
+
outcome,
|
|
8
|
+
latency_ms = null,
|
|
9
|
+
created_at = Date.now(),
|
|
10
|
+
} = {}) {
|
|
11
|
+
if (!skill_id || !outcome) throw new Error('recordSkillExecution: skill_id and outcome required');
|
|
12
|
+
if (!['success', 'failure', 'aborted'].includes(outcome)) {
|
|
13
|
+
throw new Error(`recordSkillExecution: invalid outcome '${outcome}'`);
|
|
14
|
+
}
|
|
15
|
+
db.prepare(
|
|
16
|
+
`INSERT INTO skill_telemetry (skill_id, session_id, outcome, latency_ms, created_at)
|
|
17
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
18
|
+
).run(skill_id, session_id, outcome, latency_ms, created_at);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function topKSuccessfulSkills(db, { k = 5, since = null } = {}) {
|
|
22
|
+
const params = [];
|
|
23
|
+
let whereSince = '';
|
|
24
|
+
if (since !== null) { whereSince = 'AND created_at >= ?'; params.push(since); }
|
|
25
|
+
return db
|
|
26
|
+
.prepare(
|
|
27
|
+
`SELECT skill_id, COUNT(*) AS success_count, MAX(created_at) AS last_success_at
|
|
28
|
+
FROM skill_telemetry
|
|
29
|
+
WHERE outcome = 'success' ${whereSince}
|
|
30
|
+
GROUP BY skill_id
|
|
31
|
+
ORDER BY success_count DESC, last_success_at DESC
|
|
32
|
+
LIMIT ?`,
|
|
33
|
+
)
|
|
34
|
+
.all(...params, k);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default { recordSkillExecution, topKSuccessfulSkills };
|