@sema-agent/core 5.65.0 → 6.0.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 +46 -0
- package/dist/agents/subagent.d.ts +2 -2
- package/dist/agents/subagent.js +11 -0
- package/dist/agents/verify.d.ts +1 -1
- package/dist/brain/anthropic.js +1 -1
- package/dist/brain/errors.d.ts +29 -0
- package/dist/brain/errors.js +20 -0
- package/dist/brain/open-responses.js +2 -2
- package/dist/brain/route-adjudicator.d.ts +8 -1
- package/dist/brain/route-adjudicator.js +1 -0
- package/dist/brain/status-sink.js +12 -1
- package/dist/brain/stream-engine.js +17 -6
- package/dist/core/auto-compaction.d.ts +26 -0
- package/dist/core/auto-compaction.js +7 -2
- package/dist/core/auto-mode-arming.d.ts +138 -0
- package/dist/core/auto-mode-arming.js +181 -0
- package/dist/core/auto-mode-defaults.d.ts +13 -0
- package/dist/core/auto-mode-defaults.js +5 -0
- package/dist/core/auto-mode-prompt.d.ts +14 -3
- package/dist/core/auto-mode-prompt.js +10 -7
- package/dist/core/auto-mode-rebuild.d.ts +75 -0
- package/dist/core/auto-mode-rebuild.js +41 -0
- package/dist/core/auto-mode.d.ts +15 -0
- package/dist/core/auto-mode.js +4 -2
- package/dist/core/checkpoint-store.d.ts +18 -0
- package/dist/core/context-edit.d.ts +47 -5
- package/dist/core/context-guard.d.ts +1 -1
- package/dist/core/file-history-retention.d.ts +106 -0
- package/dist/core/file-history-retention.js +36 -0
- package/dist/core/file-history-store.d.ts +768 -0
- package/dist/core/file-history-store.js +880 -0
- package/dist/core/governance-codes.d.ts +2 -1
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.d.ts +39 -7
- package/dist/core/hooks.js +38 -21
- package/dist/core/lsp.d.ts +2 -2
- package/dist/core/mcp.d.ts +29 -7
- package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
- package/dist/core/memory-engine/consolidation-driver.js +71 -4
- package/dist/core/memory-engine/consolidation.d.ts +25 -2
- package/dist/core/memory-engine/consolidation.js +4 -1
- package/dist/core/memory-engine/distiller.d.ts +84 -1
- package/dist/core/memory-engine/distiller.js +68 -0
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +328 -15
- package/dist/core/memory-engine/engine.js +355 -29
- package/dist/core/memory-engine/file-backend.d.ts +30 -0
- package/dist/core/memory-engine/file-backend.js +14 -13
- package/dist/core/memory-engine/frontmatter.d.ts +22 -1
- package/dist/core/memory-engine/frontmatter.js +3 -0
- package/dist/core/memory-engine/header-hints.d.ts +5 -0
- package/dist/core/memory-engine/index.d.ts +5 -4
- package/dist/core/memory-engine/index.js +5 -4
- package/dist/core/memory-engine/layout.d.ts +88 -2
- package/dist/core/memory-engine/layout.js +112 -3
- package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
- package/dist/core/memory-engine/provenance-wording.js +3 -0
- package/dist/core/memory-engine/tools.d.ts +89 -8
- package/dist/core/memory-engine/tools.js +263 -22
- package/dist/core/memory-engine/types.d.ts +64 -1
- package/dist/core/memory-recall.d.ts +6 -0
- package/dist/core/memory.d.ts +27 -1
- package/dist/core/memory.js +16 -2
- package/dist/core/permission-rule-consent.d.ts +20 -0
- package/dist/core/permission-rule-consent.js +12 -3
- package/dist/core/permission-rule-model.d.ts +67 -7
- package/dist/core/permission-rule-model.js +53 -7
- package/dist/core/permission-rule-store.js +15 -10
- package/dist/core/permission-rule-sync.js +15 -11
- package/dist/core/retention-policy.d.ts +9 -0
- package/dist/core/retention-policy.js +5 -2
- package/dist/core/retention.d.ts +13 -2
- package/dist/core/runner/assemble-result.d.ts +19 -1
- package/dist/core/runner/assemble-result.js +17 -2
- package/dist/core/runner/compaction-call-options.d.ts +93 -0
- package/dist/core/runner/compaction-call-options.js +3 -0
- package/dist/core/runner/memory-capture-optout.d.ts +80 -0
- package/dist/core/runner/memory-capture-optout.js +53 -0
- package/dist/core/runner/prepare-config-doors.d.ts +5 -0
- package/dist/core/runner/prepare-config-doors.js +16 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
- package/dist/core/runner/prepare-hands-readface.js +99 -7
- package/dist/core/runner/prepare-memory.d.ts +88 -0
- package/dist/core/runner/prepare-memory.js +305 -24
- package/dist/core/runner/prepare-task.d.ts +141 -1
- package/dist/core/runner/prepare-task.js +443 -79
- package/dist/core/runner/runtask.d.ts +9 -20
- package/dist/core/runner/runtask.js +133 -96
- package/dist/core/runner/session-file-state-replay.d.ts +18 -10
- package/dist/core/runner/session-file-state-replay.js +52 -1
- package/dist/core/runner/tool-disclosure.js +2 -1
- package/dist/core/runner/turn-attachments.d.ts +22 -12
- package/dist/core/session-store.d.ts +1 -1
- package/dist/core/session-store.js +6 -1
- package/dist/core/session.d.ts +34 -1
- package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/file-history-store-contract.js +720 -0
- package/dist/core/task-registry-shared.js +11 -1
- package/dist/core/tool-errors.js +1 -0
- package/dist/core/tool-policy.d.ts +172 -1
- package/dist/core/tool-policy.js +32 -1
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/trace.d.ts +24 -0
- package/dist/core/types.d.ts +784 -89
- package/dist/core/types.js +4 -3
- package/dist/core/untrusted-text.d.ts +1 -1
- package/dist/core/untrusted-text.js +8 -0
- package/dist/core/workflow-run-store-contract.js +6 -2
- package/dist/core/workflow-run-store.d.ts +4 -1
- package/dist/engine/compaction/compaction.d.ts +88 -10
- package/dist/engine/compaction/compaction.js +109 -30
- package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
- package/dist/engine/execution-env/node-execution-env.js +28 -0
- package/dist/engine/harness/agent-harness.d.ts +52 -1
- package/dist/engine/harness/agent-harness.js +36 -1
- package/dist/engine/harness/types.d.ts +26 -1
- package/dist/engine/llm/types.d.ts +50 -4
- package/dist/engine/loop/agent-loop.d.ts +5 -1
- package/dist/engine/loop/agent-loop.js +25 -0
- package/dist/engine/loop/types.d.ts +19 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
- package/dist/engine/session/session.js +1 -1
- package/dist/index.d.ts +18 -8
- package/dist/index.js +14 -6
- package/dist/orchestration/run-workflow-tool.d.ts +20 -2
- package/dist/orchestration/run-workflow-tool.js +22 -3
- package/dist/orchestration/workflow-governance.d.ts +59 -1
- package/dist/orchestration/workflow-governance.js +61 -8
- package/dist/orchestration/workflow-meta.d.ts +4 -2
- package/dist/orchestration/workflow-primitives.js +56 -13
- package/dist/orchestration/workflow-types.d.ts +78 -2
- package/dist/orchestration/workflow.d.ts +20 -0
- package/dist/orchestration/workflow.js +163 -14
- package/dist/prompt-assembly/event-registry.js +1 -1
- package/dist/prompts/default.d.ts +7 -7
- package/dist/stores/file/file-history-store.d.ts +368 -0
- package/dist/stores/file/file-history-store.js +1248 -0
- package/dist/stores/file/index.d.ts +22 -13
- package/dist/stores/file/index.js +4 -4
- package/dist/stores/file/permission-rule-store.js +1 -0
- package/dist/stores/file/strategy-store.d.ts +3 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
- package/dist/tools/fs/bash-readonly-classifier.js +106 -4
- package/dist/tools/fs/fs-bash.js +9 -5
- package/dist/tools/fs/fs-shared.d.ts +52 -1
- package/dist/tools/fs/fs-shared.js +14 -0
- package/dist/tools/fs/fs-write.d.ts +5 -5
- package/dist/tools/fs/fs-write.js +71 -14
- package/dist/tools/fs/index.d.ts +6 -1
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/web.js +2 -1
- package/package.json +5 -1
- package/test/export-surface.snapshot.json +155 -23
- package/dist/core/file-snapshot-store.d.ts +0 -165
- package/dist/core/file-snapshot-store.js +0 -259
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
- package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
- package/dist/stores/file/file-snapshot-store.d.ts +0 -58
- package/dist/stores/file/file-snapshot-store.js +0 -353
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { escapeForDisclosure, hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, } from "./permission-rule-model.js";
|
|
2
|
+
import { escapeForDisclosure, hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, translateImportedWildcardRule, } from "./permission-rule-model.js";
|
|
3
3
|
import { errText, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
|
|
4
4
|
export class InMemoryRuleApprovalRecordStore {
|
|
5
5
|
rows = new Map();
|
|
@@ -538,6 +538,7 @@ export async function prepareCcImport(opts) {
|
|
|
538
538
|
const owner = resolveCallerOwner(opts.principal, opts.owner, "prepareCcImport");
|
|
539
539
|
const candidates = [];
|
|
540
540
|
const skipped = [];
|
|
541
|
+
const translated = [];
|
|
541
542
|
const layers = [];
|
|
542
543
|
const denyAskBuckets = { deny: 0, ask: 0 };
|
|
543
544
|
for (const layer of opts.layers) {
|
|
@@ -614,17 +615,25 @@ export async function prepareCcImport(opts) {
|
|
|
614
615
|
skipped.push({ rule: entry, reason });
|
|
615
616
|
continue;
|
|
616
617
|
}
|
|
617
|
-
const
|
|
618
|
+
const wildcard = translateImportedWildcardRule(entry);
|
|
619
|
+
if (wildcard !== undefined && "skip" in wildcard) {
|
|
620
|
+
skipped.push({ rule: entry, reason: wildcard.skip });
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const text = wildcard?.rule ?? entry;
|
|
624
|
+
const parsed = parseAllowRuleText(text);
|
|
618
625
|
if ("reject" in parsed) {
|
|
619
626
|
skipped.push({ rule: entry, reason: `${parsed.reject.code}: ${parsed.reject.message}` });
|
|
620
627
|
continue;
|
|
621
628
|
}
|
|
622
629
|
if (candidates.some((c) => c.rule === parsed.rule.rule && sameScope(c.scope, scope)))
|
|
623
630
|
continue;
|
|
631
|
+
if (wildcard !== undefined)
|
|
632
|
+
translated.push({ from: entry, to: parsed.rule.rule, scope });
|
|
624
633
|
candidates.push({ rule: parsed.rule.rule, scope });
|
|
625
634
|
}
|
|
626
635
|
}
|
|
627
|
-
const preview = { candidates, skipped, layers, uncovered: { ...IMPORT_UNCOVERED_LAYERS, denyAskBuckets } };
|
|
636
|
+
const preview = { candidates, skipped, translated, layers, uncovered: { ...IMPORT_UNCOVERED_LAYERS, denyAskBuckets } };
|
|
628
637
|
if (candidates.length === 0)
|
|
629
638
|
return { preview };
|
|
630
639
|
const record = {
|
|
@@ -308,7 +308,7 @@ export declare function escapeForDisclosure(value: unknown): string;
|
|
|
308
308
|
* transform is LOSSY and NOT injective (two distinct commands can render identically — that is the
|
|
309
309
|
* price of a readable line). Correlate, match, store and adjudicate on the RAW bytes, which the
|
|
310
310
|
* carrying seats keep unchanged: this function neither changes what those seats contain nor what any
|
|
311
|
-
* of them means. A caller that must instead tell two
|
|
311
|
+
* of them means. A caller that must instead tell two adversarial texts APART wants
|
|
312
312
|
* {@link escapeForDisclosure} (which escapes rather than folds, so nothing collides) or
|
|
313
313
|
* {@link hasUnrenderableCharacters} (refuse instead of render).
|
|
314
314
|
*
|
|
@@ -348,6 +348,47 @@ export declare function parseAllowRuleText(text: string, opts?: {
|
|
|
348
348
|
};
|
|
349
349
|
/** The canonical spelling of a rule for a (already folded) command and match form. */
|
|
350
350
|
export declare function formatAllowRuleText(command: string, match: PersistedRuleMatch): string;
|
|
351
|
+
/**
|
|
352
|
+
* #490 件4 — read ANOTHER product's `Bash(<body> *)` SPACE-STAR spelling as this lane's colon-star
|
|
353
|
+
* prefix rule, for the IMPORT door only.
|
|
354
|
+
*
|
|
355
|
+
* Why a translation is owed at all: the other product mints its prefix suggestions in that spelling
|
|
356
|
+
* (its own classifier reads `X:*` as its earlier prefix form and anything else containing an unescaped star as
|
|
357
|
+
* a wildcard), so a person migrating settings had EVERY prefix rule they had ever approved refused as
|
|
358
|
+
* `unsupported.wildcard` — the import silently emptied out exactly the rules that were the point of it.
|
|
359
|
+
*
|
|
360
|
+
* Translated ONLY for the ONE spelling whose semantics this lane can state exactly: content ending in
|
|
361
|
+
* ` *` with that star the only unescaped one. Upstream compiles a single trailing ` *` into an
|
|
362
|
+
* OPTIONAL suffix (`^body( .*)?$`), which is precisely "the body, or the body followed by a space and
|
|
363
|
+
* more" — this lane's prefix predicate. Any other star placement compiles to a real glob with no rule
|
|
364
|
+
* form here, and a bare `*` is a whole-tool grant that must never become a command rule; both are
|
|
365
|
+
* refused with a reason of their own so the import can DISCLOSE them rather than drop them.
|
|
366
|
+
*
|
|
367
|
+
* WHAT THE TRANSLATED RULE GRANTS, against the source spelling, axis by axis (the direction matters,
|
|
368
|
+
* and it is one-way with one named exception): identical on the bare body, on body-plus-arguments, on
|
|
369
|
+
* the word boundary (`npm run` never reaches `npm run-x`) and on compounds (both sides refuse a chain
|
|
370
|
+
* against a single-command body). NARROWER here on four axes, because upstream normalizes the
|
|
371
|
+
* CANDIDATE before matching and this lane never does: a candidate carrying a redirection, wrapped in
|
|
372
|
+
* `timeout`/`nice`/`nohup`-style words, fed through `xargs`, or prefixed with a `VAR=value` assignment
|
|
373
|
+
* matches there and matches nothing here. WIDER on exactly one: this lane folds runs of whitespace on
|
|
374
|
+
* both sides, so `npm run build` matches the imported rule where upstream's wildcard arm (which does
|
|
375
|
+
* not fold) would refuse it. That axis is NOT introduced by this translation — whitespace folding is
|
|
376
|
+
* this lane's universal reading and already applies to every imported EXACT rule, against an upstream
|
|
377
|
+
* exact arm that is byte-strict — so it is disclosed as a lane property rather than treated as a
|
|
378
|
+
* translation defect. Every other difference runs toward granting less.
|
|
379
|
+
*
|
|
380
|
+
* Returns `undefined` when the text is not a Bash wildcard spelling at all (nothing to translate; the
|
|
381
|
+
* caller proceeds with the text as written), `{ rule }` with the spelling to parse INSTEAD, or
|
|
382
|
+
* `{ skip }` with the reason a star spelling has no form here. Never bypasses the validator: the
|
|
383
|
+
* returned text goes through {@link parseAllowRuleText} exactly like a hand-written one, so the
|
|
384
|
+
* interpreter-head refusal, the control-character screen and the length bound all still apply — a
|
|
385
|
+
* translated `Bash(node *)` is refused `invalid.bare_interpreter_prefix` just as a typed one is.
|
|
386
|
+
*/
|
|
387
|
+
export declare function translateImportedWildcardRule(text: string): {
|
|
388
|
+
rule: string;
|
|
389
|
+
} | {
|
|
390
|
+
skip: string;
|
|
391
|
+
} | undefined;
|
|
351
392
|
/**
|
|
352
393
|
* Does this rule's command pattern admit `command`?
|
|
353
394
|
*
|
|
@@ -477,9 +518,20 @@ export declare function findAdmittingRule(rules: readonly PersistedAllowRule[],
|
|
|
477
518
|
}): readonly PersistedAllowRule[] | undefined;
|
|
478
519
|
/**
|
|
479
520
|
* design/375 §5.2 — the per-segment coverage table for `command`: which segments an eligible rule
|
|
480
|
-
* already admits, judged with the SAME split
|
|
481
|
-
*
|
|
482
|
-
*
|
|
521
|
+
* already admits, judged with the SAME split and the same eligibility predicates as the gate's
|
|
522
|
+
* conjunction arm — one instrument, so the suggestion face and the consent prepare can never call a
|
|
523
|
+
* segment covered that the gate would ask about (or vice versa).
|
|
524
|
+
*
|
|
525
|
+
* The split is the OFFER reading ({@link OFFER_READING}, #490 修①), which is the MATCH reading on
|
|
526
|
+
* every axis but where a redirection is refused — this table exists to align, row for row, with
|
|
527
|
+
* {@link suggestRulesForCommand}'s own segmentation, and a table that stopped one command short of the
|
|
528
|
+
* face it aligns with is a table that face discards (its positional belt), leaving every segment
|
|
529
|
+
* reading UNCOVERED. Each ROW's verdict is still the matcher's, unchanged: `covered` is
|
|
530
|
+
* {@link ruleAdmitsCommand} under the MATCH reading, so a segment carrying a redirection is admitted
|
|
531
|
+
* by nothing and reports `false`. The gate's own conjunction arm keeps the whole-string floor and is
|
|
532
|
+
* not consulted here: for a redirection-bearing compound it has no segments at all, and this table
|
|
533
|
+
* saying "an eligible rule admits `npm test`" is a true statement about that segment either way — it
|
|
534
|
+
* is read only to keep an already-covered segment out of a batch offer, never as an admission.
|
|
483
535
|
*
|
|
484
536
|
* TWO SOURCES, two predicates, and the seat labels are the contract:
|
|
485
537
|
* · `persisted` — rules already in the store, judged {@link eligiblePersisted} ∧ admit: a tombstoned
|
|
@@ -632,9 +684,17 @@ export type RuleOffer = {
|
|
|
632
684
|
* over-offering; every offered rule still passes the validator at redemption, so an over-offer widens
|
|
633
685
|
* nothing).
|
|
634
686
|
*
|
|
635
|
-
* Returns an empty array for anything the rule lane cannot speak for (
|
|
636
|
-
*
|
|
637
|
-
* honest answer.
|
|
687
|
+
* Returns an empty array for anything the rule lane cannot speak for (substitutions, subshells,
|
|
688
|
+
* backgrounding, an unreadable segment) — the card then simply carries no "don't ask again" option,
|
|
689
|
+
* which is the honest answer.
|
|
690
|
+
*
|
|
691
|
+
* A REDIRECTION is the one member of that list judged per SEGMENT rather than over the whole string
|
|
692
|
+
* (#490 修①, {@link OFFER_READING}): `a && b 2>/dev/null && c` offers a batch for `a` and `c` with
|
|
693
|
+
* `uncoveredSegments: 1`, exactly as the same compound dirtied by a glob always has. A SIMPLE command
|
|
694
|
+
* carrying one still offers nothing — its only segment is the dirty one — and no offer this function
|
|
695
|
+
* mints ever admits the redirection-bearing text: the minting grammar and the matcher keep the
|
|
696
|
+
* whole-string floor, so the compound stays un-admitted and the honest `uncoveredSegments` count says
|
|
697
|
+
* so (design/382 §3.2: neither a sanitized rule text nor a widened matcher is on the table).
|
|
638
698
|
*/
|
|
639
699
|
export declare function suggestRulesForCommand(command: string, ctx?: {
|
|
640
700
|
readonly coverage?: readonly SegmentCoverage[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parsePermissionRule } from "./permission-rules.js";
|
|
2
|
-
import { parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
2
|
+
import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
3
3
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
4
4
|
export const MAX_RULE_TEXT_CHARS = 512;
|
|
5
5
|
export const BARE_INTERPRETER_NAMES = new Set([
|
|
@@ -117,17 +117,22 @@ function reject(code, message) {
|
|
|
117
117
|
return { reject: { code, message } };
|
|
118
118
|
}
|
|
119
119
|
const RULE_LANE_FLOOR = { pathPrefixedNameIsText: true };
|
|
120
|
-
const MATCH_READING = { terminator: "keep", quotedOperatorsAreText: false };
|
|
121
|
-
const PROGRAM_RUNS_READING = { terminator: "strip", quotedOperatorsAreText: true };
|
|
120
|
+
const MATCH_READING = { terminator: "keep", quotedOperatorsAreText: false, redirection: "reject" };
|
|
121
|
+
const PROGRAM_RUNS_READING = { terminator: "strip", quotedOperatorsAreText: true, redirection: "reject" };
|
|
122
|
+
const OFFER_READING = { terminator: "keep", quotedOperatorsAreText: false, redirection: "tolerate" };
|
|
122
123
|
function commandBasename(name) {
|
|
123
124
|
return name.slice(name.lastIndexOf("/") + 1);
|
|
124
125
|
}
|
|
125
126
|
function ruleLaneShapeOf(command, reading) {
|
|
126
|
-
const split = splitShellCompoundSegments(command, { trailingTerminator: reading.terminator });
|
|
127
|
+
const split = splitShellCompoundSegments(command, { trailingTerminator: reading.terminator, redirection: reading.redirection });
|
|
127
128
|
if ("reject" in split)
|
|
128
129
|
return { reject: split.reject };
|
|
129
130
|
const names = [];
|
|
130
131
|
for (const segment of split.segments) {
|
|
132
|
+
if (reading.redirection === "tolerate" && carriesShellRedirection(segment)) {
|
|
133
|
+
names.push(undefined);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
131
136
|
const floor = parseLeadingCommandName(segment, { ...RULE_LANE_FLOOR, quotedOperatorsAreText: reading.quotedOperatorsAreText });
|
|
132
137
|
if ("reject" in floor) {
|
|
133
138
|
return {
|
|
@@ -219,6 +224,45 @@ export function parseAllowRuleText(text, opts) {
|
|
|
219
224
|
export function formatAllowRuleText(command, match) {
|
|
220
225
|
return `Bash(${command}${match === "prefix" ? ":*" : ""})`;
|
|
221
226
|
}
|
|
227
|
+
function hasUnescapedStar(s) {
|
|
228
|
+
for (let i = 0; i < s.length; i++) {
|
|
229
|
+
if (s[i] !== "*")
|
|
230
|
+
continue;
|
|
231
|
+
let backslashes = 0;
|
|
232
|
+
for (let j = i - 1; j >= 0 && s[j] === "\\"; j--)
|
|
233
|
+
backslashes++;
|
|
234
|
+
if (backslashes % 2 === 0)
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
function isImportedWildcardContent(content) {
|
|
240
|
+
return !content.endsWith(":*") && hasUnescapedStar(content);
|
|
241
|
+
}
|
|
242
|
+
export function translateImportedWildcardRule(text) {
|
|
243
|
+
const parsed = parsePermissionRule(text);
|
|
244
|
+
const content = parsed.ruleContent;
|
|
245
|
+
if (content === undefined || parsed.toolName !== "Bash" || !isImportedWildcardContent(content))
|
|
246
|
+
return undefined;
|
|
247
|
+
const body = /^(.+) \*$/.exec(content)?.[1];
|
|
248
|
+
if (body === undefined || body.trim() === "") {
|
|
249
|
+
return {
|
|
250
|
+
skip: "unsupported.wildcard: only the trailing `<command> *` form has a prefix-rule equivalent here — it stays in the settings file, unimported",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (hasUnescapedStar(body)) {
|
|
254
|
+
return {
|
|
255
|
+
skip: "unsupported.wildcard: a `*` before the end of the pattern matches anything at that position, which has no rule form in this version — it stays in the settings file, unimported",
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const bodyShape = ruleLaneShapeOf(body, MATCH_READING);
|
|
259
|
+
if (!("reject" in bodyShape) && bodyShape.segments.length > 1) {
|
|
260
|
+
return {
|
|
261
|
+
skip: "unsupported.wildcard: a `<chain> *` pattern names more than one command, and the trailing-star form has no equivalent for a chain — it stays in the settings file, unimported",
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return { rule: formatAllowRuleText(body, "prefix") };
|
|
265
|
+
}
|
|
222
266
|
export function ruleAdmitsCommand(rule, command) {
|
|
223
267
|
return admitsUnder(rule, command, MATCH_READING);
|
|
224
268
|
}
|
|
@@ -297,7 +341,7 @@ export function segmentCoverageOf(command, rules, call) {
|
|
|
297
341
|
const folded = foldSpacing(command);
|
|
298
342
|
if (folded === undefined)
|
|
299
343
|
return undefined;
|
|
300
|
-
const shape = ruleLaneShapeOf(folded,
|
|
344
|
+
const shape = ruleLaneShapeOf(folded, OFFER_READING);
|
|
301
345
|
if ("reject" in shape)
|
|
302
346
|
return undefined;
|
|
303
347
|
const proposed = rules.proposed ?? [];
|
|
@@ -308,7 +352,7 @@ export function segmentCoverageOf(command, rules, call) {
|
|
|
308
352
|
}));
|
|
309
353
|
}
|
|
310
354
|
export function suggestRulesForCommand(command, ctx) {
|
|
311
|
-
const shape = ruleLaneShapeOf(command,
|
|
355
|
+
const shape = ruleLaneShapeOf(command, OFFER_READING);
|
|
312
356
|
if ("reject" in shape)
|
|
313
357
|
return [];
|
|
314
358
|
const folded = foldSpacing(command);
|
|
@@ -333,7 +377,7 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
333
377
|
}
|
|
334
378
|
return offers;
|
|
335
379
|
}
|
|
336
|
-
const foldedShape = ruleLaneShapeOf(folded,
|
|
380
|
+
const foldedShape = ruleLaneShapeOf(folded, OFFER_READING);
|
|
337
381
|
if ("reject" in foldedShape || foldedShape.segments.length !== shape.segments.length)
|
|
338
382
|
return offers;
|
|
339
383
|
const segments = foldedShape.segments.map((s) => s.trim());
|
|
@@ -341,6 +385,8 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
341
385
|
const alignedTable = table !== undefined && table.length === segments.length && table.every((row, i) => row.segment === segments[i]) ? table : undefined;
|
|
342
386
|
const coveredAt = (i) => alignedTable?.[i]?.covered === true;
|
|
343
387
|
const mintSegmentRule = (segment) => {
|
|
388
|
+
if (carriesShellRedirection(segment))
|
|
389
|
+
return undefined;
|
|
344
390
|
const body = bodyOf(tokensOf(segment));
|
|
345
391
|
if (body !== undefined) {
|
|
346
392
|
const parsed = parseAllowRuleText(formatAllowRuleText(body, "prefix"));
|
|
@@ -312,7 +312,7 @@ export function applySyncJoin(cur, delta, nowMs) {
|
|
|
312
312
|
quarantined,
|
|
313
313
|
observedVector: delta.observedVector !== undefined ? joinFrontiers(cur.observedVector ?? {}, delta.observedVector) : cur.observedVector,
|
|
314
314
|
},
|
|
315
|
-
report: { droppedInbound, quarantined: quarantinedNow, collectedTombstones, withheldAdds },
|
|
315
|
+
report: structuredClone({ droppedInbound, quarantined: quarantinedNow, collectedTombstones, withheldAdds }),
|
|
316
316
|
};
|
|
317
317
|
}
|
|
318
318
|
export function assertRedemptionNotQuarantined(quarantined, delta) {
|
|
@@ -327,11 +327,15 @@ export async function ruleStoreChecksum(payload) {
|
|
|
327
327
|
}
|
|
328
328
|
const REMOVE_MAX_ATTEMPTS = 8;
|
|
329
329
|
export async function removePersistedRule(opts) {
|
|
330
|
-
const
|
|
331
|
-
|
|
330
|
+
const rule = opts.rule;
|
|
331
|
+
const scope = structuredClone(opts.scope);
|
|
332
|
+
const target = opts.principal;
|
|
333
|
+
const owner = typeof target === "string" ? { kind: "principal", principal: target } : structuredClone(target);
|
|
334
|
+
const provider = opts.provider;
|
|
335
|
+
if (owner.kind === "local-owner" && provider.forLocalOwner === undefined) {
|
|
332
336
|
return { status: "failed", error: "this provider has no local-owner bucket (forLocalOwner is not implemented) — a local-owner rule cannot be removed through it" };
|
|
333
337
|
}
|
|
334
|
-
const store = owner.kind === "local-owner" ?
|
|
338
|
+
const store = owner.kind === "local-owner" ? provider.forLocalOwner() : provider.forPrincipal(owner.principal);
|
|
335
339
|
const writer = writerOf(store);
|
|
336
340
|
if (writer === undefined) {
|
|
337
341
|
return { status: "failed", error: "the resolved permission-rule store has no write face — rules cannot be removed through it" };
|
|
@@ -343,11 +347,11 @@ export async function removePersistedRule(opts) {
|
|
|
343
347
|
catch (err) {
|
|
344
348
|
return { status: "failed", error: `could not read the permission-rule store: ${errText(err)}` };
|
|
345
349
|
}
|
|
346
|
-
const live = snapshot.rules.filter((r) => r.rule ===
|
|
350
|
+
const live = snapshot.rules.filter((r) => r.rule === rule && sameScope(r.scope, scope));
|
|
347
351
|
const removedDots = live.flatMap((r) => r.adds.map((a) => a.dot));
|
|
348
352
|
if (removedDots.length === 0)
|
|
349
353
|
return { status: "no-op", rev: snapshot.rev };
|
|
350
|
-
const tombstone = { rule
|
|
354
|
+
const tombstone = { rule, scope, removedDots, deletedBy: await writer.nextDot() };
|
|
351
355
|
let expectedRev = snapshot.rev;
|
|
352
356
|
let lastError = "";
|
|
353
357
|
for (let attempt = 0; attempt < REMOVE_MAX_ATTEMPTS; attempt++) {
|
|
@@ -357,7 +361,7 @@ export async function removePersistedRule(opts) {
|
|
|
357
361
|
let stillLive = false;
|
|
358
362
|
try {
|
|
359
363
|
const after = await store.list();
|
|
360
|
-
stillLive = after.rules.some((r) => r.rule ===
|
|
364
|
+
stillLive = after.rules.some((r) => r.rule === rule && sameScope(r.scope, scope) && isRuleLive(r));
|
|
361
365
|
}
|
|
362
366
|
catch {
|
|
363
367
|
stillLive = false;
|
|
@@ -376,7 +380,7 @@ export async function removePersistedRule(opts) {
|
|
|
376
380
|
const after = await store.list();
|
|
377
381
|
const landed = after.tombstones.some((t) => t.deletedBy.actor === tombstone.deletedBy.actor && t.deletedBy.counter === tombstone.deletedBy.counter);
|
|
378
382
|
return landed
|
|
379
|
-
? { status: "removed", rev: after.rev, stillLive: after.rules.some((r) => r.rule ===
|
|
383
|
+
? { status: "removed", rev: after.rev, stillLive: after.rules.some((r) => r.rule === rule && sameScope(r.scope, scope) && isRuleLive(r)) }
|
|
380
384
|
: { status: "failed", error: `${reason} (read-back confirms no tombstone was written)` };
|
|
381
385
|
}
|
|
382
386
|
catch (err) {
|
|
@@ -407,8 +411,8 @@ export class InMemoryPermissionRuleStore {
|
|
|
407
411
|
}
|
|
408
412
|
async list() {
|
|
409
413
|
return {
|
|
410
|
-
rules: applyTombstones(this.rules, this.tombstones)
|
|
411
|
-
tombstones: this.tombstones
|
|
414
|
+
rules: structuredClone(applyTombstones(this.rules, this.tombstones)),
|
|
415
|
+
tombstones: structuredClone(this.tombstones),
|
|
412
416
|
rev: this.rev,
|
|
413
417
|
};
|
|
414
418
|
}
|
|
@@ -427,6 +431,7 @@ export class InMemoryPermissionRuleStore {
|
|
|
427
431
|
quarantined: this.quarantinedRows,
|
|
428
432
|
}),
|
|
429
433
|
apply: async (delta, opts) => {
|
|
434
|
+
delta = structuredClone(delta);
|
|
430
435
|
if (opts.expectedRev !== this.rev)
|
|
431
436
|
return { conflict: true, rev: this.rev };
|
|
432
437
|
if (delta.kind === "redemption-add") {
|
|
@@ -11,14 +11,18 @@ function refuseLocalOwnerSync() {
|
|
|
11
11
|
const SYNC_MAX_ATTEMPTS = 8;
|
|
12
12
|
const DISCLOSED_REFUSED_TOMBSTONES = 5;
|
|
13
13
|
export async function syncPermissionRules(opts) {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const principal = opts.principal;
|
|
15
|
+
const owner = opts.owner;
|
|
16
|
+
const provider = opts.provider;
|
|
17
|
+
const transport = opts.transport;
|
|
18
|
+
if (owner !== undefined) {
|
|
19
|
+
if (owner.kind === "local-owner")
|
|
16
20
|
refuseLocalOwnerSync();
|
|
17
|
-
if (
|
|
18
|
-
throw new Error(`contradictory identity: principal "${escapeForDisclosure(
|
|
21
|
+
if (owner.principal !== principal) {
|
|
22
|
+
throw new Error(`contradictory identity: principal "${escapeForDisclosure(principal)}" and owner principal "${escapeForDisclosure(owner.principal)}" disagree`);
|
|
19
23
|
}
|
|
20
24
|
}
|
|
21
|
-
if (typeof
|
|
25
|
+
if (typeof principal !== "string" || principal === "") {
|
|
22
26
|
throw new Error("syncPermissionRules requires a verified principal — an unauthenticated deployment has no cloud bucket to sync");
|
|
23
27
|
}
|
|
24
28
|
const traceClock = opts.now ?? Date.now;
|
|
@@ -30,7 +34,7 @@ export async function syncPermissionRules(opts) {
|
|
|
30
34
|
emitTrace(tracer, () => ({
|
|
31
35
|
kind: "permission.rule_sync_resurrected",
|
|
32
36
|
version: 1,
|
|
33
|
-
principal
|
|
37
|
+
principal,
|
|
34
38
|
rule: r.rule,
|
|
35
39
|
scopeKind: r.scope.kind,
|
|
36
40
|
ts: traceClock(),
|
|
@@ -40,7 +44,7 @@ export async function syncPermissionRules(opts) {
|
|
|
40
44
|
emitTrace(tracer, () => ({
|
|
41
45
|
kind: "permission.rule_sync_dropped",
|
|
42
46
|
version: 1,
|
|
43
|
-
principal
|
|
47
|
+
principal,
|
|
44
48
|
rule: d.rule,
|
|
45
49
|
scopeKind: d.scope.kind,
|
|
46
50
|
reason: d.reason,
|
|
@@ -49,20 +53,20 @@ export async function syncPermissionRules(opts) {
|
|
|
49
53
|
}
|
|
50
54
|
return result;
|
|
51
55
|
};
|
|
52
|
-
const store =
|
|
56
|
+
const store = provider.forPrincipal(principal);
|
|
53
57
|
const writer = writerOf(store);
|
|
54
58
|
if (writer === undefined) {
|
|
55
59
|
throw new Error("the resolved permission-rule store has no write face — a sync round cannot land through it");
|
|
56
60
|
}
|
|
57
61
|
const raw = await writer.readRaw();
|
|
58
62
|
const request = {
|
|
59
|
-
principal
|
|
63
|
+
principal,
|
|
60
64
|
replica: raw.actor,
|
|
61
65
|
state: { rules: raw.rules, tombstones: raw.tombstones },
|
|
62
66
|
...(raw.observedVector !== undefined ? { observedVector: raw.observedVector } : {}),
|
|
63
67
|
};
|
|
64
|
-
const rawResponse = await
|
|
65
|
-
const response = parseRuleSyncResponse(rawResponse,
|
|
68
|
+
const rawResponse = await transport(PERMISSION_RULE_SYNC_PATH, request);
|
|
69
|
+
const response = parseRuleSyncResponse(rawResponse, principal);
|
|
66
70
|
const warnings = [...response.warnings];
|
|
67
71
|
const dropped = [];
|
|
68
72
|
const inboundTombstones = [];
|
|
@@ -31,6 +31,15 @@ export interface RetentionPolicyKnobs {
|
|
|
31
31
|
/** Row-count bound, whole rows (roster's write-time cap). */
|
|
32
32
|
maxEntries?: number;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The family's typed refusal, exported so a seam whose bound has its own ADMISSIBLE RANGE refuses in
|
|
36
|
+
* the same grammar under the same code instead of minting a second one. The one in-tree case:
|
|
37
|
+
* file-history's boundary cap requires `>= 1` where the row stores accept `0` ("keep no rows" is a
|
|
38
|
+
* coherent policy for a ledger; for a history whose every commit mints a boundary it is not — the
|
|
39
|
+
* cap would reap the boundary its own commit just published), so that door cannot reuse
|
|
40
|
+
* {@link assertRetentionPolicy}'s "0 or more" wording without telling an operator something false.
|
|
41
|
+
*/
|
|
42
|
+
export declare function refuseRetentionPolicy(label: string, knob: string, value: unknown, requirement: string): never;
|
|
34
43
|
/**
|
|
35
44
|
* Refuse a malformed retention policy at the door. `undefined` on any field keeps its documented
|
|
36
45
|
* meaning — that arm is simply not applied — so a deployment that sets nothing is untouched.
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
function
|
|
2
|
-
const e = new Error(`${label}: ${knob} must be ${requirement} (got ${String(value)}) — a retention bound that cannot be evaluated silently decides what to delete instead of bounding it`);
|
|
1
|
+
export function refuseRetentionPolicy(label, knob, value, requirement) {
|
|
2
|
+
const e = new Error(`${label}: ${knob} must be ${requirement} (got ${typeof value === "string" ? JSON.stringify(value) : String(value)}) — a retention bound that cannot be evaluated silently decides what to delete instead of bounding it`);
|
|
3
3
|
e.code = "config.retention_policy_invalid";
|
|
4
4
|
throw e;
|
|
5
5
|
}
|
|
6
|
+
function invalidPolicy(label, knob, value, requirement) {
|
|
7
|
+
refuseRetentionPolicy(label, knob, value, requirement);
|
|
8
|
+
}
|
|
6
9
|
export function assertRetentionPolicy(label, opts) {
|
|
7
10
|
if (opts === undefined)
|
|
8
11
|
return;
|
package/dist/core/retention.d.ts
CHANGED
|
@@ -27,7 +27,12 @@ export interface RetentionPolicy {
|
|
|
27
27
|
* - **idempotent**: a retried invocation after a crash re-converges (already-deleted rows are
|
|
28
28
|
* counted, not errors);
|
|
29
29
|
* - **tombstoned**: deletions leave tombstones so replicas/backups converge instead of resurrecting;
|
|
30
|
-
* - **audited**: every destructive call
|
|
30
|
+
* - **audited**: every destructive call is recorded append-only. The IDENTITY of that record —
|
|
31
|
+
* which occurrence, which of the three operations, at what wall clock — belongs to the side that
|
|
32
|
+
* WRITES the row, not to the receipt: an implementation records it in the same transaction as the
|
|
33
|
+
* deletion (recording after the delete would leave an unwitnessed delete on a crash), and it is
|
|
34
|
+
* the only side that knows its own op, sweep round, and policy generation. The receipt below is
|
|
35
|
+
* therefore the CALL's three counts, deliberately not an audit row.
|
|
31
36
|
*/
|
|
32
37
|
export interface ManagedRetentionCapability {
|
|
33
38
|
/** Enumerate the retention domains (tenant/scope keys) this store holds data for. */
|
|
@@ -54,7 +59,13 @@ export interface ManagedRetentionCapability {
|
|
|
54
59
|
cutoffMs: number;
|
|
55
60
|
}): Promise<RetentionReceipt>;
|
|
56
61
|
}
|
|
57
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* The three counts one destructive retention call produced. NOT an audit row and deliberately
|
|
64
|
+
* identity-free (see the capability's `audited` clause): a scheduler that needs to tell two
|
|
65
|
+
* same-shaped calls apart reads its store's audit records, which the implementation writes with its
|
|
66
|
+
* own occurrence id, operation, and timestamp inside the deleting transaction. Adding those here
|
|
67
|
+
* would ask a return value to know its own call context.
|
|
68
|
+
*/
|
|
58
69
|
export interface RetentionReceipt {
|
|
59
70
|
domain: string;
|
|
60
71
|
/** Rows destroyed by THIS call (idempotent retries count already-gone rows as 0, not errors). */
|
|
@@ -100,6 +100,11 @@ export type BudgetAxis = "tokens" | "cost";
|
|
|
100
100
|
*/
|
|
101
101
|
export interface ResultFlags {
|
|
102
102
|
threw: unknown;
|
|
103
|
+
/** #499 — the run's ENGINE-minted identity (`Prepared.runId`), echoed verbatim on
|
|
104
|
+
* `TaskResult.runId`. Pure pass-through on every terminal: assembly neither mints nor derives it
|
|
105
|
+
* (a caller reaching assembly at all has completed prepare, so the value exists), and a
|
|
106
|
+
* hand-constructed flags bag without one simply produces a result without one. */
|
|
107
|
+
runId?: string;
|
|
103
108
|
/** design/99 MF-25: the EFFECTIVE (resolved) model id that served the task — echoed on `TaskResult.model`. */
|
|
104
109
|
model?: string;
|
|
105
110
|
/** RB-368: some spend was accounted while the serving model had NO configured price table
|
|
@@ -118,6 +123,15 @@ export interface ResultFlags {
|
|
|
118
123
|
* path the terminal is `completed`, and this is what tells that completion apart from a natural
|
|
119
124
|
* one — the model did not finish; the person stopped it and the run awaits their direction). */
|
|
120
125
|
haltedOnUserRejection?: boolean;
|
|
126
|
+
/** design/373 (#504) — a `TaskStream.halt` (the bare user interrupt, CC Esc form) was accepted
|
|
127
|
+
* while the run was live; echoed on `TaskResult.haltedByUser`. Pure pass-through on every
|
|
128
|
+
* terminal (the sibling `haltedOnUserRejection` law: the fact is about the leg that ran) PLUS
|
|
129
|
+
* one status decision it owns (slot 9.7 below): a run whose ONLY stop was this halt reads
|
|
130
|
+
* `"completed"`, never the `!final` failure and never the aborted terminal — whether the halt
|
|
131
|
+
* landed before the model said anything (empty result) or cut a turn whose partial text the
|
|
132
|
+
* brain kept (that text IS the result). The person stopped the run, which is the verb working,
|
|
133
|
+
* not a defect. A REAL abort/limit that also fired still owns the terminal above it. */
|
|
134
|
+
userHalted?: boolean;
|
|
121
135
|
/** design/174 final-round: call ids of answered-but-never-collected questions, echoed on
|
|
122
136
|
* `TaskResult.strandedHumanAnswers`. Pure pass-through; empty/absent ⇒ the field is omitted. The
|
|
123
137
|
* optional `onError` alert is NOT the disclosure — this mandatory result face is. */
|
|
@@ -240,7 +254,11 @@ export declare function errorCodeOf(err: unknown): string | undefined;
|
|
|
240
254
|
* discriminant guarantees only one is ever set)
|
|
241
255
|
* 9. `abortedLive` | stopReason aborted → `"limits.max_walltime_exceeded"` | `"limits.max_turns_exceeded"` | undefined
|
|
242
256
|
* (audit A-1: hoisted ABOVE no-final — the loop exits cleanly on abort, so `final` may be a
|
|
243
|
-
* normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`)
|
|
257
|
+
* normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`),
|
|
258
|
+
* EXCEPT when the aborted `final` is the bare halt's OWN cut artifact (`haltOwnsAbortedFinal`)
|
|
259
|
+
* 9.7 `userHalted` + (no `final` | the halt's own aborted `final`) → status `"completed"` (#504: a
|
|
260
|
+
* bare user halt — the verb working, not a defect; below the aborts on purpose, a real
|
|
261
|
+
* abort/limit that also fired owns the terminal and the halt rides as the pass-through seat)
|
|
244
262
|
* 10. no `final` → status `"failed"`
|
|
245
263
|
* 11. stopReason error → brain `[code]` prefix (or `"conflict"` from the storage-layer flag)
|
|
246
264
|
* 12. else → `"completed"`
|
|
@@ -57,12 +57,18 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
57
57
|
const result = text;
|
|
58
58
|
let errorMessage;
|
|
59
59
|
let errorCode;
|
|
60
|
+
let apiFailure;
|
|
60
61
|
let blockedReason;
|
|
61
62
|
let salvagedOutput;
|
|
62
63
|
let checkpointToken;
|
|
63
64
|
let checkpointId;
|
|
64
65
|
let checkpointGate;
|
|
65
66
|
let workspaceRestoreMode;
|
|
67
|
+
const haltOwnsAbortedFinal = flags.userHalted === true &&
|
|
68
|
+
final?.stopReason === "aborted" &&
|
|
69
|
+
!flags.abortedLive &&
|
|
70
|
+
flags.abortedForTimeout !== true &&
|
|
71
|
+
flags.abortedForTurns !== true;
|
|
66
72
|
const isDegenerate = final?.stopReason === "error" && isDegenerateCutMessage(final);
|
|
67
73
|
if (flags.outputInvalid) {
|
|
68
74
|
status = "failed";
|
|
@@ -127,11 +133,14 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
127
133
|
checkpointGate = flags.reviewRef.gate;
|
|
128
134
|
workspaceRestoreMode = flags.reviewRef.restoreMode;
|
|
129
135
|
}
|
|
130
|
-
else if (flags.abortedLive || final?.stopReason === "aborted") {
|
|
136
|
+
else if ((flags.abortedLive || final?.stopReason === "aborted") && !haltOwnsAbortedFinal) {
|
|
131
137
|
status = "failed";
|
|
132
138
|
errorCode = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
|
|
133
139
|
errorMessage = final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted");
|
|
134
140
|
}
|
|
141
|
+
else if (flags.userHalted === true && (!final || haltOwnsAbortedFinal)) {
|
|
142
|
+
status = "completed";
|
|
143
|
+
}
|
|
135
144
|
else if (!final) {
|
|
136
145
|
status = "failed";
|
|
137
146
|
errorMessage = "no assistant message produced";
|
|
@@ -139,6 +148,12 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
139
148
|
else if (final.stopReason === "error") {
|
|
140
149
|
status = "failed";
|
|
141
150
|
errorMessage = final.errorMessage ?? "model error";
|
|
151
|
+
if (final.isApiErrorMessage === true) {
|
|
152
|
+
apiFailure = {
|
|
153
|
+
...(final.apiErrorStatus !== undefined ? { status: final.apiErrorStatus } : {}),
|
|
154
|
+
...(final.requestId !== undefined ? { requestId: final.requestId } : {}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
142
157
|
const lifted = extractErrorCode(errorMessage);
|
|
143
158
|
if (lifted) {
|
|
144
159
|
errorCode = lifted;
|
|
@@ -165,5 +180,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
165
180
|
void _internalCompaction;
|
|
166
181
|
if (flags.unpricedSpend)
|
|
167
182
|
delete publicStats.costMicroUsd;
|
|
168
|
-
return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
|
|
183
|
+
return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(flags.userHalted === true ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
|
|
169
184
|
}
|