open-multi-agent-kit 0.96.2 → 0.97.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/CHANGELOG.md +16 -0
- package/README.md +4 -2
- package/dist/core/agent-session.d.ts +9 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +84 -7
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/attachment-store.d.ts +39 -0
- package/dist/core/attachment-store.d.ts.map +1 -0
- package/dist/core/attachment-store.js +81 -0
- package/dist/core/attachment-store.js.map +1 -0
- package/dist/core/command-safety.d.ts +8 -0
- package/dist/core/command-safety.d.ts.map +1 -1
- package/dist/core/command-safety.js +140 -14
- package/dist/core/command-safety.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +6 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +35 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/context-budget-governor-v2.d.ts +1 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +1 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +2 -0
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.js +3 -0
- package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +2 -1
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-scoring.d.ts +17 -0
- package/dist/core/context-budget-v2-scoring.d.ts.map +1 -1
- package/dist/core/context-budget-v2-scoring.js +25 -8
- package/dist/core/context-budget-v2-scoring.js.map +1 -1
- package/dist/core/context-budget-v2-selection.d.ts +2 -2
- package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
- package/dist/core/context-budget-v2-selection.js +18 -1
- package/dist/core/context-budget-v2-selection.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +17 -0
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js +15 -0
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/extensions/builtin/command-safety-gate.d.ts +3 -1
- package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
- package/dist/core/extensions/builtin/command-safety-gate.js +7 -0
- package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
- package/dist/core/extensions/types.d.ts +12 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-registry.d.ts +8 -3
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +71 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/package-doctor-source-scan.d.ts +9 -0
- package/dist/core/package-doctor-source-scan.d.ts.map +1 -1
- package/dist/core/package-doctor-source-scan.js +77 -4
- package/dist/core/package-doctor-source-scan.js.map +1 -1
- package/dist/core/prompt-attachment.d.ts +51 -0
- package/dist/core/prompt-attachment.d.ts.map +1 -0
- package/dist/core/prompt-attachment.js +186 -0
- package/dist/core/prompt-attachment.js.map +1 -0
- package/dist/core/provider-resilience.d.ts +28 -0
- package/dist/core/provider-resilience.d.ts.map +1 -1
- package/dist/core/provider-resilience.js +50 -0
- package/dist/core/provider-resilience.js.map +1 -1
- package/dist/core/provider-retry.d.ts +1 -1
- package/dist/core/provider-retry.d.ts.map +1 -1
- package/dist/core/provider-retry.js +1 -1
- package/dist/core/provider-retry.js.map +1 -1
- package/dist/core/provider-usage.d.ts.map +1 -1
- package/dist/core/provider-usage.js +68 -2
- package/dist/core/provider-usage.js.map +1 -1
- package/dist/core/reasoning-router-promotion.d.ts +84 -0
- package/dist/core/reasoning-router-promotion.d.ts.map +1 -0
- package/dist/core/reasoning-router-promotion.js +92 -0
- package/dist/core/reasoning-router-promotion.js.map +1 -0
- package/dist/core/session-bash-service.d.ts.map +1 -1
- package/dist/core/session-bash-service.js +4 -3
- package/dist/core/session-bash-service.js.map +1 -1
- package/dist/core/session-termination.d.ts +1 -1
- package/dist/core/session-termination.d.ts.map +1 -1
- package/dist/core/session-termination.js +11 -2
- package/dist/core/session-termination.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +3 -7
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +13 -48
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +1 -34
- package/dist/core/tools/read.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/attachment-strip.d.ts +21 -0
- package/dist/modes/interactive/components/attachment-strip.d.ts.map +1 -0
- package/dist/modes/interactive/components/attachment-strip.js +136 -0
- package/dist/modes/interactive/components/attachment-strip.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts +6 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +26 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.js +12 -1
- package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +23 -4
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +174 -53
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/compaction.md +1 -1
- package/docs/extensions.md +12 -3
- package/docs/index.md +4 -1
- package/docs/metrics.md +74 -2
- package/docs/provider-resilience.md +11 -0
- package/docs/usage.md +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +7 -7
- package/dist/core/tools/omp-pure-seams.d.ts +0 -41
- package/dist/core/tools/omp-pure-seams.d.ts.map +0 -1
- package/dist/core/tools/omp-pure-seams.js +0 -142
- package/dist/core/tools/omp-pure-seams.js.map +0 -1
- package/dist/core/tools/omp-seam-runtime.d.ts +0 -100
- package/dist/core/tools/omp-seam-runtime.d.ts.map +0 -1
- package/dist/core/tools/omp-seam-runtime.js +0 -40
- package/dist/core/tools/omp-seam-runtime.js.map +0 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promotion gate for learned reasoning-router weights.
|
|
3
|
+
*
|
|
4
|
+
* The router already produces a feedback ledger (`router-feedback-collector.ts`)
|
|
5
|
+
* and an offline calibration stack (`scripts/reasoning-router/`: calibration,
|
|
6
|
+
* held-out evaluation, golden diffing, and McNemar's exact test). What was
|
|
7
|
+
* missing is the decision policy that makes automatic promotion trustworthy:
|
|
8
|
+
* without it the loop is open and every weight change is a judgement call.
|
|
9
|
+
*
|
|
10
|
+
* This module is that policy, as a pure total function. It deliberately refuses
|
|
11
|
+
* to promote on anything short of unanimous evidence:
|
|
12
|
+
*
|
|
13
|
+
* - a statistically significant win on the held-in gold set (McNemar's exact
|
|
14
|
+
* test), computed by the caller and passed in;
|
|
15
|
+
* - enough discordant pairs for that test to mean anything;
|
|
16
|
+
* - the candidate — not the baseline — winning those pairs;
|
|
17
|
+
* - no accuracy regression on a held-out split the calibration never saw;
|
|
18
|
+
* - no unreviewed behavior changes surfaced by golden diffing; and
|
|
19
|
+
* - an explicit human approval.
|
|
20
|
+
*
|
|
21
|
+
* "The agent tried again" is not the same as "the harness improved", so a
|
|
22
|
+
* candidate that merely fails to be worse is rejected, and malformed evidence
|
|
23
|
+
* fails closed rather than defaulting to promotion.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Conservative defaults. `minDiscordant` of 20 keeps McNemar from ruling on a
|
|
27
|
+
* handful of pairs; `maxGoldenChanges` of 0 means a reviewer accepts behavior
|
|
28
|
+
* changes by regenerating the golden set, never by tolerating drift.
|
|
29
|
+
*
|
|
30
|
+
* `minHoldout` is 40 because the router gold set freezes 42 holdout rows (6 per
|
|
31
|
+
* class × 7 classes). A higher bar would be unsatisfiable rather than strict,
|
|
32
|
+
* and a gate that can never pass is a gate nobody runs.
|
|
33
|
+
*/
|
|
34
|
+
export const DEFAULT_ROUTER_PROMOTION_POLICY = {
|
|
35
|
+
maxGoldenChanges: 0,
|
|
36
|
+
minDiscordant: 20,
|
|
37
|
+
minHoldout: 40,
|
|
38
|
+
requireHumanApproval: true,
|
|
39
|
+
};
|
|
40
|
+
function isCount(value) {
|
|
41
|
+
return Number.isFinite(value) && value >= 0;
|
|
42
|
+
}
|
|
43
|
+
function isMalformed(evidence) {
|
|
44
|
+
const { goldenChanges, heldIn, holdout } = evidence;
|
|
45
|
+
if (!isCount(heldIn.baselineWins) || !isCount(heldIn.candidateWins))
|
|
46
|
+
return true;
|
|
47
|
+
if (!Number.isFinite(heldIn.pValue) || heldIn.pValue < 0 || heldIn.pValue > 1)
|
|
48
|
+
return true;
|
|
49
|
+
if (!isCount(holdout.baselineCorrect) || !isCount(holdout.candidateCorrect) || !isCount(holdout.total))
|
|
50
|
+
return true;
|
|
51
|
+
if (holdout.baselineCorrect > holdout.total || holdout.candidateCorrect > holdout.total)
|
|
52
|
+
return true;
|
|
53
|
+
return !isCount(goldenChanges);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Decide whether a candidate weight set may replace the active one.
|
|
57
|
+
*
|
|
58
|
+
* Returns every independent blocker rather than the first, so one calibration
|
|
59
|
+
* run reports the full distance to promotion instead of revealing it one
|
|
60
|
+
* rejection at a time. Malformed evidence short-circuits: the gate never
|
|
61
|
+
* reasons about numbers it cannot trust.
|
|
62
|
+
*/
|
|
63
|
+
export function evaluateRouterPromotion(evidence, policy = DEFAULT_ROUTER_PROMOTION_POLICY) {
|
|
64
|
+
if (isMalformed(evidence)) {
|
|
65
|
+
return { blockers: ["malformed_evidence"], promote: false };
|
|
66
|
+
}
|
|
67
|
+
const { goldenChanges, heldIn, holdout, humanApproved } = evidence;
|
|
68
|
+
const blockers = [];
|
|
69
|
+
if (heldIn.baselineWins + heldIn.candidateWins < policy.minDiscordant) {
|
|
70
|
+
blockers.push("insufficient_discordant_pairs");
|
|
71
|
+
}
|
|
72
|
+
if (!heldIn.significant) {
|
|
73
|
+
blockers.push("mcnemar_not_significant");
|
|
74
|
+
}
|
|
75
|
+
if (heldIn.candidateWins <= heldIn.baselineWins) {
|
|
76
|
+
blockers.push("candidate_not_favored");
|
|
77
|
+
}
|
|
78
|
+
if (holdout.total < policy.minHoldout) {
|
|
79
|
+
blockers.push("insufficient_holdout");
|
|
80
|
+
}
|
|
81
|
+
if (holdout.candidateCorrect < holdout.baselineCorrect) {
|
|
82
|
+
blockers.push("holdout_regression");
|
|
83
|
+
}
|
|
84
|
+
if (goldenChanges > policy.maxGoldenChanges) {
|
|
85
|
+
blockers.push("unreviewed_behavior_changes");
|
|
86
|
+
}
|
|
87
|
+
if (policy.requireHumanApproval && !humanApproved) {
|
|
88
|
+
blockers.push("human_approval_missing");
|
|
89
|
+
}
|
|
90
|
+
return { blockers, promote: blockers.length === 0 };
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=reasoning-router-promotion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reasoning-router-promotion.js","sourceRoot":"","sources":["../../src/core/reasoning-router-promotion.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Promotion gate for learned reasoning-router weights.\n *\n * The router already produces a feedback ledger (`router-feedback-collector.ts`)\n * and an offline calibration stack (`scripts/reasoning-router/`: calibration,\n * held-out evaluation, golden diffing, and McNemar's exact test). What was\n * missing is the decision policy that makes automatic promotion trustworthy:\n * without it the loop is open and every weight change is a judgement call.\n *\n * This module is that policy, as a pure total function. It deliberately refuses\n * to promote on anything short of unanimous evidence:\n *\n * - a statistically significant win on the held-in gold set (McNemar's exact\n * test), computed by the caller and passed in;\n * - enough discordant pairs for that test to mean anything;\n * - the candidate — not the baseline — winning those pairs;\n * - no accuracy regression on a held-out split the calibration never saw;\n * - no unreviewed behavior changes surfaced by golden diffing; and\n * - an explicit human approval.\n *\n * \"The agent tried again\" is not the same as \"the harness improved\", so a\n * candidate that merely fails to be worse is rejected, and malformed evidence\n * fails closed rather than defaulting to promotion.\n */\n\n/** Held-in McNemar outcome. Wins are counted in discordant pairs only. */\nexport interface RouterHeldInEvidence {\n\t/** Rows the baseline classified correctly and the candidate got wrong. */\n\treadonly baselineWins: number;\n\t/** Rows the candidate classified correctly and the baseline got wrong. */\n\treadonly candidateWins: number;\n\treadonly pValue: number;\n\treadonly significant: boolean;\n}\n\n/** Accuracy on a split withheld from calibration. */\nexport interface RouterHoldoutEvidence {\n\treadonly baselineCorrect: number;\n\treadonly candidateCorrect: number;\n\treadonly total: number;\n}\n\n/** Everything the gate is allowed to consider. */\nexport interface RouterPromotionEvidence {\n\treadonly heldIn: RouterHeldInEvidence;\n\treadonly holdout: RouterHoldoutEvidence;\n\t/** Golden-diff changes still unreviewed at decision time. */\n\treadonly goldenChanges: number;\n\t/** True only when a human explicitly approved this exact candidate. */\n\treadonly humanApproved: boolean;\n}\n\n/** Tunable thresholds. Loosening these can never authorize a regression. */\nexport interface RouterPromotionPolicy {\n\t/** Minimum discordant pairs before McNemar's verdict is trusted. */\n\treadonly minDiscordant: number;\n\t/** Minimum held-out rows before \"no regression\" is meaningful. */\n\treadonly minHoldout: number;\n\t/** Maximum unreviewed golden-diff changes tolerated. */\n\treadonly maxGoldenChanges: number;\n\treadonly requireHumanApproval: boolean;\n}\n\nexport type RouterPromotionBlocker =\n\t| \"malformed_evidence\"\n\t| \"insufficient_discordant_pairs\"\n\t| \"mcnemar_not_significant\"\n\t| \"candidate_not_favored\"\n\t| \"holdout_regression\"\n\t| \"insufficient_holdout\"\n\t| \"unreviewed_behavior_changes\"\n\t| \"human_approval_missing\";\n\nexport interface RouterPromotionVerdict {\n\treadonly promote: boolean;\n\t/** Every independent reason promotion was refused, in evaluation order. */\n\treadonly blockers: readonly RouterPromotionBlocker[];\n}\n\n/**\n * Conservative defaults. `minDiscordant` of 20 keeps McNemar from ruling on a\n * handful of pairs; `maxGoldenChanges` of 0 means a reviewer accepts behavior\n * changes by regenerating the golden set, never by tolerating drift.\n *\n * `minHoldout` is 40 because the router gold set freezes 42 holdout rows (6 per\n * class × 7 classes). A higher bar would be unsatisfiable rather than strict,\n * and a gate that can never pass is a gate nobody runs.\n */\nexport const DEFAULT_ROUTER_PROMOTION_POLICY: RouterPromotionPolicy = {\n\tmaxGoldenChanges: 0,\n\tminDiscordant: 20,\n\tminHoldout: 40,\n\trequireHumanApproval: true,\n};\n\nfunction isCount(value: number): boolean {\n\treturn Number.isFinite(value) && value >= 0;\n}\n\nfunction isMalformed(evidence: RouterPromotionEvidence): boolean {\n\tconst { goldenChanges, heldIn, holdout } = evidence;\n\tif (!isCount(heldIn.baselineWins) || !isCount(heldIn.candidateWins)) return true;\n\tif (!Number.isFinite(heldIn.pValue) || heldIn.pValue < 0 || heldIn.pValue > 1) return true;\n\tif (!isCount(holdout.baselineCorrect) || !isCount(holdout.candidateCorrect) || !isCount(holdout.total)) return true;\n\tif (holdout.baselineCorrect > holdout.total || holdout.candidateCorrect > holdout.total) return true;\n\treturn !isCount(goldenChanges);\n}\n\n/**\n * Decide whether a candidate weight set may replace the active one.\n *\n * Returns every independent blocker rather than the first, so one calibration\n * run reports the full distance to promotion instead of revealing it one\n * rejection at a time. Malformed evidence short-circuits: the gate never\n * reasons about numbers it cannot trust.\n */\nexport function evaluateRouterPromotion(\n\tevidence: RouterPromotionEvidence,\n\tpolicy: RouterPromotionPolicy = DEFAULT_ROUTER_PROMOTION_POLICY,\n): RouterPromotionVerdict {\n\tif (isMalformed(evidence)) {\n\t\treturn { blockers: [\"malformed_evidence\"], promote: false };\n\t}\n\n\tconst { goldenChanges, heldIn, holdout, humanApproved } = evidence;\n\tconst blockers: RouterPromotionBlocker[] = [];\n\n\tif (heldIn.baselineWins + heldIn.candidateWins < policy.minDiscordant) {\n\t\tblockers.push(\"insufficient_discordant_pairs\");\n\t}\n\tif (!heldIn.significant) {\n\t\tblockers.push(\"mcnemar_not_significant\");\n\t}\n\tif (heldIn.candidateWins <= heldIn.baselineWins) {\n\t\tblockers.push(\"candidate_not_favored\");\n\t}\n\tif (holdout.total < policy.minHoldout) {\n\t\tblockers.push(\"insufficient_holdout\");\n\t}\n\tif (holdout.candidateCorrect < holdout.baselineCorrect) {\n\t\tblockers.push(\"holdout_regression\");\n\t}\n\tif (goldenChanges > policy.maxGoldenChanges) {\n\t\tblockers.push(\"unreviewed_behavior_changes\");\n\t}\n\tif (policy.requireHumanApproval && !humanApproved) {\n\t\tblockers.push(\"human_approval_missing\");\n\t}\n\n\treturn { blockers, promote: blockers.length === 0 };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-bash-service.d.ts","sourceRoot":"","sources":["../../src/core/session-bash-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,KAAK,UAAU,EAA6B,MAAM,oBAAoB,CAAC;AAOhF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,oBAAoB,EAA6B,MAAM,iBAAiB,CAAC;AAE5G,MAAM,WAAW,kBAAkB;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,8FAA8F;IAC9F,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,gEAAgE;IAChE,aAAa,CAAC,EAAE,oBAAoB,CAAC;CACrC;AAED,iGAA+F;AAC/F,MAAM,WAAW,uBAAuB;IACvC,yEAAwE;IACxE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,aAAa,EAAE,mBAAmB,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,uBAAuB,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC9F,QAAQ,CAAC,WAAW,EAAE,MAAM,OAAO,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC9D,QAAQ,CAAC,aAAa,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAChE;;;;OAIG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;CAC5G;AAUD,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,eAAe,CAA8B;IACrD,OAAO,CAAC,mBAAmB,CAA8B;IACzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;IAE9C,YAAY,IAAI,EAAE,sBAAsB,EAEvC;IAEK,WAAW,CAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EACjC,OAAO,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAkGrB;IAED;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE;QAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAoBtG;IAED,mCAAmC;IACnC,SAAS,IAAI,IAAI,CAEhB;IAED,IAAI,aAAa,IAAI,OAAO,CAE3B;IAED,IAAI,sBAAsB,IAAI,OAAO,CAEpC;IAED,+EAA+E;IAC/E,YAAY,IAAI,IAAI,CAOnB;CACD","sourcesContent":["/**\n * Session bash service: command execution, result recording, abort, and the\n * streaming-deferral queue. Extracted from AgentSession so the session class\n * coordinates lifecycle while this owns the bash surface end-to-end.\n *\n * Ordering contract (preserved from the session implementation):\n * - results recorded while the agent is streaming are queued and flushed on\n * turn end (`flushPending`) so tool_use/tool_result ordering never breaks;\n * - `recordBashResult` is idempotent-safe to call from executeBash and from\n * extensions that run bash themselves.\n */\nimport { type BashResult, executeBashWithOperations } from \"./bash-executor.ts\";\nimport { classifyShellCommand } from \"./command-safety.ts\";\nimport {\n\tbuildBlockedBashResult,\n\tevaluateCommandGate,\n\tisCommandSafetyAssumeYesEnabled,\n} from \"./extensions/builtin/command-safety-gate.ts\";\nimport type { LoadoutAccessPolicy } from \"./loadout-access-policy.ts\";\nimport { assertLoadoutAccess, decideLoadoutAccess } from \"./loadout-access-policy.ts\";\nimport type { BashExecutionMessage } from \"./messages.ts\";\nimport type { SessionBashRuntime } from \"./session-bash-runtime.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport { type BashOperations, type BashSandboxPreflight, createLocalBashOperations } from \"./tools/bash.ts\";\n\nexport interface ExecuteBashOptions {\n\texcludeFromContext?: boolean;\n\t/** Optional identifier echoed in `bash_execution_update` events (RPC request correlation). */\n\tid?: string;\n\toperations?: BashOperations;\n\tsafetyGate?: \"headless\";\n\t/** Trusted internal/test override for local bash sandboxing. */\n\tsandboxPolicy?: BashSandboxPreflight;\n}\n\n/** Resource gate grant (roadmap §11/M3): §11.3 blocked payload, permit release, or ungated. */\nexport interface BashResourcePermitGrant {\n\t/** §11.3 structured block JSON; when set the command must not spawn. */\n\treadonly blocked?: string;\n\treadonly release?: () => void;\n}\n\nexport interface SessionBashServiceDeps {\n\treadonly settings: SettingsManager;\n\treadonly runtime: SessionBashRuntime;\n\treadonly loadoutPolicy: LoadoutAccessPolicy | undefined;\n\treadonly getCwd: () => string;\n\treadonly emit: (event: { type: \"bash_execution_update\"; id?: string; delta: string }) => void;\n\treadonly isStreaming: () => boolean;\n\treadonly pushMessage: (message: BashExecutionMessage) => void;\n\treadonly appendMessage: (message: BashExecutionMessage) => void;\n\t/**\n\t * Optional resource governor gate, invoked AFTER command safety (§9.4\n\t * order) and before any shell spawns. The abort signal cancels a queued\n\t * permit wait. Implementations must never throw.\n\t */\n\treadonly acquireResourcePermit?: (command: string, signal: AbortSignal) => Promise<BashResourcePermitGrant>;\n}\n\nfunction isSandboxDeniedError(error: unknown): error is Error {\n\treturn error instanceof Error && error.message.startsWith(\"sandbox: shell denied\");\n}\n\nfunction buildSandboxDeniedBashResult(reason: string): BashResult {\n\treturn { output: reason, exitCode: 1, cancelled: false, truncated: false };\n}\n\nexport class SessionBashService {\n\tprivate abortController: AbortController | undefined;\n\tprivate pendingBashMessages: BashExecutionMessage[] = [];\n\tprivate readonly deps: SessionBashServiceDeps;\n\n\tconstructor(deps: SessionBashServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tasync executeBash(\n\t\tcommand: string,\n\t\tonChunk?: (chunk: string) => void,\n\t\toptions?: ExecuteBashOptions,\n\t): Promise<BashResult> {\n\t\tconst { settings, runtime, loadoutPolicy } = this.deps;\n\t\tconst prefix = settings.getShellCommandPrefix();\n\t\tconst shellPath = settings.getShellPath();\n\t\tconst resolvedCommand = prefix ? `${prefix}\\n${command}` : command;\n\t\tif (loadoutPolicy) {\n\t\t\tassertLoadoutAccess((request) => decideLoadoutAccess(loadoutPolicy, request), {\n\t\t\t\toperation: \"execute\",\n\t\t\t\ttoolName: \"bash\",\n\t\t\t\tcommand: resolvedCommand,\n\t\t\t});\n\t\t}\n\n\t\t// Non-negotiable safety floor for headless callers (RPC bash): hard-deny\n\t\t// block-tier commands before any shell is spawned.\n\t\tif (options?.safetyGate === \"headless\") {\n\t\t\tconst floorVerdict = classifyShellCommand(command);\n\t\t\tif (floorVerdict.risk === \"block\") {\n\t\t\t\tthrow new Error(`OMK §0.1 safety floor blocked bash: [${floorVerdict.rule}] ${floorVerdict.reason}`);\n\t\t\t}\n\t\t}\n\n\t\t// Command-safety parity for non-interactive callers (RPC bash). Interactive\n\t\t// `!`/`!!` bash is gated earlier through the user_bash extension event, which\n\t\t// keeps its prompt-based approval semantics, so it does not pass safetyGate and\n\t\t// is never double-prompted here. confirm/block-tier verdicts deny headlessly;\n\t\t// the EFFECTIVE command (after the shell command prefix) is classified.\n\t\tif (options?.safetyGate === \"headless\") {\n\t\t\tconst decision = await evaluateCommandGate(resolvedCommand, {\n\t\t\t\thasUI: false,\n\t\t\t\theadlessConfirmPolicy: isCommandSafetyAssumeYesEnabled() ? \"allow\" : \"deny\",\n\t\t\t});\n\t\t\tif (decision?.deny) {\n\t\t\t\tconst blocked = buildBlockedBashResult(decision.reason);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t}\n\n\t\tthis.abortController = new AbortController();\n\t\tlet releaseResourcePermit: (() => void) | undefined;\n\n\t\ttry {\n\t\t\t// Resource safety gate (§9.4: command safety above stays authoritative;\n\t\t\t// pressure can only throttle or defer an already-allowed command).\n\t\t\tif (this.deps.acquireResourcePermit) {\n\t\t\t\tconst grant = await this.deps.acquireResourcePermit(resolvedCommand, this.abortController.signal);\n\t\t\t\tif (grant.blocked !== undefined) {\n\t\t\t\t\tconst blocked = buildBlockedBashResult(grant.blocked);\n\t\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\t\treturn blocked;\n\t\t\t\t}\n\t\t\t\treleaseResourcePermit = grant.release;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst operations =\n\t\t\t\t\toptions?.operations ??\n\t\t\t\t\tcreateLocalBashOperations({\n\t\t\t\t\t\tshellPath,\n\t\t\t\t\t\tsandboxPolicy: runtime.sandboxPreflight(options?.sandboxPolicy),\n\t\t\t\t\t});\n\t\t\t\tconst cwd = this.deps.getCwd();\n\t\t\t\tconst onChunkWrapped = (delta: string) => {\n\t\t\t\t\tonChunk?.(delta);\n\t\t\t\t\tthis.deps.emit({ type: \"bash_execution_update\", id: options?.id, delta });\n\t\t\t\t};\n\n\t\t\t\t// Default-on verified path when a session ledger exists; OMK_VERIFIED_BASH=0 rolls back.\n\t\t\t\tconst verified = await runtime.executeVerified({\n\t\t\t\t\tresolvedCommand,\n\t\t\t\t\tcwd,\n\t\t\t\t\tshellPath,\n\t\t\t\t\toperations,\n\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t});\n\t\t\t\tconst result: BashResult =\n\t\t\t\t\tverified ??\n\t\t\t\t\t(await executeBashWithOperations(resolvedCommand, cwd, operations, {\n\t\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\t}));\n\n\t\t\t\tthis.recordBashResult(command, result, options);\n\t\t\t\treturn result;\n\t\t\t} catch (error) {\n\t\t\t\tif (!isSandboxDeniedError(error)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tconst blocked = buildSandboxDeniedBashResult(error.message);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t} finally {\n\t\t\t// §10.3 exactly-once: the pool treats a repeated release as a no-op.\n\t\t\treleaseResourcePermit?.();\n\t\t\tthis.abortController = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Record a bash execution result in session history.\n\t * Used by executeBash and by extensions that handle bash execution themselves.\n\t */\n\trecordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {\n\t\tconst bashMessage: BashExecutionMessage = {\n\t\t\trole: \"bashExecution\",\n\t\t\tcommand,\n\t\t\toutput: result.output,\n\t\t\texitCode: result.exitCode,\n\t\t\tcancelled: result.cancelled,\n\t\t\ttruncated: result.truncated,\n\t\t\tfullOutputPath: result.fullOutputPath,\n\t\t\ttimestamp: Date.now(),\n\t\t\texcludeFromContext: options?.excludeFromContext,\n\t\t};\n\n\t\t// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering\n\t\tif (this.deps.isStreaming()) {\n\t\t\tthis.pendingBashMessages.push(bashMessage);\n\t\t} else {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t}\n\n\t/** Cancel running bash command. */\n\tabortBash(): void {\n\t\tthis.abortController?.abort();\n\t}\n\n\tget isBashRunning(): boolean {\n\t\treturn this.abortController !== undefined;\n\t}\n\n\tget hasPendingBashMessages(): boolean {\n\t\treturn this.pendingBashMessages.length > 0;\n\t}\n\n\t/** Flush queued results after the agent turn completes (ordering contract). */\n\tflushPending(): void {\n\t\tif (this.pendingBashMessages.length === 0) return;\n\t\tfor (const bashMessage of this.pendingBashMessages) {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t\tthis.pendingBashMessages = [];\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-bash-service.d.ts","sourceRoot":"","sources":["../../src/core/session-bash-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,KAAK,UAAU,EAA6B,MAAM,oBAAoB,CAAC;AAQhF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,oBAAoB,EAA6B,MAAM,iBAAiB,CAAC;AAE5G,MAAM,WAAW,kBAAkB;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,8FAA8F;IAC9F,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,gEAAgE;IAChE,aAAa,CAAC,EAAE,oBAAoB,CAAC;CACrC;AAED,iGAA+F;AAC/F,MAAM,WAAW,uBAAuB;IACvC,yEAAwE;IACxE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,aAAa,EAAE,mBAAmB,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,uBAAuB,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC9F,QAAQ,CAAC,WAAW,EAAE,MAAM,OAAO,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC9D,QAAQ,CAAC,aAAa,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAChE;;;;OAIG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;CAC5G;AAUD,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,eAAe,CAA8B;IACrD,OAAO,CAAC,mBAAmB,CAA8B;IACzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;IAE9C,YAAY,IAAI,EAAE,sBAAsB,EAEvC;IAEK,WAAW,CAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EACjC,OAAO,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAmGrB;IAED;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE;QAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAoBtG;IAED,mCAAmC;IACnC,SAAS,IAAI,IAAI,CAEhB;IAED,IAAI,aAAa,IAAI,OAAO,CAE3B;IAED,IAAI,sBAAsB,IAAI,OAAO,CAEpC;IAED,+EAA+E;IAC/E,YAAY,IAAI,IAAI,CAOnB;CACD","sourcesContent":["/**\n * Session bash service: command execution, result recording, abort, and the\n * streaming-deferral queue. Extracted from AgentSession so the session class\n * coordinates lifecycle while this owns the bash surface end-to-end.\n *\n * Ordering contract (preserved from the session implementation):\n * - results recorded while the agent is streaming are queued and flushed on\n * turn end (`flushPending`) so tool_use/tool_result ordering never breaks;\n * - `recordBashResult` is idempotent-safe to call from executeBash and from\n * extensions that run bash themselves.\n */\nimport { type BashResult, executeBashWithOperations } from \"./bash-executor.ts\";\nimport { classifyShellCommand } from \"./command-safety.ts\";\nimport {\n\tbuildBlockedBashResult,\n\tevaluateCommandGate,\n\tisCommandSafetyAssumeYesEnabled,\n\tisCommandSafetyDisabled,\n} from \"./extensions/builtin/command-safety-gate.ts\";\nimport type { LoadoutAccessPolicy } from \"./loadout-access-policy.ts\";\nimport { assertLoadoutAccess, decideLoadoutAccess } from \"./loadout-access-policy.ts\";\nimport type { BashExecutionMessage } from \"./messages.ts\";\nimport type { SessionBashRuntime } from \"./session-bash-runtime.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport { type BashOperations, type BashSandboxPreflight, createLocalBashOperations } from \"./tools/bash.ts\";\n\nexport interface ExecuteBashOptions {\n\texcludeFromContext?: boolean;\n\t/** Optional identifier echoed in `bash_execution_update` events (RPC request correlation). */\n\tid?: string;\n\toperations?: BashOperations;\n\tsafetyGate?: \"headless\";\n\t/** Trusted internal/test override for local bash sandboxing. */\n\tsandboxPolicy?: BashSandboxPreflight;\n}\n\n/** Resource gate grant (roadmap §11/M3): §11.3 blocked payload, permit release, or ungated. */\nexport interface BashResourcePermitGrant {\n\t/** §11.3 structured block JSON; when set the command must not spawn. */\n\treadonly blocked?: string;\n\treadonly release?: () => void;\n}\n\nexport interface SessionBashServiceDeps {\n\treadonly settings: SettingsManager;\n\treadonly runtime: SessionBashRuntime;\n\treadonly loadoutPolicy: LoadoutAccessPolicy | undefined;\n\treadonly getCwd: () => string;\n\treadonly emit: (event: { type: \"bash_execution_update\"; id?: string; delta: string }) => void;\n\treadonly isStreaming: () => boolean;\n\treadonly pushMessage: (message: BashExecutionMessage) => void;\n\treadonly appendMessage: (message: BashExecutionMessage) => void;\n\t/**\n\t * Optional resource governor gate, invoked AFTER command safety (§9.4\n\t * order) and before any shell spawns. The abort signal cancels a queued\n\t * permit wait. Implementations must never throw.\n\t */\n\treadonly acquireResourcePermit?: (command: string, signal: AbortSignal) => Promise<BashResourcePermitGrant>;\n}\n\nfunction isSandboxDeniedError(error: unknown): error is Error {\n\treturn error instanceof Error && error.message.startsWith(\"sandbox: shell denied\");\n}\n\nfunction buildSandboxDeniedBashResult(reason: string): BashResult {\n\treturn { output: reason, exitCode: 1, cancelled: false, truncated: false };\n}\n\nexport class SessionBashService {\n\tprivate abortController: AbortController | undefined;\n\tprivate pendingBashMessages: BashExecutionMessage[] = [];\n\tprivate readonly deps: SessionBashServiceDeps;\n\n\tconstructor(deps: SessionBashServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tasync executeBash(\n\t\tcommand: string,\n\t\tonChunk?: (chunk: string) => void,\n\t\toptions?: ExecuteBashOptions,\n\t): Promise<BashResult> {\n\t\tconst { settings, runtime, loadoutPolicy } = this.deps;\n\t\tconst prefix = settings.getShellCommandPrefix();\n\t\tconst shellPath = settings.getShellPath();\n\t\tconst resolvedCommand = prefix ? `${prefix}\\n${command}` : command;\n\t\tif (loadoutPolicy) {\n\t\t\tassertLoadoutAccess((request) => decideLoadoutAccess(loadoutPolicy, request), {\n\t\t\t\toperation: \"execute\",\n\t\t\t\ttoolName: \"bash\",\n\t\t\t\tcommand: resolvedCommand,\n\t\t\t});\n\t\t}\n\n\t\t// Non-negotiable safety floor for headless callers (RPC bash): hard-deny\n\t\t// block-tier commands before any shell is spawned. YOLO mode opts out of\n\t\t// the entire gate — the floor included — so nothing prompts or denies.\n\t\tif (options?.safetyGate === \"headless\" && !isCommandSafetyDisabled()) {\n\t\t\tconst floorVerdict = classifyShellCommand(command);\n\t\t\tif (floorVerdict.risk === \"block\") {\n\t\t\t\tthrow new Error(`OMK §0.1 safety floor blocked bash: [${floorVerdict.rule}] ${floorVerdict.reason}`);\n\t\t\t}\n\t\t}\n\n\t\t// Command-safety parity for non-interactive callers (RPC bash). Interactive\n\t\t// `!`/`!!` bash is gated earlier through the user_bash extension event, which\n\t\t// keeps its prompt-based approval semantics, so it does not pass safetyGate and\n\t\t// is never double-prompted here. confirm/block-tier verdicts deny headlessly;\n\t\t// the EFFECTIVE command (after the shell command prefix) is classified.\n\t\tif (options?.safetyGate === \"headless\") {\n\t\t\tconst decision = await evaluateCommandGate(resolvedCommand, {\n\t\t\t\thasUI: false,\n\t\t\t\theadlessConfirmPolicy: isCommandSafetyAssumeYesEnabled() ? \"allow\" : \"deny\",\n\t\t\t});\n\t\t\tif (decision?.deny) {\n\t\t\t\tconst blocked = buildBlockedBashResult(decision.reason);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t}\n\n\t\tthis.abortController = new AbortController();\n\t\tlet releaseResourcePermit: (() => void) | undefined;\n\n\t\ttry {\n\t\t\t// Resource safety gate (§9.4: command safety above stays authoritative;\n\t\t\t// pressure can only throttle or defer an already-allowed command).\n\t\t\tif (this.deps.acquireResourcePermit) {\n\t\t\t\tconst grant = await this.deps.acquireResourcePermit(resolvedCommand, this.abortController.signal);\n\t\t\t\tif (grant.blocked !== undefined) {\n\t\t\t\t\tconst blocked = buildBlockedBashResult(grant.blocked);\n\t\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\t\treturn blocked;\n\t\t\t\t}\n\t\t\t\treleaseResourcePermit = grant.release;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst operations =\n\t\t\t\t\toptions?.operations ??\n\t\t\t\t\tcreateLocalBashOperations({\n\t\t\t\t\t\tshellPath,\n\t\t\t\t\t\tsandboxPolicy: runtime.sandboxPreflight(options?.sandboxPolicy),\n\t\t\t\t\t});\n\t\t\t\tconst cwd = this.deps.getCwd();\n\t\t\t\tconst onChunkWrapped = (delta: string) => {\n\t\t\t\t\tonChunk?.(delta);\n\t\t\t\t\tthis.deps.emit({ type: \"bash_execution_update\", id: options?.id, delta });\n\t\t\t\t};\n\n\t\t\t\t// Default-on verified path when a session ledger exists; OMK_VERIFIED_BASH=0 rolls back.\n\t\t\t\tconst verified = await runtime.executeVerified({\n\t\t\t\t\tresolvedCommand,\n\t\t\t\t\tcwd,\n\t\t\t\t\tshellPath,\n\t\t\t\t\toperations,\n\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t});\n\t\t\t\tconst result: BashResult =\n\t\t\t\t\tverified ??\n\t\t\t\t\t(await executeBashWithOperations(resolvedCommand, cwd, operations, {\n\t\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\t}));\n\n\t\t\t\tthis.recordBashResult(command, result, options);\n\t\t\t\treturn result;\n\t\t\t} catch (error) {\n\t\t\t\tif (!isSandboxDeniedError(error)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tconst blocked = buildSandboxDeniedBashResult(error.message);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t} finally {\n\t\t\t// §10.3 exactly-once: the pool treats a repeated release as a no-op.\n\t\t\treleaseResourcePermit?.();\n\t\t\tthis.abortController = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Record a bash execution result in session history.\n\t * Used by executeBash and by extensions that handle bash execution themselves.\n\t */\n\trecordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {\n\t\tconst bashMessage: BashExecutionMessage = {\n\t\t\trole: \"bashExecution\",\n\t\t\tcommand,\n\t\t\toutput: result.output,\n\t\t\texitCode: result.exitCode,\n\t\t\tcancelled: result.cancelled,\n\t\t\ttruncated: result.truncated,\n\t\t\tfullOutputPath: result.fullOutputPath,\n\t\t\ttimestamp: Date.now(),\n\t\t\texcludeFromContext: options?.excludeFromContext,\n\t\t};\n\n\t\t// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering\n\t\tif (this.deps.isStreaming()) {\n\t\t\tthis.pendingBashMessages.push(bashMessage);\n\t\t} else {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t}\n\n\t/** Cancel running bash command. */\n\tabortBash(): void {\n\t\tthis.abortController?.abort();\n\t}\n\n\tget isBashRunning(): boolean {\n\t\treturn this.abortController !== undefined;\n\t}\n\n\tget hasPendingBashMessages(): boolean {\n\t\treturn this.pendingBashMessages.length > 0;\n\t}\n\n\t/** Flush queued results after the agent turn completes (ordering contract). */\n\tflushPending(): void {\n\t\tif (this.pendingBashMessages.length === 0) return;\n\t\tfor (const bashMessage of this.pendingBashMessages) {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t\tthis.pendingBashMessages = [];\n\t}\n}\n"]}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { executeBashWithOperations } from "./bash-executor.js";
|
|
13
13
|
import { classifyShellCommand } from "./command-safety.js";
|
|
14
|
-
import { buildBlockedBashResult, evaluateCommandGate, isCommandSafetyAssumeYesEnabled, } from "./extensions/builtin/command-safety-gate.js";
|
|
14
|
+
import { buildBlockedBashResult, evaluateCommandGate, isCommandSafetyAssumeYesEnabled, isCommandSafetyDisabled, } from "./extensions/builtin/command-safety-gate.js";
|
|
15
15
|
import { assertLoadoutAccess, decideLoadoutAccess } from "./loadout-access-policy.js";
|
|
16
16
|
import { createLocalBashOperations } from "./tools/bash.js";
|
|
17
17
|
function isSandboxDeniedError(error) {
|
|
@@ -40,8 +40,9 @@ export class SessionBashService {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
// Non-negotiable safety floor for headless callers (RPC bash): hard-deny
|
|
43
|
-
// block-tier commands before any shell is spawned.
|
|
44
|
-
|
|
43
|
+
// block-tier commands before any shell is spawned. YOLO mode opts out of
|
|
44
|
+
// the entire gate — the floor included — so nothing prompts or denies.
|
|
45
|
+
if (options?.safetyGate === "headless" && !isCommandSafetyDisabled()) {
|
|
45
46
|
const floorVerdict = classifyShellCommand(command);
|
|
46
47
|
if (floorVerdict.risk === "block") {
|
|
47
48
|
throw new Error(`OMK §0.1 safety floor blocked bash: [${floorVerdict.rule}] ${floorVerdict.reason}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-bash-service.js","sourceRoot":"","sources":["../../src/core/session-bash-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session bash service: command execution, result recording, abort, and the\n * streaming-deferral queue. Extracted from AgentSession so the session class\n * coordinates lifecycle while this owns the bash surface end-to-end.\n *\n * Ordering contract (preserved from the session implementation):\n * - results recorded while the agent is streaming are queued and flushed on\n * turn end (`flushPending`) so tool_use/tool_result ordering never breaks;\n * - `recordBashResult` is idempotent-safe to call from executeBash and from\n * extensions that run bash themselves.\n */\nimport { type BashResult, executeBashWithOperations } from \"./bash-executor.ts\";\nimport { classifyShellCommand } from \"./command-safety.ts\";\nimport {\n\tbuildBlockedBashResult,\n\tevaluateCommandGate,\n\tisCommandSafetyAssumeYesEnabled,\n} from \"./extensions/builtin/command-safety-gate.ts\";\nimport type { LoadoutAccessPolicy } from \"./loadout-access-policy.ts\";\nimport { assertLoadoutAccess, decideLoadoutAccess } from \"./loadout-access-policy.ts\";\nimport type { BashExecutionMessage } from \"./messages.ts\";\nimport type { SessionBashRuntime } from \"./session-bash-runtime.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport { type BashOperations, type BashSandboxPreflight, createLocalBashOperations } from \"./tools/bash.ts\";\n\nexport interface ExecuteBashOptions {\n\texcludeFromContext?: boolean;\n\t/** Optional identifier echoed in `bash_execution_update` events (RPC request correlation). */\n\tid?: string;\n\toperations?: BashOperations;\n\tsafetyGate?: \"headless\";\n\t/** Trusted internal/test override for local bash sandboxing. */\n\tsandboxPolicy?: BashSandboxPreflight;\n}\n\n/** Resource gate grant (roadmap §11/M3): §11.3 blocked payload, permit release, or ungated. */\nexport interface BashResourcePermitGrant {\n\t/** §11.3 structured block JSON; when set the command must not spawn. */\n\treadonly blocked?: string;\n\treadonly release?: () => void;\n}\n\nexport interface SessionBashServiceDeps {\n\treadonly settings: SettingsManager;\n\treadonly runtime: SessionBashRuntime;\n\treadonly loadoutPolicy: LoadoutAccessPolicy | undefined;\n\treadonly getCwd: () => string;\n\treadonly emit: (event: { type: \"bash_execution_update\"; id?: string; delta: string }) => void;\n\treadonly isStreaming: () => boolean;\n\treadonly pushMessage: (message: BashExecutionMessage) => void;\n\treadonly appendMessage: (message: BashExecutionMessage) => void;\n\t/**\n\t * Optional resource governor gate, invoked AFTER command safety (§9.4\n\t * order) and before any shell spawns. The abort signal cancels a queued\n\t * permit wait. Implementations must never throw.\n\t */\n\treadonly acquireResourcePermit?: (command: string, signal: AbortSignal) => Promise<BashResourcePermitGrant>;\n}\n\nfunction isSandboxDeniedError(error: unknown): error is Error {\n\treturn error instanceof Error && error.message.startsWith(\"sandbox: shell denied\");\n}\n\nfunction buildSandboxDeniedBashResult(reason: string): BashResult {\n\treturn { output: reason, exitCode: 1, cancelled: false, truncated: false };\n}\n\nexport class SessionBashService {\n\tprivate abortController: AbortController | undefined;\n\tprivate pendingBashMessages: BashExecutionMessage[] = [];\n\tprivate readonly deps: SessionBashServiceDeps;\n\n\tconstructor(deps: SessionBashServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tasync executeBash(\n\t\tcommand: string,\n\t\tonChunk?: (chunk: string) => void,\n\t\toptions?: ExecuteBashOptions,\n\t): Promise<BashResult> {\n\t\tconst { settings, runtime, loadoutPolicy } = this.deps;\n\t\tconst prefix = settings.getShellCommandPrefix();\n\t\tconst shellPath = settings.getShellPath();\n\t\tconst resolvedCommand = prefix ? `${prefix}\\n${command}` : command;\n\t\tif (loadoutPolicy) {\n\t\t\tassertLoadoutAccess((request) => decideLoadoutAccess(loadoutPolicy, request), {\n\t\t\t\toperation: \"execute\",\n\t\t\t\ttoolName: \"bash\",\n\t\t\t\tcommand: resolvedCommand,\n\t\t\t});\n\t\t}\n\n\t\t// Non-negotiable safety floor for headless callers (RPC bash): hard-deny\n\t\t// block-tier commands before any shell is spawned.\n\t\tif (options?.safetyGate === \"headless\") {\n\t\t\tconst floorVerdict = classifyShellCommand(command);\n\t\t\tif (floorVerdict.risk === \"block\") {\n\t\t\t\tthrow new Error(`OMK §0.1 safety floor blocked bash: [${floorVerdict.rule}] ${floorVerdict.reason}`);\n\t\t\t}\n\t\t}\n\n\t\t// Command-safety parity for non-interactive callers (RPC bash). Interactive\n\t\t// `!`/`!!` bash is gated earlier through the user_bash extension event, which\n\t\t// keeps its prompt-based approval semantics, so it does not pass safetyGate and\n\t\t// is never double-prompted here. confirm/block-tier verdicts deny headlessly;\n\t\t// the EFFECTIVE command (after the shell command prefix) is classified.\n\t\tif (options?.safetyGate === \"headless\") {\n\t\t\tconst decision = await evaluateCommandGate(resolvedCommand, {\n\t\t\t\thasUI: false,\n\t\t\t\theadlessConfirmPolicy: isCommandSafetyAssumeYesEnabled() ? \"allow\" : \"deny\",\n\t\t\t});\n\t\t\tif (decision?.deny) {\n\t\t\t\tconst blocked = buildBlockedBashResult(decision.reason);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t}\n\n\t\tthis.abortController = new AbortController();\n\t\tlet releaseResourcePermit: (() => void) | undefined;\n\n\t\ttry {\n\t\t\t// Resource safety gate (§9.4: command safety above stays authoritative;\n\t\t\t// pressure can only throttle or defer an already-allowed command).\n\t\t\tif (this.deps.acquireResourcePermit) {\n\t\t\t\tconst grant = await this.deps.acquireResourcePermit(resolvedCommand, this.abortController.signal);\n\t\t\t\tif (grant.blocked !== undefined) {\n\t\t\t\t\tconst blocked = buildBlockedBashResult(grant.blocked);\n\t\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\t\treturn blocked;\n\t\t\t\t}\n\t\t\t\treleaseResourcePermit = grant.release;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst operations =\n\t\t\t\t\toptions?.operations ??\n\t\t\t\t\tcreateLocalBashOperations({\n\t\t\t\t\t\tshellPath,\n\t\t\t\t\t\tsandboxPolicy: runtime.sandboxPreflight(options?.sandboxPolicy),\n\t\t\t\t\t});\n\t\t\t\tconst cwd = this.deps.getCwd();\n\t\t\t\tconst onChunkWrapped = (delta: string) => {\n\t\t\t\t\tonChunk?.(delta);\n\t\t\t\t\tthis.deps.emit({ type: \"bash_execution_update\", id: options?.id, delta });\n\t\t\t\t};\n\n\t\t\t\t// Default-on verified path when a session ledger exists; OMK_VERIFIED_BASH=0 rolls back.\n\t\t\t\tconst verified = await runtime.executeVerified({\n\t\t\t\t\tresolvedCommand,\n\t\t\t\t\tcwd,\n\t\t\t\t\tshellPath,\n\t\t\t\t\toperations,\n\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t});\n\t\t\t\tconst result: BashResult =\n\t\t\t\t\tverified ??\n\t\t\t\t\t(await executeBashWithOperations(resolvedCommand, cwd, operations, {\n\t\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\t}));\n\n\t\t\t\tthis.recordBashResult(command, result, options);\n\t\t\t\treturn result;\n\t\t\t} catch (error) {\n\t\t\t\tif (!isSandboxDeniedError(error)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tconst blocked = buildSandboxDeniedBashResult(error.message);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t} finally {\n\t\t\t// §10.3 exactly-once: the pool treats a repeated release as a no-op.\n\t\t\treleaseResourcePermit?.();\n\t\t\tthis.abortController = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Record a bash execution result in session history.\n\t * Used by executeBash and by extensions that handle bash execution themselves.\n\t */\n\trecordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {\n\t\tconst bashMessage: BashExecutionMessage = {\n\t\t\trole: \"bashExecution\",\n\t\t\tcommand,\n\t\t\toutput: result.output,\n\t\t\texitCode: result.exitCode,\n\t\t\tcancelled: result.cancelled,\n\t\t\ttruncated: result.truncated,\n\t\t\tfullOutputPath: result.fullOutputPath,\n\t\t\ttimestamp: Date.now(),\n\t\t\texcludeFromContext: options?.excludeFromContext,\n\t\t};\n\n\t\t// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering\n\t\tif (this.deps.isStreaming()) {\n\t\t\tthis.pendingBashMessages.push(bashMessage);\n\t\t} else {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t}\n\n\t/** Cancel running bash command. */\n\tabortBash(): void {\n\t\tthis.abortController?.abort();\n\t}\n\n\tget isBashRunning(): boolean {\n\t\treturn this.abortController !== undefined;\n\t}\n\n\tget hasPendingBashMessages(): boolean {\n\t\treturn this.pendingBashMessages.length > 0;\n\t}\n\n\t/** Flush queued results after the agent turn completes (ordering contract). */\n\tflushPending(): void {\n\t\tif (this.pendingBashMessages.length === 0) return;\n\t\tfor (const bashMessage of this.pendingBashMessages) {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t\tthis.pendingBashMessages = [];\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-bash-service.js","sourceRoot":"","sources":["../../src/core/session-bash-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session bash service: command execution, result recording, abort, and the\n * streaming-deferral queue. Extracted from AgentSession so the session class\n * coordinates lifecycle while this owns the bash surface end-to-end.\n *\n * Ordering contract (preserved from the session implementation):\n * - results recorded while the agent is streaming are queued and flushed on\n * turn end (`flushPending`) so tool_use/tool_result ordering never breaks;\n * - `recordBashResult` is idempotent-safe to call from executeBash and from\n * extensions that run bash themselves.\n */\nimport { type BashResult, executeBashWithOperations } from \"./bash-executor.ts\";\nimport { classifyShellCommand } from \"./command-safety.ts\";\nimport {\n\tbuildBlockedBashResult,\n\tevaluateCommandGate,\n\tisCommandSafetyAssumeYesEnabled,\n\tisCommandSafetyDisabled,\n} from \"./extensions/builtin/command-safety-gate.ts\";\nimport type { LoadoutAccessPolicy } from \"./loadout-access-policy.ts\";\nimport { assertLoadoutAccess, decideLoadoutAccess } from \"./loadout-access-policy.ts\";\nimport type { BashExecutionMessage } from \"./messages.ts\";\nimport type { SessionBashRuntime } from \"./session-bash-runtime.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport { type BashOperations, type BashSandboxPreflight, createLocalBashOperations } from \"./tools/bash.ts\";\n\nexport interface ExecuteBashOptions {\n\texcludeFromContext?: boolean;\n\t/** Optional identifier echoed in `bash_execution_update` events (RPC request correlation). */\n\tid?: string;\n\toperations?: BashOperations;\n\tsafetyGate?: \"headless\";\n\t/** Trusted internal/test override for local bash sandboxing. */\n\tsandboxPolicy?: BashSandboxPreflight;\n}\n\n/** Resource gate grant (roadmap §11/M3): §11.3 blocked payload, permit release, or ungated. */\nexport interface BashResourcePermitGrant {\n\t/** §11.3 structured block JSON; when set the command must not spawn. */\n\treadonly blocked?: string;\n\treadonly release?: () => void;\n}\n\nexport interface SessionBashServiceDeps {\n\treadonly settings: SettingsManager;\n\treadonly runtime: SessionBashRuntime;\n\treadonly loadoutPolicy: LoadoutAccessPolicy | undefined;\n\treadonly getCwd: () => string;\n\treadonly emit: (event: { type: \"bash_execution_update\"; id?: string; delta: string }) => void;\n\treadonly isStreaming: () => boolean;\n\treadonly pushMessage: (message: BashExecutionMessage) => void;\n\treadonly appendMessage: (message: BashExecutionMessage) => void;\n\t/**\n\t * Optional resource governor gate, invoked AFTER command safety (§9.4\n\t * order) and before any shell spawns. The abort signal cancels a queued\n\t * permit wait. Implementations must never throw.\n\t */\n\treadonly acquireResourcePermit?: (command: string, signal: AbortSignal) => Promise<BashResourcePermitGrant>;\n}\n\nfunction isSandboxDeniedError(error: unknown): error is Error {\n\treturn error instanceof Error && error.message.startsWith(\"sandbox: shell denied\");\n}\n\nfunction buildSandboxDeniedBashResult(reason: string): BashResult {\n\treturn { output: reason, exitCode: 1, cancelled: false, truncated: false };\n}\n\nexport class SessionBashService {\n\tprivate abortController: AbortController | undefined;\n\tprivate pendingBashMessages: BashExecutionMessage[] = [];\n\tprivate readonly deps: SessionBashServiceDeps;\n\n\tconstructor(deps: SessionBashServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tasync executeBash(\n\t\tcommand: string,\n\t\tonChunk?: (chunk: string) => void,\n\t\toptions?: ExecuteBashOptions,\n\t): Promise<BashResult> {\n\t\tconst { settings, runtime, loadoutPolicy } = this.deps;\n\t\tconst prefix = settings.getShellCommandPrefix();\n\t\tconst shellPath = settings.getShellPath();\n\t\tconst resolvedCommand = prefix ? `${prefix}\\n${command}` : command;\n\t\tif (loadoutPolicy) {\n\t\t\tassertLoadoutAccess((request) => decideLoadoutAccess(loadoutPolicy, request), {\n\t\t\t\toperation: \"execute\",\n\t\t\t\ttoolName: \"bash\",\n\t\t\t\tcommand: resolvedCommand,\n\t\t\t});\n\t\t}\n\n\t\t// Non-negotiable safety floor for headless callers (RPC bash): hard-deny\n\t\t// block-tier commands before any shell is spawned. YOLO mode opts out of\n\t\t// the entire gate — the floor included — so nothing prompts or denies.\n\t\tif (options?.safetyGate === \"headless\" && !isCommandSafetyDisabled()) {\n\t\t\tconst floorVerdict = classifyShellCommand(command);\n\t\t\tif (floorVerdict.risk === \"block\") {\n\t\t\t\tthrow new Error(`OMK §0.1 safety floor blocked bash: [${floorVerdict.rule}] ${floorVerdict.reason}`);\n\t\t\t}\n\t\t}\n\n\t\t// Command-safety parity for non-interactive callers (RPC bash). Interactive\n\t\t// `!`/`!!` bash is gated earlier through the user_bash extension event, which\n\t\t// keeps its prompt-based approval semantics, so it does not pass safetyGate and\n\t\t// is never double-prompted here. confirm/block-tier verdicts deny headlessly;\n\t\t// the EFFECTIVE command (after the shell command prefix) is classified.\n\t\tif (options?.safetyGate === \"headless\") {\n\t\t\tconst decision = await evaluateCommandGate(resolvedCommand, {\n\t\t\t\thasUI: false,\n\t\t\t\theadlessConfirmPolicy: isCommandSafetyAssumeYesEnabled() ? \"allow\" : \"deny\",\n\t\t\t});\n\t\t\tif (decision?.deny) {\n\t\t\t\tconst blocked = buildBlockedBashResult(decision.reason);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t}\n\n\t\tthis.abortController = new AbortController();\n\t\tlet releaseResourcePermit: (() => void) | undefined;\n\n\t\ttry {\n\t\t\t// Resource safety gate (§9.4: command safety above stays authoritative;\n\t\t\t// pressure can only throttle or defer an already-allowed command).\n\t\t\tif (this.deps.acquireResourcePermit) {\n\t\t\t\tconst grant = await this.deps.acquireResourcePermit(resolvedCommand, this.abortController.signal);\n\t\t\t\tif (grant.blocked !== undefined) {\n\t\t\t\t\tconst blocked = buildBlockedBashResult(grant.blocked);\n\t\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\t\treturn blocked;\n\t\t\t\t}\n\t\t\t\treleaseResourcePermit = grant.release;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst operations =\n\t\t\t\t\toptions?.operations ??\n\t\t\t\t\tcreateLocalBashOperations({\n\t\t\t\t\t\tshellPath,\n\t\t\t\t\t\tsandboxPolicy: runtime.sandboxPreflight(options?.sandboxPolicy),\n\t\t\t\t\t});\n\t\t\t\tconst cwd = this.deps.getCwd();\n\t\t\t\tconst onChunkWrapped = (delta: string) => {\n\t\t\t\t\tonChunk?.(delta);\n\t\t\t\t\tthis.deps.emit({ type: \"bash_execution_update\", id: options?.id, delta });\n\t\t\t\t};\n\n\t\t\t\t// Default-on verified path when a session ledger exists; OMK_VERIFIED_BASH=0 rolls back.\n\t\t\t\tconst verified = await runtime.executeVerified({\n\t\t\t\t\tresolvedCommand,\n\t\t\t\t\tcwd,\n\t\t\t\t\tshellPath,\n\t\t\t\t\toperations,\n\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t});\n\t\t\t\tconst result: BashResult =\n\t\t\t\t\tverified ??\n\t\t\t\t\t(await executeBashWithOperations(resolvedCommand, cwd, operations, {\n\t\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\t}));\n\n\t\t\t\tthis.recordBashResult(command, result, options);\n\t\t\t\treturn result;\n\t\t\t} catch (error) {\n\t\t\t\tif (!isSandboxDeniedError(error)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tconst blocked = buildSandboxDeniedBashResult(error.message);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t} finally {\n\t\t\t// §10.3 exactly-once: the pool treats a repeated release as a no-op.\n\t\t\treleaseResourcePermit?.();\n\t\t\tthis.abortController = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Record a bash execution result in session history.\n\t * Used by executeBash and by extensions that handle bash execution themselves.\n\t */\n\trecordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {\n\t\tconst bashMessage: BashExecutionMessage = {\n\t\t\trole: \"bashExecution\",\n\t\t\tcommand,\n\t\t\toutput: result.output,\n\t\t\texitCode: result.exitCode,\n\t\t\tcancelled: result.cancelled,\n\t\t\ttruncated: result.truncated,\n\t\t\tfullOutputPath: result.fullOutputPath,\n\t\t\ttimestamp: Date.now(),\n\t\t\texcludeFromContext: options?.excludeFromContext,\n\t\t};\n\n\t\t// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering\n\t\tif (this.deps.isStreaming()) {\n\t\t\tthis.pendingBashMessages.push(bashMessage);\n\t\t} else {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t}\n\n\t/** Cancel running bash command. */\n\tabortBash(): void {\n\t\tthis.abortController?.abort();\n\t}\n\n\tget isBashRunning(): boolean {\n\t\treturn this.abortController !== undefined;\n\t}\n\n\tget hasPendingBashMessages(): boolean {\n\t\treturn this.pendingBashMessages.length > 0;\n\t}\n\n\t/** Flush queued results after the agent turn completes (ordering contract). */\n\tflushPending(): void {\n\t\tif (this.pendingBashMessages.length === 0) return;\n\t\tfor (const bashMessage of this.pendingBashMessages) {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t\tthis.pendingBashMessages = [];\n\t}\n}\n"]}
|
|
@@ -15,7 +15,7 @@ export type ProviderTerminationCauseCode = "abort" | "auth" | "rate_limit" | "ne
|
|
|
15
15
|
export type ToolTerminationCauseCode = "timeout" | "fatal";
|
|
16
16
|
/** §15.4 resource-aware runtime roadmap: host-pressure termination causes. */
|
|
17
17
|
export type ResourceTerminationCauseCode = "memory" | "disk" | "cpu" | "heap" | "probe_unavailable" | "queue_overflow";
|
|
18
|
-
export type CompactionTerminationCauseCode = "aborted" | "failed" | "stale";
|
|
18
|
+
export type CompactionTerminationCauseCode = "aborted" | "failed" | "stale" | "quota_exhausted";
|
|
19
19
|
export type PersistenceTerminationCauseCode = "read_failed" | "append_failed" | "replace_failed" | "fsync_failed" | "lock_failed";
|
|
20
20
|
export type TranscriptTerminationCauseCode = "missing_result" | "duplicate_result" | "orphan_result" | "duplicate_call_id" | "interleaved_non_result" | "invalid_jsonl" | "invalid_tree" | "unsupported_version" | "trailing_fragment";
|
|
21
21
|
export type SessionTerminationCauseCode = "session.completed" | "session.user_abort" | `provider.${ProviderTerminationCauseCode}` | `tool.${ToolTerminationCauseCode}` | `compaction.${CompactionTerminationCauseCode}` | `persistence.${PersistenceTerminationCauseCode}` | "process.signal" | "process.crash" | `transcript.${TranscriptTerminationCauseCode}` | "configuration.invalid" | "internal.unclassified" | `resource.${ResourceTerminationCauseCode}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-termination.d.ts","sourceRoot":"","sources":["../../src/core/session-termination.ts"],"names":[],"mappings":"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","sourcesContent":["export const SESSION_TERMINATION_SCHEMA_VERSION = 1 as const;\nexport const MAX_SESSION_TERMINATION_MESSAGE_LENGTH = 512;\n\n/**\n * Canonical list of termination kinds. The run-journal validator derives its\n * runtime bounds from this tuple so a newly added kind can never crash\n * journaling with \"termination kind is out of bounds\" again.\n */\nexport const SESSION_TERMINATION_KIND_VALUES = [\n\t\"completed\",\n\t\"user_abort\",\n\t\"provider_abort\",\n\t\"provider_auth\",\n\t\"provider_rate_limit\",\n\t\"provider_network\",\n\t\"provider_protocol\",\n\t\"provider_refusal\",\n\t\"context_overflow\",\n\t\"transcript_invalid\",\n\t\"tool_timeout\",\n\t\"tool_fatal\",\n\t\"compaction\",\n\t\"persistence\",\n\t\"process_signal\",\n\t\"process_crash\",\n\t\"configuration\",\n\t\"internal_error\",\n\t\"resource_pressure\",\n] as const;\n\nexport type SessionTerminationKind = (typeof SESSION_TERMINATION_KIND_VALUES)[number];\n\nexport type SessionTerminationPhase =\n\t| \"completed\"\n\t| \"control\"\n\t| \"preflight\"\n\t| \"provider\"\n\t| \"tool\"\n\t| \"compaction\"\n\t| \"persistence\"\n\t| \"process\"\n\t| \"resume\";\n\nexport type SessionTerminationSource = \"observed\" | \"inferred_on_resume\";\nexport type SessionSideEffects = \"none\" | \"possible\" | \"confirmed\";\nexport type SessionProcessSignal = \"SIGINT\" | \"SIGTERM\" | \"SIGHUP\" | \"SIGQUIT\";\n\nexport type ProviderTerminationCauseCode =\n\t| \"abort\"\n\t| \"auth\"\n\t| \"rate_limit\"\n\t| \"network\"\n\t| \"protocol\"\n\t| \"refusal\"\n\t| \"context_overflow\";\nexport type ToolTerminationCauseCode = \"timeout\" | \"fatal\";\n/** §15.4 resource-aware runtime roadmap: host-pressure termination causes. */\nexport type ResourceTerminationCauseCode = \"memory\" | \"disk\" | \"cpu\" | \"heap\" | \"probe_unavailable\" | \"queue_overflow\";\nexport type CompactionTerminationCauseCode = \"aborted\" | \"failed\" | \"stale\";\nexport type PersistenceTerminationCauseCode =\n\t| \"read_failed\"\n\t| \"append_failed\"\n\t| \"replace_failed\"\n\t| \"fsync_failed\"\n\t| \"lock_failed\";\nexport type TranscriptTerminationCauseCode =\n\t| \"missing_result\"\n\t| \"duplicate_result\"\n\t| \"orphan_result\"\n\t| \"duplicate_call_id\"\n\t| \"interleaved_non_result\"\n\t| \"invalid_jsonl\"\n\t| \"invalid_tree\"\n\t| \"unsupported_version\"\n\t| \"trailing_fragment\";\nexport type SessionTerminationCauseCode =\n\t| \"session.completed\"\n\t| \"session.user_abort\"\n\t| `provider.${ProviderTerminationCauseCode}`\n\t| `tool.${ToolTerminationCauseCode}`\n\t| `compaction.${CompactionTerminationCauseCode}`\n\t| `persistence.${PersistenceTerminationCauseCode}`\n\t| \"process.signal\"\n\t| \"process.crash\"\n\t| `transcript.${TranscriptTerminationCauseCode}`\n\t| \"configuration.invalid\"\n\t| \"internal.unclassified\"\n\t| `resource.${ResourceTerminationCauseCode}`;\n\nexport type SessionTerminationCause =\n\t| { readonly area: \"completed\" }\n\t| { readonly area: \"user\"; readonly code: \"abort\" }\n\t| { readonly area: \"provider\"; readonly code: ProviderTerminationCauseCode }\n\t| { readonly area: \"tool\"; readonly code: ToolTerminationCauseCode }\n\t| { readonly area: \"compaction\"; readonly code: CompactionTerminationCauseCode }\n\t| { readonly area: \"persistence\"; readonly code: PersistenceTerminationCauseCode }\n\t| { readonly area: \"process\"; readonly code: \"signal\"; readonly signal: SessionProcessSignal }\n\t| { readonly area: \"process\"; readonly code: \"crash\" }\n\t| { readonly area: \"transcript\"; readonly code: TranscriptTerminationCauseCode }\n\t| { readonly area: \"configuration\"; readonly code: \"invalid\" }\n\t| { readonly area: \"internal\"; readonly code: \"unclassified\" }\n\t| { readonly area: \"resource\"; readonly code: ResourceTerminationCauseCode };\n\nexport interface ClassifySessionTerminationInput {\n\treadonly sessionId: string;\n\treadonly runId: string;\n\t/** Deterministic caller-provided ISO-8601 timestamp. */\n\treadonly timestamp: string;\n\treadonly source: SessionTerminationSource;\n\t/** Caller-supplied, pre-redacted diagnostic text. */\n\treadonly message: string;\n\treadonly cause: SessionTerminationCause;\n\treadonly sideEffects: SessionSideEffects;\n\treadonly retryAfterMs?: number;\n\treadonly provider?: string;\n\treadonly model?: string;\n\treadonly toolCallId?: string;\n\treadonly toolName?: string;\n}\n\nexport interface SessionTermination {\n\treadonly schemaVersion: typeof SESSION_TERMINATION_SCHEMA_VERSION;\n\treadonly sessionId: string;\n\treadonly runId: string;\n\treadonly kind: SessionTerminationKind;\n\treadonly phase: SessionTerminationPhase;\n\treadonly source: SessionTerminationSource;\n\treadonly message: string;\n\treadonly causeCode: SessionTerminationCauseCode;\n\t/** Stable operator guidance suitable for print, JSON, RPC, and TUI surfaces. */\n\treadonly nextAction: string;\n\treadonly retryable: boolean;\n\treadonly safeToAutoRetry: boolean;\n\treadonly sideEffects: SessionSideEffects;\n\treadonly timestamp: string;\n\treadonly retryAfterMs?: number;\n\treadonly provider?: string;\n\treadonly model?: string;\n\treadonly toolCallId?: string;\n\treadonly toolName?: string;\n\treadonly processSignal?: SessionProcessSignal;\n\treadonly transcriptIssue?: TranscriptTerminationCauseCode;\n}\n\nexport class SessionTerminationError extends Error {\n\treadonly termination: SessionTermination;\n\n\tconstructor(message: string, termination: SessionTermination) {\n\t\tsuper(message);\n\t\tthis.name = \"SessionTerminationError\";\n\t\tthis.termination = termination;\n\t}\n}\n\ninterface Classification {\n\treadonly kind: SessionTerminationKind;\n\treadonly phase: SessionTerminationPhase;\n\treadonly causeCode: SessionTerminationCauseCode;\n\treadonly retryable: boolean;\n\treadonly processSignal?: SessionProcessSignal;\n\treadonly transcriptIssue?: TranscriptTerminationCauseCode;\n}\n\nconst CREDENTIAL_SHAPES = [\n\t/-----BEGIN [A-Z ]*PRIVATE KEY-----/i,\n\t/\\b(?:sk-[A-Za-z0-9_-]{16,}|github_pat_[A-Za-z0-9_]{16,}|gh[pousr]_[A-Za-z0-9]{16,}|xox[baprs]-[A-Za-z0-9-]{16,}|AKIA[A-Z0-9]{16})\\b/,\n\t/\\bBearer\\s+[A-Za-z0-9._~+/-]{8,}={0,2}\\b/i,\n\t/\\beyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\b/,\n\t/(?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]+[_-])*(?:authorization|api[-_ ]?key|access[-_ ]?token|token|password|secret(?:[-_ ]?key)?|private[-_ ]?key|cookie)[\"']?\\s*[:=]/i,\n] as const;\n\nconst TOP_LEVEL_KEYS = new Set([\n\t\"sessionId\",\n\t\"runId\",\n\t\"timestamp\",\n\t\"source\",\n\t\"message\",\n\t\"cause\",\n\t\"sideEffects\",\n\t\"retryAfterMs\",\n\t\"provider\",\n\t\"model\",\n\t\"toolCallId\",\n\t\"toolName\",\n]);\nconst PROVIDER_CAUSE_CODES = new Set<ProviderTerminationCauseCode>([\n\t\"abort\",\n\t\"auth\",\n\t\"rate_limit\",\n\t\"network\",\n\t\"protocol\",\n\t\"refusal\",\n\t\"context_overflow\",\n]);\nconst TOOL_CAUSE_CODES = new Set<ToolTerminationCauseCode>([\"timeout\", \"fatal\"]);\nconst RESOURCE_CAUSE_CODES = new Set<ResourceTerminationCauseCode>([\n\t\"memory\",\n\t\"disk\",\n\t\"cpu\",\n\t\"heap\",\n\t\"probe_unavailable\",\n\t\"queue_overflow\",\n]);\nconst COMPACTION_CAUSE_CODES = new Set<CompactionTerminationCauseCode>([\"aborted\", \"failed\", \"stale\"]);\nconst PERSISTENCE_CAUSE_CODES = new Set<PersistenceTerminationCauseCode>([\n\t\"read_failed\",\n\t\"append_failed\",\n\t\"replace_failed\",\n\t\"fsync_failed\",\n\t\"lock_failed\",\n]);\nconst TRANSCRIPT_CAUSE_CODES = new Set<TranscriptTerminationCauseCode>([\n\t\"missing_result\",\n\t\"duplicate_result\",\n\t\"orphan_result\",\n\t\"duplicate_call_id\",\n\t\"interleaved_non_result\",\n\t\"invalid_jsonl\",\n\t\"invalid_tree\",\n\t\"unsupported_version\",\n\t\"trailing_fragment\",\n]);\nconst PROCESS_SIGNALS = new Set<SessionProcessSignal>([\"SIGINT\", \"SIGTERM\", \"SIGHUP\", \"SIGQUIT\"]);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasOnlyKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {\n\tconst allowed = new Set(keys);\n\treturn Object.keys(value).every((key) => allowed.has(key));\n}\n\nfunction assertCause(cause: unknown): asserts cause is SessionTerminationCause {\n\tif (!isRecord(cause) || typeof cause.area !== \"string\") {\n\t\tthrow new TypeError(\"cause must be a bounded structured termination cause\");\n\t}\n\tconst code = cause.code;\n\tlet valid = false;\n\tswitch (cause.area) {\n\t\tcase \"completed\":\n\t\t\tvalid = hasOnlyKeys(cause, [\"area\"]);\n\t\t\tbreak;\n\t\tcase \"user\":\n\t\t\tvalid = code === \"abort\" && hasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"provider\":\n\t\t\tvalid =\n\t\t\t\ttypeof code === \"string\" &&\n\t\t\t\tPROVIDER_CAUSE_CODES.has(code as ProviderTerminationCauseCode) &&\n\t\t\t\thasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"tool\":\n\t\t\tvalid =\n\t\t\t\ttypeof code === \"string\" &&\n\t\t\t\tTOOL_CAUSE_CODES.has(code as ToolTerminationCauseCode) &&\n\t\t\t\thasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"compaction\":\n\t\t\tvalid =\n\t\t\t\ttypeof code === \"string\" &&\n\t\t\t\tCOMPACTION_CAUSE_CODES.has(code as CompactionTerminationCauseCode) &&\n\t\t\t\thasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"persistence\":\n\t\t\tvalid =\n\t\t\t\ttypeof code === \"string\" &&\n\t\t\t\tPERSISTENCE_CAUSE_CODES.has(code as PersistenceTerminationCauseCode) &&\n\t\t\t\thasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"process\":\n\t\t\tvalid =\n\t\t\t\t(code === \"crash\" && hasOnlyKeys(cause, [\"area\", \"code\"])) ||\n\t\t\t\t(code === \"signal\" &&\n\t\t\t\t\ttypeof cause.signal === \"string\" &&\n\t\t\t\t\tPROCESS_SIGNALS.has(cause.signal as SessionProcessSignal) &&\n\t\t\t\t\thasOnlyKeys(cause, [\"area\", \"code\", \"signal\"]));\n\t\t\tbreak;\n\t\tcase \"transcript\":\n\t\t\tvalid =\n\t\t\t\ttypeof code === \"string\" &&\n\t\t\t\tTRANSCRIPT_CAUSE_CODES.has(code as TranscriptTerminationCauseCode) &&\n\t\t\t\thasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"configuration\":\n\t\t\tvalid = code === \"invalid\" && hasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"internal\":\n\t\t\tvalid = code === \"unclassified\" && hasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"resource\":\n\t\t\tvalid =\n\t\t\t\ttypeof code === \"string\" &&\n\t\t\t\tRESOURCE_CAUSE_CODES.has(code as ResourceTerminationCauseCode) &&\n\t\t\t\thasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t}\n\tif (!valid) throw new TypeError(\"cause must be a bounded structured termination cause\");\n}\n\nfunction assertClassifierInput(input: unknown): asserts input is ClassifySessionTerminationInput {\n\tif (!isRecord(input) || Object.keys(input).some((key) => !TOP_LEVEL_KEYS.has(key))) {\n\t\tthrow new TypeError(\"input must contain only bounded structured termination fields\");\n\t}\n\tif (\n\t\ttypeof input.sessionId !== \"string\" ||\n\t\ttypeof input.runId !== \"string\" ||\n\t\ttypeof input.timestamp !== \"string\" ||\n\t\ttypeof input.message !== \"string\" ||\n\t\t(input.source !== \"observed\" && input.source !== \"inferred_on_resume\") ||\n\t\t(input.sideEffects !== \"none\" && input.sideEffects !== \"possible\" && input.sideEffects !== \"confirmed\") ||\n\t\t(input.retryAfterMs !== undefined && typeof input.retryAfterMs !== \"number\") ||\n\t\t(input.provider !== undefined && typeof input.provider !== \"string\") ||\n\t\t(input.model !== undefined && typeof input.model !== \"string\") ||\n\t\t(input.toolCallId !== undefined && typeof input.toolCallId !== \"string\") ||\n\t\t(input.toolName !== undefined && typeof input.toolName !== \"string\")\n\t) {\n\t\tthrow new TypeError(\"input must contain only bounded structured termination fields\");\n\t}\n\tassertCause(input.cause);\n}\n\nfunction assertIdentifier(value: string, name: string, maxLength: number): void {\n\tif (value.length === 0 || value.length > maxLength || /[\\u0000-\\u001f\\u007f]/.test(value)) {\n\t\tthrow new TypeError(`${name} must be non-empty bounded text without C0 or DEL control characters`);\n\t}\n}\n\n/** Validate text at the termination trust boundary without attempting redaction. */\nexport function assertPreRedactedTerminationMessage(message: string): void {\n\tif (message.length === 0 || message.length > MAX_SESSION_TERMINATION_MESSAGE_LENGTH) {\n\t\tthrow new TypeError(\n\t\t\t`message must contain 1-${MAX_SESSION_TERMINATION_MESSAGE_LENGTH} characters of pre-redacted text`,\n\t\t);\n\t}\n\tif (message.includes(\"\\0\")) {\n\t\tthrow new TypeError(\"message must not contain NUL\");\n\t}\n\tif (CREDENTIAL_SHAPES.some((pattern) => pattern.test(message))) {\n\t\tthrow new TypeError(\"message contains a credential-shaped literal; redact it before classification\");\n\t}\n}\n\nfunction assertTimestamp(timestamp: string): void {\n\tconst parsed = new Date(timestamp);\n\tif (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== timestamp) {\n\t\tthrow new TypeError(\"timestamp must be a canonical ISO-8601 UTC instant\");\n\t}\n}\n\nfunction classifyProvider(code: ProviderTerminationCauseCode): Classification {\n\tswitch (code) {\n\t\tcase \"abort\":\n\t\t\treturn {\n\t\t\t\tkind: \"provider_abort\",\n\t\t\t\tphase: \"provider\",\n\t\t\t\tcauseCode: \"provider.abort\",\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"auth\":\n\t\t\treturn {\n\t\t\t\tkind: \"provider_auth\",\n\t\t\t\tphase: \"provider\",\n\t\t\t\tcauseCode: \"provider.auth\",\n\t\t\t\tretryable: false,\n\t\t\t};\n\t\tcase \"rate_limit\":\n\t\t\treturn {\n\t\t\t\tkind: \"provider_rate_limit\",\n\t\t\t\tphase: \"provider\",\n\t\t\t\tcauseCode: \"provider.rate_limit\",\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"network\":\n\t\t\treturn {\n\t\t\t\tkind: \"provider_network\",\n\t\t\t\tphase: \"provider\",\n\t\t\t\tcauseCode: \"provider.network\",\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"protocol\":\n\t\t\t// Many \"protocol\" stops are sticky transcript shape bugs (orphan tool_call_id)\n\t\t\t// that succeed after transform-messages sanitizes and the turn is retried.\n\t\t\t// Mark retryable so auto-retry can heal; safeToAutoRetry stays false (needs sanitize path).\n\t\t\treturn {\n\t\t\t\tkind: \"provider_protocol\",\n\t\t\t\tphase: \"provider\",\n\t\t\t\tcauseCode: \"provider.protocol\",\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"context_overflow\":\n\t\t\treturn {\n\t\t\t\tkind: \"context_overflow\",\n\t\t\t\tphase: \"provider\",\n\t\t\t\tcauseCode: \"provider.context_overflow\",\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"refusal\":\n\t\t\treturn {\n\t\t\t\tkind: \"provider_refusal\",\n\t\t\t\tphase: \"provider\",\n\t\t\t\tcauseCode: \"provider.refusal\",\n\t\t\t\tretryable: true,\n\t\t\t};\n\t}\n}\n\nfunction classifyCause(cause: SessionTerminationCause, source: SessionTerminationSource): Classification {\n\tswitch (cause.area) {\n\t\tcase \"completed\":\n\t\t\treturn {\n\t\t\t\tkind: \"completed\",\n\t\t\t\tphase: \"completed\",\n\t\t\t\tcauseCode: \"session.completed\",\n\t\t\t\tretryable: false,\n\t\t\t};\n\t\tcase \"user\":\n\t\t\treturn {\n\t\t\t\tkind: \"user_abort\",\n\t\t\t\tphase: \"control\",\n\t\t\t\tcauseCode: \"session.user_abort\",\n\t\t\t\tretryable: false,\n\t\t\t};\n\t\tcase \"provider\":\n\t\t\treturn classifyProvider(cause.code);\n\t\tcase \"tool\":\n\t\t\treturn cause.code === \"timeout\"\n\t\t\t\t? { kind: \"tool_timeout\", phase: \"tool\", causeCode: \"tool.timeout\", retryable: true }\n\t\t\t\t: { kind: \"tool_fatal\", phase: \"tool\", causeCode: \"tool.fatal\", retryable: false };\n\t\tcase \"compaction\":\n\t\t\treturn {\n\t\t\t\tkind: \"compaction\",\n\t\t\t\tphase: \"compaction\",\n\t\t\t\tcauseCode: `compaction.${cause.code}`,\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"persistence\":\n\t\t\treturn {\n\t\t\t\tkind: \"persistence\",\n\t\t\t\tphase: \"persistence\",\n\t\t\t\tcauseCode: `persistence.${cause.code}`,\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"process\":\n\t\t\tif (cause.code === \"signal\") {\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"process_signal\",\n\t\t\t\t\tphase: \"process\",\n\t\t\t\t\tcauseCode: \"process.signal\",\n\t\t\t\t\tretryable: false,\n\t\t\t\t\tprocessSignal: cause.signal,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tkind: \"process_crash\",\n\t\t\t\tphase: \"resume\",\n\t\t\t\tcauseCode: \"process.crash\",\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"transcript\":\n\t\t\treturn {\n\t\t\t\tkind: \"transcript_invalid\",\n\t\t\t\tphase: source === \"inferred_on_resume\" ? \"resume\" : \"preflight\",\n\t\t\t\tcauseCode: `transcript.${cause.code}`,\n\t\t\t\tretryable: false,\n\t\t\t\ttranscriptIssue: cause.code,\n\t\t\t};\n\t\tcase \"configuration\":\n\t\t\treturn {\n\t\t\t\tkind: \"configuration\",\n\t\t\t\tphase: \"preflight\",\n\t\t\t\tcauseCode: \"configuration.invalid\",\n\t\t\t\tretryable: false,\n\t\t\t};\n\t\tcase \"internal\":\n\t\t\treturn {\n\t\t\t\tkind: \"internal_error\",\n\t\t\t\tphase: \"preflight\",\n\t\t\t\tcauseCode: \"internal.unclassified\",\n\t\t\t\tretryable: false,\n\t\t\t};\n\t\tcase \"resource\":\n\t\t\t// §15.4: every resource cause is retryable; queue overflow surfaces at\n\t\t\t// the tool boundary, everything else at preflight admission.\n\t\t\treturn {\n\t\t\t\tkind: \"resource_pressure\",\n\t\t\t\tphase: cause.code === \"queue_overflow\" ? \"tool\" : \"preflight\",\n\t\t\t\tcauseCode: `resource.${cause.code}`,\n\t\t\t\tretryable: true,\n\t\t\t};\n\t}\n}\n\nfunction isSafeToAutoRetry(classification: Classification, input: ClassifySessionTerminationInput): boolean {\n\t// provider_refusal: Fable/Claude often false-positive on benign turns. Safe to auto-retry once\n\t// when no tool side effects occurred (same class of recovery as rate_limit/network).\n\treturn (\n\t\tclassification.retryable &&\n\t\tinput.source === \"observed\" &&\n\t\tinput.sideEffects === \"none\" &&\n\t\t(classification.kind === \"provider_rate_limit\" ||\n\t\t\tclassification.kind === \"provider_network\" ||\n\t\t\tclassification.kind === \"provider_refusal\" ||\n\t\t\t// §15.4 mapping: only cpu pressure is safe to auto-retry (with delay).\n\t\t\tclassification.causeCode === \"resource.cpu\")\n\t);\n}\n\nfunction nextActionFor(classification: Classification, input: ClassifySessionTerminationInput): string {\n\tswitch (classification.kind) {\n\t\tcase \"completed\":\n\t\t\treturn \"No recovery action is required; continue with the next prompt.\";\n\t\tcase \"user_abort\":\n\t\t\treturn \"Review possible partial side effects, then retry only if intended.\";\n\t\tcase \"resource_pressure\":\n\t\t\treturn \"Host resources are constrained; reduce load or wait for recovery, then retry.\";\n\t\tcase \"provider_abort\":\n\t\t\treturn \"Confirm provider availability, then retry the request.\";\n\t\tcase \"provider_auth\":\n\t\t\treturn `Run /login${input.provider ? ` ${input.provider}` : \"\"} or configure valid credentials, then retry.`;\n\t\tcase \"provider_rate_limit\":\n\t\t\treturn \"Wait for the provider retry window or choose another model, then retry.\";\n\t\tcase \"provider_network\":\n\t\t\treturn \"Check network and provider connectivity, then retry.\";\n\t\tcase \"provider_protocol\":\n\t\t\treturn \"Often an orphan tool_call_id / sticky transcript after a dropped error turn — auto-retry after sanitize, or /new session if it persists.\";\n\t\tcase \"provider_refusal\":\n\t\t\treturn \"Model declined this turn (content/safety stop). Usually a false positive on Fable/Claude — auto-retry once, or switch model (k3/qwen3.8-max/grok-4.5/deepseek) / rephrase as a pure coding task.\";\n\t\tcase \"context_overflow\":\n\t\t\treturn \"Compact or reduce context, or switch to a larger-context model.\";\n\t\tcase \"tool_timeout\":\n\t\t\treturn \"Inspect possible tool side effects and increase the tool timeout only if safe.\";\n\t\tcase \"tool_fatal\":\n\t\t\treturn \"Inspect the failed tool result and repair its configuration before retrying.\";\n\t\tcase \"compaction\":\n\t\t\tswitch (classification.causeCode) {\n\t\t\t\tcase \"compaction.aborted\":\n\t\t\t\t\treturn \"Retry /compact if cancellation was unintended; avoid Escape while compaction is running.\";\n\t\t\t\tcase \"compaction.stale\":\n\t\t\t\t\treturn \"Retry /compact after pending session changes settle.\";\n\t\t\t\tdefault:\n\t\t\t\t\treturn \"Resolve the reported compaction error, then retry /compact.\";\n\t\t\t}\n\t\tcase \"persistence\":\n\t\t\treturn `Run omk session doctor --session ${input.sessionId} before resuming.`;\n\t\tcase \"process_signal\":\n\t\t\treturn \"Review possible partial side effects before resuming the session.\";\n\t\tcase \"process_crash\":\n\t\t\treturn `Run omk session doctor --session ${input.sessionId} and review partial side effects.`;\n\t\tcase \"transcript_invalid\":\n\t\t\treturn `Run omk session doctor --session ${input.sessionId}; do not resume until integrity passes.`;\n\t\tcase \"configuration\":\n\t\t\treturn \"Correct the provider, model, tool, or session configuration and retry.\";\n\t\tcase \"internal_error\":\n\t\t\treturn `Inspect run ${input.runId} diagnostics and the run journal before retrying.`;\n\t}\n}\n\n/** Concise actionable rendering shared by non-JSON and TUI surfaces. */\nexport function formatSessionTermination(termination: SessionTermination): string {\n\tconst route =\n\t\ttermination.provider || termination.model\n\t\t\t? `${termination.provider ?? \"unknown\"}/${termination.model ?? \"unknown\"}`\n\t\t\t: \"n/a\";\n\treturn [\n\t\t`kind=${termination.kind}`,\n\t\t`provider/model=${route}`,\n\t\t`retryable=${termination.retryable}`,\n\t\t`cause=${termination.causeCode}`,\n\t\t`message=${termination.message}`,\n\t\t`run=${termination.runId}`,\n\t\t`next=${termination.nextAction}`,\n\t].join(\" \");\n}\n\n/**\n * Classify a bounded, structured termination cause. This intentionally accepts\n * no raw Error value, stack, provider body, or arbitrary cause code.\n */\nexport function classifySessionTermination(input: ClassifySessionTerminationInput): SessionTermination {\n\tassertClassifierInput(input);\n\tassertIdentifier(input.sessionId, \"sessionId\", 128);\n\tassertIdentifier(input.runId, \"runId\", 128);\n\tassertTimestamp(input.timestamp);\n\tassertPreRedactedTerminationMessage(input.message);\n\tif (input.provider !== undefined) assertIdentifier(input.provider, \"provider\", 128);\n\tif (input.model !== undefined) assertIdentifier(input.model, \"model\", 256);\n\tif (input.toolCallId !== undefined) assertIdentifier(input.toolCallId, \"toolCallId\", 512);\n\tif (input.toolName !== undefined) assertIdentifier(input.toolName, \"toolName\", 128);\n\tif (input.retryAfterMs !== undefined && (!Number.isSafeInteger(input.retryAfterMs) || input.retryAfterMs < 0)) {\n\t\tthrow new TypeError(\"retryAfterMs must be a non-negative safe integer\");\n\t}\n\tif (input.cause.area === \"process\" && input.cause.code === \"crash\" && input.source !== \"inferred_on_resume\") {\n\t\tthrow new TypeError(\"process crashes may only be classified as inferred_on_resume\");\n\t}\n\tif (input.cause.area === \"process\" && input.cause.code === \"signal\" && input.source !== \"observed\") {\n\t\tthrow new TypeError(\"process signals must be observed\");\n\t}\n\n\tconst classification = classifyCause(input.cause, input.source);\n\treturn Object.freeze({\n\t\tschemaVersion: SESSION_TERMINATION_SCHEMA_VERSION,\n\t\tsessionId: input.sessionId,\n\t\trunId: input.runId,\n\t\tkind: classification.kind,\n\t\tphase: classification.phase,\n\t\tsource: input.source,\n\t\tmessage: input.message,\n\t\tcauseCode: classification.causeCode,\n\t\tnextAction: nextActionFor(classification, input),\n\t\tretryable: classification.retryable,\n\t\tsafeToAutoRetry: isSafeToAutoRetry(classification, input),\n\t\tsideEffects: input.sideEffects,\n\t\ttimestamp: input.timestamp,\n\t\t...(input.retryAfterMs === undefined ? {} : { retryAfterMs: input.retryAfterMs }),\n\t\t...(input.provider === undefined ? {} : { provider: input.provider }),\n\t\t...(input.model === undefined ? {} : { model: input.model }),\n\t\t...(input.toolCallId === undefined ? {} : { toolCallId: input.toolCallId }),\n\t\t...(input.toolName === undefined ? {} : { toolName: input.toolName }),\n\t\t...(classification.processSignal === undefined ? {} : { processSignal: classification.processSignal }),\n\t\t...(classification.transcriptIssue === undefined ? {} : { transcriptIssue: classification.transcriptIssue }),\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-termination.d.ts","sourceRoot":"","sources":["../../src/core/session-termination.ts"],"names":[],"mappings":"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","sourcesContent":["export const SESSION_TERMINATION_SCHEMA_VERSION = 1 as const;\nexport const MAX_SESSION_TERMINATION_MESSAGE_LENGTH = 512;\n\n/**\n * Canonical list of termination kinds. The run-journal validator derives its\n * runtime bounds from this tuple so a newly added kind can never crash\n * journaling with \"termination kind is out of bounds\" again.\n */\nexport const SESSION_TERMINATION_KIND_VALUES = [\n\t\"completed\",\n\t\"user_abort\",\n\t\"provider_abort\",\n\t\"provider_auth\",\n\t\"provider_rate_limit\",\n\t\"provider_network\",\n\t\"provider_protocol\",\n\t\"provider_refusal\",\n\t\"context_overflow\",\n\t\"transcript_invalid\",\n\t\"tool_timeout\",\n\t\"tool_fatal\",\n\t\"compaction\",\n\t\"persistence\",\n\t\"process_signal\",\n\t\"process_crash\",\n\t\"configuration\",\n\t\"internal_error\",\n\t\"resource_pressure\",\n] as const;\n\nexport type SessionTerminationKind = (typeof SESSION_TERMINATION_KIND_VALUES)[number];\n\nexport type SessionTerminationPhase =\n\t| \"completed\"\n\t| \"control\"\n\t| \"preflight\"\n\t| \"provider\"\n\t| \"tool\"\n\t| \"compaction\"\n\t| \"persistence\"\n\t| \"process\"\n\t| \"resume\";\n\nexport type SessionTerminationSource = \"observed\" | \"inferred_on_resume\";\nexport type SessionSideEffects = \"none\" | \"possible\" | \"confirmed\";\nexport type SessionProcessSignal = \"SIGINT\" | \"SIGTERM\" | \"SIGHUP\" | \"SIGQUIT\";\n\nexport type ProviderTerminationCauseCode =\n\t| \"abort\"\n\t| \"auth\"\n\t| \"rate_limit\"\n\t| \"network\"\n\t| \"protocol\"\n\t| \"refusal\"\n\t| \"context_overflow\";\nexport type ToolTerminationCauseCode = \"timeout\" | \"fatal\";\n/** §15.4 resource-aware runtime roadmap: host-pressure termination causes. */\nexport type ResourceTerminationCauseCode = \"memory\" | \"disk\" | \"cpu\" | \"heap\" | \"probe_unavailable\" | \"queue_overflow\";\nexport type CompactionTerminationCauseCode = \"aborted\" | \"failed\" | \"stale\" | \"quota_exhausted\";\nexport type PersistenceTerminationCauseCode =\n\t| \"read_failed\"\n\t| \"append_failed\"\n\t| \"replace_failed\"\n\t| \"fsync_failed\"\n\t| \"lock_failed\";\nexport type TranscriptTerminationCauseCode =\n\t| \"missing_result\"\n\t| \"duplicate_result\"\n\t| \"orphan_result\"\n\t| \"duplicate_call_id\"\n\t| \"interleaved_non_result\"\n\t| \"invalid_jsonl\"\n\t| \"invalid_tree\"\n\t| \"unsupported_version\"\n\t| \"trailing_fragment\";\nexport type SessionTerminationCauseCode =\n\t| \"session.completed\"\n\t| \"session.user_abort\"\n\t| `provider.${ProviderTerminationCauseCode}`\n\t| `tool.${ToolTerminationCauseCode}`\n\t| `compaction.${CompactionTerminationCauseCode}`\n\t| `persistence.${PersistenceTerminationCauseCode}`\n\t| \"process.signal\"\n\t| \"process.crash\"\n\t| `transcript.${TranscriptTerminationCauseCode}`\n\t| \"configuration.invalid\"\n\t| \"internal.unclassified\"\n\t| `resource.${ResourceTerminationCauseCode}`;\n\nexport type SessionTerminationCause =\n\t| { readonly area: \"completed\" }\n\t| { readonly area: \"user\"; readonly code: \"abort\" }\n\t| { readonly area: \"provider\"; readonly code: ProviderTerminationCauseCode }\n\t| { readonly area: \"tool\"; readonly code: ToolTerminationCauseCode }\n\t| { readonly area: \"compaction\"; readonly code: CompactionTerminationCauseCode }\n\t| { readonly area: \"persistence\"; readonly code: PersistenceTerminationCauseCode }\n\t| { readonly area: \"process\"; readonly code: \"signal\"; readonly signal: SessionProcessSignal }\n\t| { readonly area: \"process\"; readonly code: \"crash\" }\n\t| { readonly area: \"transcript\"; readonly code: TranscriptTerminationCauseCode }\n\t| { readonly area: \"configuration\"; readonly code: \"invalid\" }\n\t| { readonly area: \"internal\"; readonly code: \"unclassified\" }\n\t| { readonly area: \"resource\"; readonly code: ResourceTerminationCauseCode };\n\nexport interface ClassifySessionTerminationInput {\n\treadonly sessionId: string;\n\treadonly runId: string;\n\t/** Deterministic caller-provided ISO-8601 timestamp. */\n\treadonly timestamp: string;\n\treadonly source: SessionTerminationSource;\n\t/** Caller-supplied, pre-redacted diagnostic text. */\n\treadonly message: string;\n\treadonly cause: SessionTerminationCause;\n\treadonly sideEffects: SessionSideEffects;\n\treadonly retryAfterMs?: number;\n\treadonly provider?: string;\n\treadonly model?: string;\n\treadonly toolCallId?: string;\n\treadonly toolName?: string;\n}\n\nexport interface SessionTermination {\n\treadonly schemaVersion: typeof SESSION_TERMINATION_SCHEMA_VERSION;\n\treadonly sessionId: string;\n\treadonly runId: string;\n\treadonly kind: SessionTerminationKind;\n\treadonly phase: SessionTerminationPhase;\n\treadonly source: SessionTerminationSource;\n\treadonly message: string;\n\treadonly causeCode: SessionTerminationCauseCode;\n\t/** Stable operator guidance suitable for print, JSON, RPC, and TUI surfaces. */\n\treadonly nextAction: string;\n\treadonly retryable: boolean;\n\treadonly safeToAutoRetry: boolean;\n\treadonly sideEffects: SessionSideEffects;\n\treadonly timestamp: string;\n\treadonly retryAfterMs?: number;\n\treadonly provider?: string;\n\treadonly model?: string;\n\treadonly toolCallId?: string;\n\treadonly toolName?: string;\n\treadonly processSignal?: SessionProcessSignal;\n\treadonly transcriptIssue?: TranscriptTerminationCauseCode;\n}\n\nexport class SessionTerminationError extends Error {\n\treadonly termination: SessionTermination;\n\n\tconstructor(message: string, termination: SessionTermination) {\n\t\tsuper(message);\n\t\tthis.name = \"SessionTerminationError\";\n\t\tthis.termination = termination;\n\t}\n}\n\ninterface Classification {\n\treadonly kind: SessionTerminationKind;\n\treadonly phase: SessionTerminationPhase;\n\treadonly causeCode: SessionTerminationCauseCode;\n\treadonly retryable: boolean;\n\treadonly processSignal?: SessionProcessSignal;\n\treadonly transcriptIssue?: TranscriptTerminationCauseCode;\n}\n\nconst CREDENTIAL_SHAPES = [\n\t/-----BEGIN [A-Z ]*PRIVATE KEY-----/i,\n\t/\\b(?:sk-[A-Za-z0-9_-]{16,}|github_pat_[A-Za-z0-9_]{16,}|gh[pousr]_[A-Za-z0-9]{16,}|xox[baprs]-[A-Za-z0-9-]{16,}|AKIA[A-Z0-9]{16})\\b/,\n\t/\\bBearer\\s+[A-Za-z0-9._~+/-]{8,}={0,2}\\b/i,\n\t/\\beyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\b/,\n\t/(?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]+[_-])*(?:authorization|api[-_ ]?key|access[-_ ]?token|token|password|secret(?:[-_ ]?key)?|private[-_ ]?key|cookie)[\"']?\\s*[:=]/i,\n] as const;\n\nconst TOP_LEVEL_KEYS = new Set([\n\t\"sessionId\",\n\t\"runId\",\n\t\"timestamp\",\n\t\"source\",\n\t\"message\",\n\t\"cause\",\n\t\"sideEffects\",\n\t\"retryAfterMs\",\n\t\"provider\",\n\t\"model\",\n\t\"toolCallId\",\n\t\"toolName\",\n]);\nconst PROVIDER_CAUSE_CODES = new Set<ProviderTerminationCauseCode>([\n\t\"abort\",\n\t\"auth\",\n\t\"rate_limit\",\n\t\"network\",\n\t\"protocol\",\n\t\"refusal\",\n\t\"context_overflow\",\n]);\nconst TOOL_CAUSE_CODES = new Set<ToolTerminationCauseCode>([\"timeout\", \"fatal\"]);\nconst RESOURCE_CAUSE_CODES = new Set<ResourceTerminationCauseCode>([\n\t\"memory\",\n\t\"disk\",\n\t\"cpu\",\n\t\"heap\",\n\t\"probe_unavailable\",\n\t\"queue_overflow\",\n]);\nconst COMPACTION_CAUSE_CODES = new Set<CompactionTerminationCauseCode>([\n\t\"aborted\",\n\t\"failed\",\n\t\"stale\",\n\t\"quota_exhausted\",\n]);\nconst PERSISTENCE_CAUSE_CODES = new Set<PersistenceTerminationCauseCode>([\n\t\"read_failed\",\n\t\"append_failed\",\n\t\"replace_failed\",\n\t\"fsync_failed\",\n\t\"lock_failed\",\n]);\nconst TRANSCRIPT_CAUSE_CODES = new Set<TranscriptTerminationCauseCode>([\n\t\"missing_result\",\n\t\"duplicate_result\",\n\t\"orphan_result\",\n\t\"duplicate_call_id\",\n\t\"interleaved_non_result\",\n\t\"invalid_jsonl\",\n\t\"invalid_tree\",\n\t\"unsupported_version\",\n\t\"trailing_fragment\",\n]);\nconst PROCESS_SIGNALS = new Set<SessionProcessSignal>([\"SIGINT\", \"SIGTERM\", \"SIGHUP\", \"SIGQUIT\"]);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasOnlyKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {\n\tconst allowed = new Set(keys);\n\treturn Object.keys(value).every((key) => allowed.has(key));\n}\n\nfunction assertCause(cause: unknown): asserts cause is SessionTerminationCause {\n\tif (!isRecord(cause) || typeof cause.area !== \"string\") {\n\t\tthrow new TypeError(\"cause must be a bounded structured termination cause\");\n\t}\n\tconst code = cause.code;\n\tlet valid = false;\n\tswitch (cause.area) {\n\t\tcase \"completed\":\n\t\t\tvalid = hasOnlyKeys(cause, [\"area\"]);\n\t\t\tbreak;\n\t\tcase \"user\":\n\t\t\tvalid = code === \"abort\" && hasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"provider\":\n\t\t\tvalid =\n\t\t\t\ttypeof code === \"string\" &&\n\t\t\t\tPROVIDER_CAUSE_CODES.has(code as ProviderTerminationCauseCode) &&\n\t\t\t\thasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"tool\":\n\t\t\tvalid =\n\t\t\t\ttypeof code === \"string\" &&\n\t\t\t\tTOOL_CAUSE_CODES.has(code as ToolTerminationCauseCode) &&\n\t\t\t\thasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"compaction\":\n\t\t\tvalid =\n\t\t\t\ttypeof code === \"string\" &&\n\t\t\t\tCOMPACTION_CAUSE_CODES.has(code as CompactionTerminationCauseCode) &&\n\t\t\t\thasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"persistence\":\n\t\t\tvalid =\n\t\t\t\ttypeof code === \"string\" &&\n\t\t\t\tPERSISTENCE_CAUSE_CODES.has(code as PersistenceTerminationCauseCode) &&\n\t\t\t\thasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"process\":\n\t\t\tvalid =\n\t\t\t\t(code === \"crash\" && hasOnlyKeys(cause, [\"area\", \"code\"])) ||\n\t\t\t\t(code === \"signal\" &&\n\t\t\t\t\ttypeof cause.signal === \"string\" &&\n\t\t\t\t\tPROCESS_SIGNALS.has(cause.signal as SessionProcessSignal) &&\n\t\t\t\t\thasOnlyKeys(cause, [\"area\", \"code\", \"signal\"]));\n\t\t\tbreak;\n\t\tcase \"transcript\":\n\t\t\tvalid =\n\t\t\t\ttypeof code === \"string\" &&\n\t\t\t\tTRANSCRIPT_CAUSE_CODES.has(code as TranscriptTerminationCauseCode) &&\n\t\t\t\thasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"configuration\":\n\t\t\tvalid = code === \"invalid\" && hasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"internal\":\n\t\t\tvalid = code === \"unclassified\" && hasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t\tcase \"resource\":\n\t\t\tvalid =\n\t\t\t\ttypeof code === \"string\" &&\n\t\t\t\tRESOURCE_CAUSE_CODES.has(code as ResourceTerminationCauseCode) &&\n\t\t\t\thasOnlyKeys(cause, [\"area\", \"code\"]);\n\t\t\tbreak;\n\t}\n\tif (!valid) throw new TypeError(\"cause must be a bounded structured termination cause\");\n}\n\nfunction assertClassifierInput(input: unknown): asserts input is ClassifySessionTerminationInput {\n\tif (!isRecord(input) || Object.keys(input).some((key) => !TOP_LEVEL_KEYS.has(key))) {\n\t\tthrow new TypeError(\"input must contain only bounded structured termination fields\");\n\t}\n\tif (\n\t\ttypeof input.sessionId !== \"string\" ||\n\t\ttypeof input.runId !== \"string\" ||\n\t\ttypeof input.timestamp !== \"string\" ||\n\t\ttypeof input.message !== \"string\" ||\n\t\t(input.source !== \"observed\" && input.source !== \"inferred_on_resume\") ||\n\t\t(input.sideEffects !== \"none\" && input.sideEffects !== \"possible\" && input.sideEffects !== \"confirmed\") ||\n\t\t(input.retryAfterMs !== undefined && typeof input.retryAfterMs !== \"number\") ||\n\t\t(input.provider !== undefined && typeof input.provider !== \"string\") ||\n\t\t(input.model !== undefined && typeof input.model !== \"string\") ||\n\t\t(input.toolCallId !== undefined && typeof input.toolCallId !== \"string\") ||\n\t\t(input.toolName !== undefined && typeof input.toolName !== \"string\")\n\t) {\n\t\tthrow new TypeError(\"input must contain only bounded structured termination fields\");\n\t}\n\tassertCause(input.cause);\n}\n\nfunction assertIdentifier(value: string, name: string, maxLength: number): void {\n\tif (value.length === 0 || value.length > maxLength || /[\\u0000-\\u001f\\u007f]/.test(value)) {\n\t\tthrow new TypeError(`${name} must be non-empty bounded text without C0 or DEL control characters`);\n\t}\n}\n\n/** Validate text at the termination trust boundary without attempting redaction. */\nexport function assertPreRedactedTerminationMessage(message: string): void {\n\tif (message.length === 0 || message.length > MAX_SESSION_TERMINATION_MESSAGE_LENGTH) {\n\t\tthrow new TypeError(\n\t\t\t`message must contain 1-${MAX_SESSION_TERMINATION_MESSAGE_LENGTH} characters of pre-redacted text`,\n\t\t);\n\t}\n\tif (message.includes(\"\\0\")) {\n\t\tthrow new TypeError(\"message must not contain NUL\");\n\t}\n\tif (CREDENTIAL_SHAPES.some((pattern) => pattern.test(message))) {\n\t\tthrow new TypeError(\"message contains a credential-shaped literal; redact it before classification\");\n\t}\n}\n\nfunction assertTimestamp(timestamp: string): void {\n\tconst parsed = new Date(timestamp);\n\tif (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== timestamp) {\n\t\tthrow new TypeError(\"timestamp must be a canonical ISO-8601 UTC instant\");\n\t}\n}\n\nfunction classifyProvider(code: ProviderTerminationCauseCode): Classification {\n\tswitch (code) {\n\t\tcase \"abort\":\n\t\t\treturn {\n\t\t\t\tkind: \"provider_abort\",\n\t\t\t\tphase: \"provider\",\n\t\t\t\tcauseCode: \"provider.abort\",\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"auth\":\n\t\t\treturn {\n\t\t\t\tkind: \"provider_auth\",\n\t\t\t\tphase: \"provider\",\n\t\t\t\tcauseCode: \"provider.auth\",\n\t\t\t\tretryable: false,\n\t\t\t};\n\t\tcase \"rate_limit\":\n\t\t\treturn {\n\t\t\t\tkind: \"provider_rate_limit\",\n\t\t\t\tphase: \"provider\",\n\t\t\t\tcauseCode: \"provider.rate_limit\",\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"network\":\n\t\t\treturn {\n\t\t\t\tkind: \"provider_network\",\n\t\t\t\tphase: \"provider\",\n\t\t\t\tcauseCode: \"provider.network\",\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"protocol\":\n\t\t\t// Many \"protocol\" stops are sticky transcript shape bugs (orphan tool_call_id)\n\t\t\t// that succeed after transform-messages sanitizes and the turn is retried.\n\t\t\t// Mark retryable so auto-retry can heal; safeToAutoRetry stays false (needs sanitize path).\n\t\t\treturn {\n\t\t\t\tkind: \"provider_protocol\",\n\t\t\t\tphase: \"provider\",\n\t\t\t\tcauseCode: \"provider.protocol\",\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"context_overflow\":\n\t\t\treturn {\n\t\t\t\tkind: \"context_overflow\",\n\t\t\t\tphase: \"provider\",\n\t\t\t\tcauseCode: \"provider.context_overflow\",\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"refusal\":\n\t\t\treturn {\n\t\t\t\tkind: \"provider_refusal\",\n\t\t\t\tphase: \"provider\",\n\t\t\t\tcauseCode: \"provider.refusal\",\n\t\t\t\tretryable: true,\n\t\t\t};\n\t}\n}\n\nfunction classifyCause(cause: SessionTerminationCause, source: SessionTerminationSource): Classification {\n\tswitch (cause.area) {\n\t\tcase \"completed\":\n\t\t\treturn {\n\t\t\t\tkind: \"completed\",\n\t\t\t\tphase: \"completed\",\n\t\t\t\tcauseCode: \"session.completed\",\n\t\t\t\tretryable: false,\n\t\t\t};\n\t\tcase \"user\":\n\t\t\treturn {\n\t\t\t\tkind: \"user_abort\",\n\t\t\t\tphase: \"control\",\n\t\t\t\tcauseCode: \"session.user_abort\",\n\t\t\t\tretryable: false,\n\t\t\t};\n\t\tcase \"provider\":\n\t\t\treturn classifyProvider(cause.code);\n\t\tcase \"tool\":\n\t\t\treturn cause.code === \"timeout\"\n\t\t\t\t? { kind: \"tool_timeout\", phase: \"tool\", causeCode: \"tool.timeout\", retryable: true }\n\t\t\t\t: { kind: \"tool_fatal\", phase: \"tool\", causeCode: \"tool.fatal\", retryable: false };\n\t\tcase \"compaction\":\n\t\t\treturn {\n\t\t\t\tkind: \"compaction\",\n\t\t\t\tphase: \"compaction\",\n\t\t\t\tcauseCode: `compaction.${cause.code}`,\n\t\t\t\t// Quota exhaustion does not heal by retrying: same-model retries\n\t\t\t\t// keep failing until the provider cycle resets or the model changes.\n\t\t\t\tretryable: cause.code !== \"quota_exhausted\",\n\t\t\t};\n\t\tcase \"persistence\":\n\t\t\treturn {\n\t\t\t\tkind: \"persistence\",\n\t\t\t\tphase: \"persistence\",\n\t\t\t\tcauseCode: `persistence.${cause.code}`,\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"process\":\n\t\t\tif (cause.code === \"signal\") {\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"process_signal\",\n\t\t\t\t\tphase: \"process\",\n\t\t\t\t\tcauseCode: \"process.signal\",\n\t\t\t\t\tretryable: false,\n\t\t\t\t\tprocessSignal: cause.signal,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tkind: \"process_crash\",\n\t\t\t\tphase: \"resume\",\n\t\t\t\tcauseCode: \"process.crash\",\n\t\t\t\tretryable: true,\n\t\t\t};\n\t\tcase \"transcript\":\n\t\t\treturn {\n\t\t\t\tkind: \"transcript_invalid\",\n\t\t\t\tphase: source === \"inferred_on_resume\" ? \"resume\" : \"preflight\",\n\t\t\t\tcauseCode: `transcript.${cause.code}`,\n\t\t\t\tretryable: false,\n\t\t\t\ttranscriptIssue: cause.code,\n\t\t\t};\n\t\tcase \"configuration\":\n\t\t\treturn {\n\t\t\t\tkind: \"configuration\",\n\t\t\t\tphase: \"preflight\",\n\t\t\t\tcauseCode: \"configuration.invalid\",\n\t\t\t\tretryable: false,\n\t\t\t};\n\t\tcase \"internal\":\n\t\t\treturn {\n\t\t\t\tkind: \"internal_error\",\n\t\t\t\tphase: \"preflight\",\n\t\t\t\tcauseCode: \"internal.unclassified\",\n\t\t\t\tretryable: false,\n\t\t\t};\n\t\tcase \"resource\":\n\t\t\t// §15.4: every resource cause is retryable; queue overflow surfaces at\n\t\t\t// the tool boundary, everything else at preflight admission.\n\t\t\treturn {\n\t\t\t\tkind: \"resource_pressure\",\n\t\t\t\tphase: cause.code === \"queue_overflow\" ? \"tool\" : \"preflight\",\n\t\t\t\tcauseCode: `resource.${cause.code}`,\n\t\t\t\tretryable: true,\n\t\t\t};\n\t}\n}\n\nfunction isSafeToAutoRetry(classification: Classification, input: ClassifySessionTerminationInput): boolean {\n\t// provider_refusal: Fable/Claude often false-positive on benign turns. Safe to auto-retry once\n\t// when no tool side effects occurred (same class of recovery as rate_limit/network).\n\treturn (\n\t\tclassification.retryable &&\n\t\tinput.source === \"observed\" &&\n\t\tinput.sideEffects === \"none\" &&\n\t\t(classification.kind === \"provider_rate_limit\" ||\n\t\t\tclassification.kind === \"provider_network\" ||\n\t\t\tclassification.kind === \"provider_refusal\" ||\n\t\t\t// §15.4 mapping: only cpu pressure is safe to auto-retry (with delay).\n\t\t\tclassification.causeCode === \"resource.cpu\")\n\t);\n}\n\nfunction nextActionFor(classification: Classification, input: ClassifySessionTerminationInput): string {\n\tswitch (classification.kind) {\n\t\tcase \"completed\":\n\t\t\treturn \"No recovery action is required; continue with the next prompt.\";\n\t\tcase \"user_abort\":\n\t\t\treturn \"Review possible partial side effects, then retry only if intended.\";\n\t\tcase \"resource_pressure\":\n\t\t\treturn \"Host resources are constrained; reduce load or wait for recovery, then retry.\";\n\t\tcase \"provider_abort\":\n\t\t\treturn \"Confirm provider availability, then retry the request.\";\n\t\tcase \"provider_auth\":\n\t\t\treturn `Run /login${input.provider ? ` ${input.provider}` : \"\"} or configure valid credentials, then retry.`;\n\t\tcase \"provider_rate_limit\":\n\t\t\treturn \"Wait for the provider retry window or choose another model, then retry.\";\n\t\tcase \"provider_network\":\n\t\t\treturn \"Check network and provider connectivity, then retry.\";\n\t\tcase \"provider_protocol\":\n\t\t\treturn \"Often an orphan tool_call_id / sticky transcript after a dropped error turn — auto-retry after sanitize, or /new session if it persists.\";\n\t\tcase \"provider_refusal\":\n\t\t\treturn \"Model declined this turn (content/safety stop). Usually a false positive on Fable/Claude — auto-retry once, or switch model (k3/qwen3.8-max/grok-4.5/deepseek) / rephrase as a pure coding task.\";\n\t\tcase \"context_overflow\":\n\t\t\treturn \"Compact or reduce context, or switch to a larger-context model.\";\n\t\tcase \"tool_timeout\":\n\t\t\treturn \"Inspect possible tool side effects and increase the tool timeout only if safe.\";\n\t\tcase \"tool_fatal\":\n\t\t\treturn \"Inspect the failed tool result and repair its configuration before retrying.\";\n\t\tcase \"compaction\":\n\t\t\tswitch (classification.causeCode) {\n\t\t\t\tcase \"compaction.aborted\":\n\t\t\t\t\treturn \"Retry /compact if cancellation was unintended; avoid Escape while compaction is running.\";\n\t\t\t\tcase \"compaction.stale\":\n\t\t\t\t\treturn \"Retry /compact after pending session changes settle.\";\n\t\t\t\tcase \"compaction.quota_exhausted\":\n\t\t\t\t\treturn \"The summarization model's quota is exhausted — retrying cannot help yet. Switch model (/model), set compaction.model in settings, or wait for the quota reset, then retry /compact. OMK already tried the failover chain automatically.\";\n\t\t\t\tdefault:\n\t\t\t\t\treturn \"Resolve the reported compaction error, then retry /compact.\";\n\t\t\t}\n\t\tcase \"persistence\":\n\t\t\treturn `Run omk session doctor --session ${input.sessionId} before resuming.`;\n\t\tcase \"process_signal\":\n\t\t\treturn \"Review possible partial side effects before resuming the session.\";\n\t\tcase \"process_crash\":\n\t\t\treturn `Run omk session doctor --session ${input.sessionId} and review partial side effects.`;\n\t\tcase \"transcript_invalid\":\n\t\t\treturn `Run omk session doctor --session ${input.sessionId}; do not resume until integrity passes.`;\n\t\tcase \"configuration\":\n\t\t\treturn \"Correct the provider, model, tool, or session configuration and retry.\";\n\t\tcase \"internal_error\":\n\t\t\treturn `Inspect run ${input.runId} diagnostics and the run journal before retrying.`;\n\t}\n}\n\n/** Concise actionable rendering shared by non-JSON and TUI surfaces. */\nexport function formatSessionTermination(termination: SessionTermination): string {\n\tconst route =\n\t\ttermination.provider || termination.model\n\t\t\t? `${termination.provider ?? \"unknown\"}/${termination.model ?? \"unknown\"}`\n\t\t\t: \"n/a\";\n\treturn [\n\t\t`kind=${termination.kind}`,\n\t\t`provider/model=${route}`,\n\t\t`retryable=${termination.retryable}`,\n\t\t`cause=${termination.causeCode}`,\n\t\t`message=${termination.message}`,\n\t\t`run=${termination.runId}`,\n\t\t`next=${termination.nextAction}`,\n\t].join(\" \");\n}\n\n/**\n * Classify a bounded, structured termination cause. This intentionally accepts\n * no raw Error value, stack, provider body, or arbitrary cause code.\n */\nexport function classifySessionTermination(input: ClassifySessionTerminationInput): SessionTermination {\n\tassertClassifierInput(input);\n\tassertIdentifier(input.sessionId, \"sessionId\", 128);\n\tassertIdentifier(input.runId, \"runId\", 128);\n\tassertTimestamp(input.timestamp);\n\tassertPreRedactedTerminationMessage(input.message);\n\tif (input.provider !== undefined) assertIdentifier(input.provider, \"provider\", 128);\n\tif (input.model !== undefined) assertIdentifier(input.model, \"model\", 256);\n\tif (input.toolCallId !== undefined) assertIdentifier(input.toolCallId, \"toolCallId\", 512);\n\tif (input.toolName !== undefined) assertIdentifier(input.toolName, \"toolName\", 128);\n\tif (input.retryAfterMs !== undefined && (!Number.isSafeInteger(input.retryAfterMs) || input.retryAfterMs < 0)) {\n\t\tthrow new TypeError(\"retryAfterMs must be a non-negative safe integer\");\n\t}\n\tif (input.cause.area === \"process\" && input.cause.code === \"crash\" && input.source !== \"inferred_on_resume\") {\n\t\tthrow new TypeError(\"process crashes may only be classified as inferred_on_resume\");\n\t}\n\tif (input.cause.area === \"process\" && input.cause.code === \"signal\" && input.source !== \"observed\") {\n\t\tthrow new TypeError(\"process signals must be observed\");\n\t}\n\n\tconst classification = classifyCause(input.cause, input.source);\n\treturn Object.freeze({\n\t\tschemaVersion: SESSION_TERMINATION_SCHEMA_VERSION,\n\t\tsessionId: input.sessionId,\n\t\trunId: input.runId,\n\t\tkind: classification.kind,\n\t\tphase: classification.phase,\n\t\tsource: input.source,\n\t\tmessage: input.message,\n\t\tcauseCode: classification.causeCode,\n\t\tnextAction: nextActionFor(classification, input),\n\t\tretryable: classification.retryable,\n\t\tsafeToAutoRetry: isSafeToAutoRetry(classification, input),\n\t\tsideEffects: input.sideEffects,\n\t\ttimestamp: input.timestamp,\n\t\t...(input.retryAfterMs === undefined ? {} : { retryAfterMs: input.retryAfterMs }),\n\t\t...(input.provider === undefined ? {} : { provider: input.provider }),\n\t\t...(input.model === undefined ? {} : { model: input.model }),\n\t\t...(input.toolCallId === undefined ? {} : { toolCallId: input.toolCallId }),\n\t\t...(input.toolName === undefined ? {} : { toolName: input.toolName }),\n\t\t...(classification.processSignal === undefined ? {} : { processSignal: classification.processSignal }),\n\t\t...(classification.transcriptIssue === undefined ? {} : { transcriptIssue: classification.transcriptIssue }),\n\t});\n}\n"]}
|
|
@@ -73,7 +73,12 @@ const RESOURCE_CAUSE_CODES = new Set([
|
|
|
73
73
|
"probe_unavailable",
|
|
74
74
|
"queue_overflow",
|
|
75
75
|
]);
|
|
76
|
-
const COMPACTION_CAUSE_CODES = new Set([
|
|
76
|
+
const COMPACTION_CAUSE_CODES = new Set([
|
|
77
|
+
"aborted",
|
|
78
|
+
"failed",
|
|
79
|
+
"stale",
|
|
80
|
+
"quota_exhausted",
|
|
81
|
+
]);
|
|
77
82
|
const PERSISTENCE_CAUSE_CODES = new Set([
|
|
78
83
|
"read_failed",
|
|
79
84
|
"append_failed",
|
|
@@ -292,7 +297,9 @@ function classifyCause(cause, source) {
|
|
|
292
297
|
kind: "compaction",
|
|
293
298
|
phase: "compaction",
|
|
294
299
|
causeCode: `compaction.${cause.code}`,
|
|
295
|
-
|
|
300
|
+
// Quota exhaustion does not heal by retrying: same-model retries
|
|
301
|
+
// keep failing until the provider cycle resets or the model changes.
|
|
302
|
+
retryable: cause.code !== "quota_exhausted",
|
|
296
303
|
};
|
|
297
304
|
case "persistence":
|
|
298
305
|
return {
|
|
@@ -394,6 +401,8 @@ function nextActionFor(classification, input) {
|
|
|
394
401
|
return "Retry /compact if cancellation was unintended; avoid Escape while compaction is running.";
|
|
395
402
|
case "compaction.stale":
|
|
396
403
|
return "Retry /compact after pending session changes settle.";
|
|
404
|
+
case "compaction.quota_exhausted":
|
|
405
|
+
return "The summarization model's quota is exhausted — retrying cannot help yet. Switch model (/model), set compaction.model in settings, or wait for the quota reset, then retry /compact. OMK already tried the failover chain automatically.";
|
|
397
406
|
default:
|
|
398
407
|
return "Resolve the reported compaction error, then retry /compact.";
|
|
399
408
|
}
|