@deksden-com/dd-flow-cli 0.9.0-beta.55 → 0.9.0-beta.62
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 +43 -0
- package/README.md +22 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/help.js +2 -0
- package/dist/cli/run-cli.js +13 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +17 -3
- package/dist/harness-runtime/lib/dd-zcode.mjs +57 -18
- package/dist/services/harness-adapter.js +2 -1
- package/dist/services/hooks.js +15 -7
- package/dist/services/lifecycle-invocations.js +84 -14
- package/dist/services/native-session-control.js +6 -4
- package/dist/services/recovery-observation-budget.js +53 -0
- package/dist/services/run-control-worker.js +55 -8
- package/dist/services/run-control.js +33 -9
- package/dist/services/run-controller-capture.js +20 -3
- package/dist/services/run-controller-process.js +12 -1
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +27 -6
- package/dist/services/run-recovery.js +15 -4
- package/dist/services/runs.js +46 -18
- package/dist/services/runtime-scope-control.js +20 -8
- package/dist/services/runtime-scope-worker.js +50 -5
- package/package.json +1 -1
- package/tools/repair-paused-run-status.mjs +59 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.62
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Verify a controlled native Session with a fresh inspection after cancellation before treating its tree as settled.
|
|
8
|
+
|
|
9
|
+
## 0.9.0-beta.61
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Keep the scope reconciliation contract test type-safe.
|
|
14
|
+
|
|
15
|
+
## 0.9.0-beta.60
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Document and verify explicit reconciliation of a drained runtime scope without automatic observer respawn.
|
|
20
|
+
|
|
21
|
+
## 0.9.0-beta.59
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Keep bundled ZCode close-contract fixtures aligned with retained native topology checks.
|
|
26
|
+
|
|
27
|
+
## 0.9.0-beta.58
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- Keep local task artifacts outside the release lint scope.
|
|
32
|
+
|
|
33
|
+
## 0.9.0-beta.57
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- Bound recovery observation, expose safe external reconciliation, and require
|
|
38
|
+
complete non-resident ZCode session-tree evidence before a stopped run settles.
|
|
39
|
+
|
|
40
|
+
## 0.9.0-beta.56
|
|
41
|
+
|
|
42
|
+
### Patch Changes
|
|
43
|
+
|
|
44
|
+
- Keep RUN database and runtime status aligned with pause, stop, controller failures and trusted recovery. Preserve real Stage pauses and terminal results, reject late revival under a recovery fence, roll back status changes after persistence failures, and provide an explicit backed-up legacy status repair tool.
|
|
45
|
+
|
|
3
46
|
## 0.9.0-beta.55
|
|
4
47
|
|
|
5
48
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -452,3 +452,25 @@ pnpm build
|
|
|
452
452
|
```
|
|
453
453
|
|
|
454
454
|
Manual smoke checks should use a temporary `DD_FLOW_HOME` when they do not intentionally inspect the operator's real local state.
|
|
455
|
+
|
|
456
|
+
### RUN status during recovery
|
|
457
|
+
|
|
458
|
+
A controller failure pauses the logical RUN without rewriting its Stage/Work
|
|
459
|
+
outcomes. Accepted pause/stop or recovery intent immediately persists `paused` in the RUN row,
|
|
460
|
+
`index_json`, and `run.json`. A trusted recovery acknowledgement restores the
|
|
461
|
+
operational status; an existing Stage/Work pause remains paused. Terminal RUNs
|
|
462
|
+
are never reopened. Physical tree settlement remains a separate control receipt.
|
|
463
|
+
|
|
464
|
+
Legacy stores with a fenced recovery may still contain `running`. After building,
|
|
465
|
+
inspect one exact RUN with:
|
|
466
|
+
|
|
467
|
+
```sh
|
|
468
|
+
node tools/repair-paused-run-status.mjs --home /path/to/dd-flow-home --project PRJ-001-project --run RUN-001-eval-subject
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Add `--apply` to repair it. This requires the latest recovery to be draining, sealed or resuming, saves the original row and runtime JSON
|
|
472
|
+
beside `run.json`, and changes no schema, engine binding or recovery capture.
|
|
473
|
+
It does not claim physical settlement or alter its receipts. `--cancel-abandoned`
|
|
474
|
+
explicitly cancels an abandoned fixture only when the project directory is absent
|
|
475
|
+
and it has no Work, Session, controller or recovery history. Historical experiment
|
|
476
|
+
engine snapshots remain pinned; rebuild/release the engine for future executions.
|
package/dist/build-info.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
3
|
+
"cli_version": "0.9.0-beta.62",
|
|
4
|
+
"cli_commit": "a496c8a3b9781cd1808bd4a49fe314f0ef17c52c",
|
|
5
|
+
"built_at": "2026-09-14T08:57:06.311Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.1",
|
|
8
8
|
"commit": "97f811d33c212ae3497020178b1ed825c7c3ebac",
|
|
9
9
|
"flow_contract": "dd-flow-canonical-2026-08",
|
|
10
|
-
"repo_root": "/
|
|
11
|
-
"memorybank_root": "/
|
|
12
|
-
"flow_root": "/
|
|
10
|
+
"repo_root": "/Users/deksden/Documents/_Projects/dd-memorybank",
|
|
11
|
+
"memorybank_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank",
|
|
12
|
+
"flow_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank/dd-flow",
|
|
13
13
|
"layout": "dot_memory_bank"
|
|
14
14
|
}
|
|
15
15
|
}
|
package/dist/cli/help.js
CHANGED
|
@@ -5,6 +5,7 @@ const helpPages = new Map([
|
|
|
5
5
|
dd-flow runtime scope stop --scope-id EVAL_ID --request-id ID --json
|
|
6
6
|
dd-flow runtime scope control --scope-id EVAL_ID --request-id ID --mode pause|stop [--manifest-json JSON] --json
|
|
7
7
|
dd-flow runtime scope resume --scope-id EVAL_ID --request-id ID --generation N --capture-key SHA256 --json
|
|
8
|
+
dd-flow runtime scope reconcile --scope-id EVAL_ID --generation N --request-id ID --json
|
|
8
9
|
|
|
9
10
|
Use the EVAL manifest's retained DD_FLOW_RESOURCE_HOME. Commands select exactly
|
|
10
11
|
one scope across its registered runtime homes; neighboring scopes are untouched.
|
|
@@ -188,6 +189,7 @@ Usage:
|
|
|
188
189
|
dd-flow id next --type protocol|run --project-root <root> --slug <slug> --json
|
|
189
190
|
dd-flow run status <RUN-ID|RUN-short-id> --project-root <root> --json
|
|
190
191
|
dd-flow run control status --run <RUN-ID> --project-root <root> --json
|
|
192
|
+
dd-flow run control reconcile --run <RUN-ID> --project-root <root> --control-id <ID> --request-id <ID> --json
|
|
191
193
|
dd-flow run drive launch --run <RUN-ID> --project-root <root> --request-id <id> --context-file <file> --context-sha256 <sha256> [--stop-after <stage>] [--capture-root <directory>] --json
|
|
192
194
|
dd-flow run drive status --run <RUN-ID> --project-root <root> [--after <sequence>] --json
|
|
193
195
|
dd-flow run list --project-root <root> --json
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -59,11 +59,11 @@ import { loadExternalStageContext } from "../services/stage-context.js";
|
|
|
59
59
|
import { stopManagedProcess, confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
|
|
60
60
|
import { fenceRuntimeBudget, runtimeBudgetStatus, admitRuntimeCheck, parseRuntimeBudget, reserveProviderTurn, releaseProviderTurns } from "../services/runtime-budget.js";
|
|
61
61
|
import { stopRuntimeScope } from "../services/runtime-scope-stop.js";
|
|
62
|
-
import { launchRuntimeScopeControl, launchRuntimeScopeResume, serveRuntimeScopeControl } from "../services/runtime-scope-worker.js";
|
|
62
|
+
import { launchRuntimeScopeControl, launchRuntimeScopeResume, reconcileRuntimeScopeControl, serveRuntimeScopeControl } from "../services/runtime-scope-worker.js";
|
|
63
63
|
import { launchRunController, resumeRunController, serveRunController, runControllerStatus, answerRunController, supplyRunControllerContext } from "../services/run-controller.js";
|
|
64
64
|
import { controllerHitlAdmission } from "../services/run-controller-adapter.js";
|
|
65
65
|
import { controlLifecycleReply, runControlStatus } from "../services/run-control.js";
|
|
66
|
-
import { requestRunControl, serveRunControl } from "../services/run-control-worker.js";
|
|
66
|
+
import { reconcileRunControl, requestRunControl, serveRunControl } from "../services/run-control-worker.js";
|
|
67
67
|
import { startRuntimeService } from "../services/runtime-service.js";
|
|
68
68
|
const defaultIo = {
|
|
69
69
|
stdout: process.stdout,
|
|
@@ -1142,6 +1142,11 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1142
1142
|
const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
|
|
1143
1143
|
if (action === "status")
|
|
1144
1144
|
return runControlStatus(context, scope);
|
|
1145
|
+
if (action === "reconcile") {
|
|
1146
|
+
if ([...parsed.options.keys()].some(key => !["run", "project-root", "control-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
1147
|
+
throw new AppError("usage", "Use run control reconcile --run ID --project-root PATH --control-id ID --request-id ID", 2);
|
|
1148
|
+
return await reconcileRunControl(context, { ...scope, controlId: requiredOption(parsed, "control-id"), requestId: requiredOption(parsed, "request-id") });
|
|
1149
|
+
}
|
|
1145
1150
|
if (action === "resume") {
|
|
1146
1151
|
if (["force", "grace-ms", "escalate"].some(option => hasOption(parsed, option)))
|
|
1147
1152
|
throw new AppError("usage", "Resume does not accept stop or pause escalation options", 2);
|
|
@@ -1157,7 +1162,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1157
1162
|
...(hasOption(parsed, "escalate") ? { escalate: requiredOption(parsed, "escalate") } : {}),
|
|
1158
1163
|
...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {})
|
|
1159
1164
|
});
|
|
1160
|
-
throw new AppError("usage", "Use: dd-flow run control pause|stop|resume|status", 2);
|
|
1165
|
+
throw new AppError("usage", "Use: dd-flow run control pause|stop|resume|reconcile|status", 2);
|
|
1161
1166
|
}
|
|
1162
1167
|
if (command === "drive") {
|
|
1163
1168
|
const action = requiredPosition(parsed, 0, "drive action");
|
|
@@ -1790,6 +1795,11 @@ function dispatchRun(context, command, parsed) {
|
|
|
1790
1795
|
}
|
|
1791
1796
|
async function dispatchRuntime(context, command, parsed, progress) {
|
|
1792
1797
|
if (command === "scope") {
|
|
1798
|
+
if (parsed.positional.length === 1 && parsed.positional[0] === "reconcile") {
|
|
1799
|
+
if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "generation"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
1800
|
+
throw new AppError("usage", "Use runtime scope reconcile --scope-id ID --generation N --request-id ID", 2);
|
|
1801
|
+
return { ok: true, ...await reconcileRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation")), requiredOption(parsed, "request-id")) };
|
|
1802
|
+
}
|
|
1793
1803
|
if (parsed.positional.length === 1 && parsed.positional[0] === "resume") {
|
|
1794
1804
|
if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "generation", "capture-key"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
1795
1805
|
throw new AppError("usage", "Use runtime scope resume --scope-id ID --request-id ID --generation N --capture-key SHA256", 2);
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
forkSessionWithBridge,
|
|
17
17
|
inspectSessionWithBridge,
|
|
18
18
|
promptSessionWithBridge,
|
|
19
|
+
zcodeChildSessionIds,
|
|
20
|
+
observeClosedZcodeTree,
|
|
19
21
|
} from "./dd-zcode.mjs";
|
|
20
22
|
|
|
21
23
|
const REQUEST_SCHEMA = "dd-zcode/daemon-request@1";
|
|
@@ -228,7 +230,7 @@ export class DaemonRuntime {
|
|
|
228
230
|
const known = this.sessions.get(params.sessionId);
|
|
229
231
|
const adapter = params.adapterSessionId ?? known?.adapter_session_id ?? params.sessionId;
|
|
230
232
|
const generation = this.activeGeneration ?? this.controlGeneration ?? 0;
|
|
231
|
-
return { ...this.state.config, ...params, adapterSessionId: adapter, daemonId: this.state.daemon_id, allowBackground: true, liveSession: this.liveAdapters.has(adapter), assertDispatch: () => { if (generation !== (this.controlGeneration ?? 0)) throw new DaemonError("operation_cancelled", "ZCode prompt was cancelled before native dispatch"); } };
|
|
233
|
+
return { ...this.state.config, ...params, knownChildSessionIds: known?.known_child_session_ids ?? [], adapterSessionId: adapter, daemonId: this.state.daemon_id, allowBackground: true, liveSession: this.liveAdapters.has(adapter), assertDispatch: () => { if (generation !== (this.controlGeneration ?? 0)) throw new DaemonError("operation_cancelled", "ZCode prompt was cancelled before native dispatch"); } };
|
|
232
234
|
}
|
|
233
235
|
|
|
234
236
|
track(result, topology = result?.evidence?.subagents) {
|
|
@@ -244,6 +246,7 @@ export class DaemonRuntime {
|
|
|
244
246
|
parent_provider_session_id: parent,
|
|
245
247
|
root_provider_session_id: existing?.root_provider_session_id ?? (parent ? parentRoot : result.provider_session_id),
|
|
246
248
|
topology: topology ?? existing?.topology ?? null,
|
|
249
|
+
known_child_session_ids: [...new Set([...(existing?.known_child_session_ids ?? zcodeChildSessionIds(existing?.topology)), ...zcodeChildSessionIds(topology)])],
|
|
247
250
|
completed_turns: existing?.completed_turns ?? 0,
|
|
248
251
|
close_receipt: result.close?.closed === true ? result : existing?.close_receipt ?? null,
|
|
249
252
|
}); }
|
|
@@ -321,7 +324,17 @@ export class DaemonRuntime {
|
|
|
321
324
|
const closed = this.sessions.get(params.sessionId)?.close_receipt;
|
|
322
325
|
if (closed) {
|
|
323
326
|
if (operation === "session.resume") throw new DaemonError("session_closed", "Closed Session requires explicit recovery");
|
|
324
|
-
|
|
327
|
+
const topology = closed.topology;
|
|
328
|
+
if (topology?.root_session_id !== params.sessionId || !Array.isArray(topology.child_session_ids) || topology.child_session_ids.some(id => typeof id !== "string" || !id)) return { ...closed, settled: false, observation_error: { code: "topology_incomplete", message: "closed Session has no native topology identity" } };
|
|
329
|
+
const previous = this.state.settlement_observations?.[params.sessionId];
|
|
330
|
+
const known = [...new Set([...topology.child_session_ids, ...(previous?.topology?.child_session_ids ?? [])])];
|
|
331
|
+
const observation = { ...closed, ...await observeClosedZcodeTree(this.bridge, params.sessionId, known), observed_at: new Date().toISOString() };
|
|
332
|
+
// Observations are not ownership inventory: their timestamps must not
|
|
333
|
+
// invalidate a drain fence on every read.
|
|
334
|
+
this.state.settlement_observations ??= {};
|
|
335
|
+
this.state.settlement_observations[params.sessionId] = observation;
|
|
336
|
+
await this.persist();
|
|
337
|
+
return observation;
|
|
325
338
|
}
|
|
326
339
|
const adapter = params.adapterSessionId ?? params.sessionId;
|
|
327
340
|
if (!this.liveAdapters.has(adapter) && this.activeProductive) {
|
|
@@ -367,7 +380,8 @@ export class DaemonRuntime {
|
|
|
367
380
|
const unsettled = [];
|
|
368
381
|
for (const session of this.sessions.values()) {
|
|
369
382
|
if (session.close_receipt) {
|
|
370
|
-
|
|
383
|
+
const observed = await this.dispatch("session.inspect", { sessionId: session.provider_session_id });
|
|
384
|
+
if (observed.settled !== true) unsettled.push(session.provider_session_id);
|
|
371
385
|
continue;
|
|
372
386
|
}
|
|
373
387
|
const options = this.options({ sessionId: session.provider_session_id, adapterSessionId: session.adapter_session_id });
|
|
@@ -199,11 +199,14 @@ export class AcpBridge {
|
|
|
199
199
|
if (message.method === "session/update" && this.options.onLifecycleNotification) {
|
|
200
200
|
const observer = this.options.onLifecycleNotification;
|
|
201
201
|
const pending = Promise.resolve().then(() => observer(message)).catch(async error => {
|
|
202
|
-
|
|
202
|
+
// A validated no-effect rejection is returned through the invocation
|
|
203
|
+
// ledger. It is evidence, not a poisoned ACP Session.
|
|
204
|
+
const fatal = fatalLifecycleError(error);
|
|
205
|
+
if (!this.notificationError || fatal) this.notificationError = error;
|
|
203
206
|
// A missing/foreign authority is not a recoverable CLI validation
|
|
204
207
|
// result. Stop admitting productive requests immediately; the managed
|
|
205
208
|
// controller still owns physical stop/drain and evidence capture.
|
|
206
|
-
if (
|
|
209
|
+
if (fatal) this.rejectAll(error);
|
|
207
210
|
try {
|
|
208
211
|
await this.journal.write("notification_failure", { code: error.code ?? "invocation_observation_failed", message: error.message,
|
|
209
212
|
session_id: message.params?.sessionId ?? null, tool_call_id: update?.toolCallId ?? null, event_time: now(), delivery_stage: "lifecycle_receipt", completeness: "incomplete" });
|
|
@@ -429,7 +432,50 @@ export function latestAssistantText(read) {
|
|
|
429
432
|
return null;
|
|
430
433
|
}
|
|
431
434
|
|
|
432
|
-
|
|
435
|
+
export function zcodeChildSessionIds(snapshot) {
|
|
436
|
+
return [...new Set([
|
|
437
|
+
...(Array.isArray(snapshot?.childSessionIds) ? snapshot.childSessionIds : []),
|
|
438
|
+
...(Array.isArray(snapshot?.running) ? snapshot.running : []).map(child => child?.childSessionId),
|
|
439
|
+
...(Array.isArray(snapshot?.ended?.items) ? snapshot.ended.items : []).map(child => child?.childSessionId),
|
|
440
|
+
].filter(id => typeof id === "string" && id))];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Read residency BEFORE each durable topology snapshot. An inactive node cannot
|
|
444
|
+
// add descendants; a resident node never proves settlement, even when idle.
|
|
445
|
+
// No resolve/read/subagents extension here: those may implicitly resume.
|
|
446
|
+
export async function observeClosedZcodeTree(bridge, root, retainedChildren = []) {
|
|
447
|
+
const pending = new Set([root, ...retainedChildren]), residents = [], snapshots = [];
|
|
448
|
+
const deadline = performance.now() + 30_000;
|
|
449
|
+
try {
|
|
450
|
+
for (const sessionId of pending) {
|
|
451
|
+
if (pending.size > 1024 || performance.now() >= deadline) throw new Error("Closed tree observation limit exceeded");
|
|
452
|
+
const timeout = () => Math.max(1, Math.min(5000, deadline - performance.now()));
|
|
453
|
+
const resident = await bridge.request("zcode/session/resident", { sessionId }, timeout());
|
|
454
|
+
if (resident?.sessionId !== sessionId || typeof resident.resident !== "boolean") throw new Error("Invalid residency identity or state");
|
|
455
|
+
residents.push(resident);
|
|
456
|
+
const retained = await bridge.request("zcode/session/retainedSubagents", { sessionId }, timeout());
|
|
457
|
+
const tree = retained?.topology;
|
|
458
|
+
if (retained?.sessionId !== sessionId || !Array.isArray(tree?.childSessionIds)
|
|
459
|
+
|| tree.childSessionIds.some(id => typeof id !== "string" || !id || id === sessionId)
|
|
460
|
+
|| !Array.isArray(tree?.running) || !Array.isArray(tree?.ended?.items)
|
|
461
|
+
|| !Number.isSafeInteger(tree.ended.total) || tree.ended.total < 0
|
|
462
|
+
|| tree.ended.total > tree.childSessionIds.length
|
|
463
|
+
|| [...tree.running, ...tree.ended.items].some(child => !tree.childSessionIds.includes(child?.childSessionId))) throw new Error("Incomplete retained native topology");
|
|
464
|
+
snapshots.push({ sessionId, ...tree });
|
|
465
|
+
for (const child of tree.childSessionIds) pending.add(child);
|
|
466
|
+
}
|
|
467
|
+
return { topology: { root_session_id: root, child_session_ids: [...pending].filter(id => id !== root), complete: true }, residents, snapshots, settled: residents.every(item => item.resident === false) };
|
|
468
|
+
} catch (error) {
|
|
469
|
+
return { topology: { root_session_id: root, child_session_ids: [...pending].filter(id => id !== root), complete: false }, residents, snapshots, settled: false, observation_error: { code: error.code ?? "topology_observation_failed", message: error.message } };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function cancelTree(bridge, sessionId, before, providerSessionId, retainedChildren = []) {
|
|
474
|
+
const knownChildren = new Set(retainedChildren);
|
|
475
|
+
const remember = snapshot => {
|
|
476
|
+
for (const id of zcodeChildSessionIds(snapshot)) knownChildren.add(id);
|
|
477
|
+
};
|
|
478
|
+
remember(before);
|
|
433
479
|
const cancellations = [];
|
|
434
480
|
for (const child of before.running ?? []) {
|
|
435
481
|
const taskId = child.taskId ?? child.agentId;
|
|
@@ -452,27 +498,20 @@ async function cancelTree(bridge, sessionId, before, providerSessionId) {
|
|
|
452
498
|
bridge.request("zcode/session/read", { sessionId }, timeout).catch(error => ({ observation_error: { code: error.code, message: error.message } })),
|
|
453
499
|
]);
|
|
454
500
|
root_status = read?.projection?.status ?? read?.session?.status ?? null;
|
|
501
|
+
remember(after);
|
|
455
502
|
if (after.observation_error || read?.observation_error) break;
|
|
456
503
|
if (Array.isArray(after.running) && !after.running.length && !(read?.projection?.activeToolCalls ?? []).length && ["idle", "completed", "cancelled", "failed", "stopped"].includes(root_status)) {
|
|
457
|
-
|
|
504
|
+
// Idle is only a turn-level observation. Close the owned resident before
|
|
505
|
+
// proving the transitive tree cannot create another child.
|
|
506
|
+
break;
|
|
458
507
|
}
|
|
459
508
|
}
|
|
460
509
|
let close = null;
|
|
461
510
|
try { close = await bridge.request("zcode/session/close", { sessionId }); }
|
|
462
511
|
catch (error) { close = { closed: false, error: { code: error.code ?? "close_failed", message: error.message } }; }
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const observed = await bridge.request("zcode/session/resident", { sessionId: id }, 5000);
|
|
467
|
-
if (observed.sessionId !== id) throw Object.assign(new Error("Residency evidence belongs to another Session"), { code: "session_identity_mismatch" });
|
|
468
|
-
return observed;
|
|
469
|
-
}
|
|
470
|
-
catch (error) { return { sessionId: id, observation_error: { code: error.code, message: error.message } }; }
|
|
471
|
-
})) : [];
|
|
472
|
-
const completeTopology = !before.observation_error && Array.isArray(before.running) && !after.observation_error && Array.isArray(after.running)
|
|
473
|
-
&& [...(before.running ?? []), ...after.running].every(child => typeof child.childSessionId === "string");
|
|
474
|
-
return { cancellations, before, after, root_status, cancellation_requested: true, close_requested: true, close, residents,
|
|
475
|
-
settled: close?.closed === true && completeTopology && residents.length > 0 && residents.every(item => item.resident === false) };
|
|
512
|
+
const observation = close?.closed === true ? await observeClosedZcodeTree(bridge, providerSessionId, [...knownChildren])
|
|
513
|
+
: { topology: { root_session_id: providerSessionId, child_session_ids: [...knownChildren], complete: false }, settled: false };
|
|
514
|
+
return { cancellations, before, after, root_status, cancellation_requested: true, close_requested: true, close, ...observation };
|
|
476
515
|
}
|
|
477
516
|
|
|
478
517
|
// Cancel one known child without cancelling its parent turn. The parent keeps
|
|
@@ -749,7 +788,7 @@ export async function cancelSessionWithBridge(bridge, options) {
|
|
|
749
788
|
requireJournal(options);
|
|
750
789
|
const identity = await controlledIdentity(bridge, options);
|
|
751
790
|
const before = await bridge.request("zcode/session/subagents", { sessionId: identity.adapterSessionId }).catch(error => ({ observation_error: { code: error.code, message: error.message } }));
|
|
752
|
-
return receipt(options, { harness: "zcode-acp", provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...await cancelTree(bridge, identity.adapterSessionId, before, identity.providerSessionId) });
|
|
791
|
+
return receipt(options, { harness: "zcode-acp", provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...await cancelTree(bridge, identity.adapterSessionId, before, identity.providerSessionId, options.knownChildSessionIds) });
|
|
753
792
|
}
|
|
754
793
|
|
|
755
794
|
export async function forkSession(options) {
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import { recoveryRpcTimeout } from "./recovery-observation-budget.js";
|
|
6
7
|
const maxAdapterOutputBytes = 1_000_000;
|
|
7
8
|
/** Native hooks need a real executable, not a PATH-dependent shell expression. */
|
|
8
9
|
export function prepareHarnessFlowExecutable(context, stateDir) {
|
|
@@ -37,7 +38,7 @@ export function prepareHarnessFlowExecutable(context, stateDir) {
|
|
|
37
38
|
}
|
|
38
39
|
/** Runs one harness command with the lifecycle guarantees shared by agent services. */
|
|
39
40
|
export function runHarnessAdapter(input) {
|
|
40
|
-
const timeoutMs = input.timeoutMs ?? 120_000;
|
|
41
|
+
const timeoutMs = recoveryRpcTimeout(input.timeoutMs ?? 120_000);
|
|
41
42
|
if (!input.operationId.trim())
|
|
42
43
|
throw new AppError("operation_id_required", "Harness calls require a scoped operation id", 2);
|
|
43
44
|
// The current adapter transport uses this name; its identity is runtime-owned,
|
package/dist/services/hooks.js
CHANGED
|
@@ -9,7 +9,7 @@ import { ensureDir, resolveProjectRoot } from "../storage/paths.js";
|
|
|
9
9
|
import { appendAudit } from "./audit.js";
|
|
10
10
|
import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
|
|
11
11
|
import { assertRecoveryHookCurrent } from "./run-recovery.js";
|
|
12
|
-
import { observeLifecycleInvocation } from "./lifecycle-invocations.js";
|
|
12
|
+
import { observeLifecycleInvocation, settleLifecycleNoEffectRejection } from "./lifecycle-invocations.js";
|
|
13
13
|
import { commandHasOption, commandOption, commandPosition, shellSuffixOffset, parseLifecycleCommand, unwrapShellCommand } from "./lifecycle-command.js";
|
|
14
14
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
15
15
|
import { activeFlowSessionsForProject, bindObservedFlowSession, flowSessionPayloadFromRegisterCommand, recordFlowSessionObservation } from "./sessions.js";
|
|
@@ -433,9 +433,9 @@ export function handleZcodeEvent(context, input) {
|
|
|
433
433
|
}) }, commandOption(parsed.invocation, "invocation-id"));
|
|
434
434
|
}
|
|
435
435
|
/**
|
|
436
|
-
* Accept the synchronous native ZCode PreToolUse callback.
|
|
437
|
-
*
|
|
438
|
-
*
|
|
436
|
+
* Accept the synchronous native ZCode PreToolUse callback. Invocation-ID
|
|
437
|
+
* commands instead rendezvous with their ACP event while the CLI awaits
|
|
438
|
+
* admission; the native hook must not create a competing receipt for them.
|
|
439
439
|
*/
|
|
440
440
|
export function handleZcodeHook(context, input) {
|
|
441
441
|
return recordZcodeLifecycle(context, input);
|
|
@@ -455,9 +455,6 @@ function recordZcodeLifecycle(context, input, invocationId) {
|
|
|
455
455
|
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
456
456
|
if (!invocationId && commandOption(lifecycle.invocation, "invocation-id"))
|
|
457
457
|
return { ok: true, observed: false, reason: "cli_invocation_owns_admission" };
|
|
458
|
-
if (lifecycle.analysis.kind === "compound") {
|
|
459
|
-
throw new AppError("compound_lifecycle_command", "dd-flow lifecycle commands must be a standalone ZCode Bash tool call", 1, { standalone_command: lifecycle.invocation.command });
|
|
460
|
-
}
|
|
461
458
|
const expectedRoot = resolveProjectRoot(input.projectRoot);
|
|
462
459
|
if (lifecycle.commandProjectRoot && resolveProjectRoot(lifecycle.commandProjectRoot) !== expectedRoot) {
|
|
463
460
|
throw new AppError("project_mismatch", "ZCode lifecycle command project root does not match the controlled workspace", 1, { expected: expectedRoot, actual: lifecycle.commandProjectRoot });
|
|
@@ -477,6 +474,17 @@ function recordZcodeLifecycle(context, input, invocationId) {
|
|
|
477
474
|
const toolUseId = stringValue(hook.tool_use_id) ?? stringValue(hook.toolUseId) ?? stringValue(hook.event_id) ?? stringValue(hook.eventId);
|
|
478
475
|
if (!toolUseId)
|
|
479
476
|
throw new AppError("zcode_hook_event_missing", "ZCode native hook has no tool-use ID", 1);
|
|
477
|
+
if (lifecycle.analysis.kind === "compound") {
|
|
478
|
+
if (lifecycle.analysis.reason !== "shell_composition")
|
|
479
|
+
throw new AppError("compound_lifecycle_command", "Ambiguous shell cannot authorize a lifecycle replacement", 1, { reason: lifecycle.analysis.reason });
|
|
480
|
+
if (invocationId) {
|
|
481
|
+
const error = { code: "compound_lifecycle_command", message: "dd-flow lifecycle commands must be a standalone ZCode Bash tool call", exitCode: 1,
|
|
482
|
+
details: { standalone_command: lifecycle.invocation.command } };
|
|
483
|
+
settleLifecycleNoEffectRejection(context, invocationId, lifecycle.invocation.command, error, { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId });
|
|
484
|
+
return { ok: true, observed: false, invocation_id: invocationId, no_effect: true, error };
|
|
485
|
+
}
|
|
486
|
+
throw new AppError("compound_lifecycle_command", "dd-flow lifecycle commands must be a standalone ZCode Bash tool call", 1, { standalone_command: lifecycle.invocation.command });
|
|
487
|
+
}
|
|
480
488
|
const eventKey = crypto.createHash("sha256").update(JSON.stringify(["zcode-acp", daemonId, providerSessionId, toolUseId])).digest("hex");
|
|
481
489
|
const rewritten = invocationId ? command : commandWithHookEvent(command, eventKey);
|
|
482
490
|
if (!invocationId && rewritten === command && commandOption(lifecycle.invocation, "hook-event-id") !== eventKey)
|
|
@@ -69,7 +69,7 @@ export function lifecycleRetryCommands(context, stage) {
|
|
|
69
69
|
return [];
|
|
70
70
|
const successor = load(context, commandOption(invocation(retry), "invocation-id") ?? "");
|
|
71
71
|
const latest = context.db.get("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? ORDER BY rowid DESC LIMIT 1", [row.scope_json, row.fingerprint]);
|
|
72
|
-
return successor.id === latest?.id && successor.status === "issued" ? [retry] : [];
|
|
72
|
+
return successor.id === latest?.id && successor.status === "issued" && (successor.deadline === null || successor.deadline > Date.now()) ? [retry] : [];
|
|
73
73
|
}
|
|
74
74
|
catch {
|
|
75
75
|
return [];
|
|
@@ -109,6 +109,7 @@ export function managedLifecycleCommand(context, command) {
|
|
|
109
109
|
throw new AppError("invocation_scope_mismatch", "Lifecycle attempt belongs to another managed scope", 1, { invocation_id: suppliedId });
|
|
110
110
|
}
|
|
111
111
|
checkCommand(retained, command);
|
|
112
|
+
assertRenderableInvocation(retained);
|
|
112
113
|
return command;
|
|
113
114
|
}
|
|
114
115
|
if (!context.db.writable || context.env.DD_FLOW_INVOCATION_READONLY === "1") {
|
|
@@ -143,21 +144,67 @@ export function retryLifecycleInvocationCommand(context, id) {
|
|
|
143
144
|
const prior = load(context, id);
|
|
144
145
|
if (prior.status !== "executing")
|
|
145
146
|
throw new AppError("invocation_retry_not_authorized", "Only the executing lifecycle operation can publish its next attempt", 1);
|
|
147
|
+
return successorLifecycleInvocationCommand(context, prior);
|
|
148
|
+
}
|
|
149
|
+
/** A predecessor may publish exactly one successor only while the caller has
|
|
150
|
+
* proved that no lifecycle effect was admitted. This is shared by ordinary
|
|
151
|
+
* command validation and the native pre-execution rejection path. */
|
|
152
|
+
function successorLifecycleInvocationCommand(context, prior) {
|
|
146
153
|
const parsed = invocation(prior.command);
|
|
147
154
|
const argv = [...parsed.argv];
|
|
148
155
|
const at = argv.indexOf("--invocation-id");
|
|
149
|
-
if (at < 0 || argv[at + 1] !== id)
|
|
156
|
+
if (at < 0 || argv[at + 1] !== prior.id)
|
|
150
157
|
throw new AppError("invocation_command_invalid", "Retained issued command lost its invocation marker", 1);
|
|
151
158
|
// The parser proves the marker belongs to argv. Remove that one rendered
|
|
152
159
|
// token from the retained text so response-file/stdin/heredoc presentation
|
|
153
160
|
// remains byte-for-byte stable across a retry.
|
|
154
|
-
const marker = ` --invocation-id ${id}`;
|
|
161
|
+
const marker = ` --invocation-id ${prior.id}`;
|
|
155
162
|
const markerAt = prior.command.indexOf(marker);
|
|
156
163
|
if (markerAt < 0 || prior.command.indexOf(marker, markerAt + marker.length) >= 0)
|
|
157
164
|
throw new AppError("invocation_command_invalid", "Retained issued command has an ambiguous invocation marker", 1);
|
|
158
165
|
const command = `${prior.command.slice(0, markerAt)}${prior.command.slice(markerAt + marker.length)}`;
|
|
159
|
-
return managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: prior.scope_json, DD_FLOW_CURRENT_INVOCATION: id } }, command);
|
|
166
|
+
return managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: prior.scope_json, DD_FLOW_CURRENT_INVOCATION: prior.id } }, command);
|
|
167
|
+
}
|
|
168
|
+
/** A native observer can reject a malformed tool call before the CLI claims
|
|
169
|
+
* execution. Persist that fact and its sole replacement atomically: waiting
|
|
170
|
+
* callers must never time out merely because the observer rejected syntax. */
|
|
171
|
+
export function settleLifecycleNoEffectRejection(context, id, command, error, identity) {
|
|
172
|
+
storage(context);
|
|
173
|
+
context.db.exec("BEGIN IMMEDIATE");
|
|
174
|
+
try {
|
|
175
|
+
const prior = load(context, id);
|
|
176
|
+
checkCommand(prior, command);
|
|
177
|
+
const scope = JSON.parse(prior.scope_json);
|
|
178
|
+
checkNativeIdentity(scope, identity, id);
|
|
179
|
+
if (prior.status === "settled") {
|
|
180
|
+
context.db.exec("COMMIT");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (scope.runId)
|
|
184
|
+
assertLifecycleInvocationCurrent(context, scope, command);
|
|
185
|
+
if (prior.status !== "issued")
|
|
186
|
+
throw new AppError("invocation_rejection_conflict", "A lifecycle rejection arrived after execution admission", 1, { invocation_id: id });
|
|
187
|
+
settleUnadmittedInvocation(context, prior, error);
|
|
188
|
+
context.db.exec("COMMIT");
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
context.db.exec("ROLLBACK");
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
160
194
|
}
|
|
195
|
+
// Caller holds the write transaction and has checked authority. Issued (or
|
|
196
|
+
// legacy expired) attempts have never admitted an effect; observed/executing
|
|
197
|
+
// attempts must never receive an automatic replacement.
|
|
198
|
+
function settleUnadmittedInvocation(context, prior, error) {
|
|
199
|
+
if (!["issued", "expired"].includes(prior.status))
|
|
200
|
+
throw new AppError("invocation_rejection_conflict", "Attempt already admitted native execution", 1);
|
|
201
|
+
const details = { ...error.details, retry_command: successorLifecycleInvocationCommand(context, prior),
|
|
202
|
+
retry_instruction: "Use retry_command as one standalone lifecycle command. The rejected command did not change lifecycle state." };
|
|
203
|
+
if (context.db.run("UPDATE lifecycle_invocations SET status = 'settled', outcome_json = ?, updated_at = ? WHERE id = ? AND status = ?", [JSON.stringify({ error: { ...error, details } }), context.now(), prior.id, prior.status]).changes !== 1) {
|
|
204
|
+
throw new AppError("invocation_settlement_conflict", "Lifecycle rejection could not be committed", 1, { invocation_id: prior.id });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const receiptTimeout = (id) => ({ code: "invocation_receipt_timeout", message: "No native confirmation before the receipt deadline; lifecycle state was not changed", exitCode: 1, details: { invocation_id: id } });
|
|
161
208
|
/** Settle a conclusive rejection and publish its only legal successor atomically.
|
|
162
209
|
* A crash must leave either the old attempt executing, or both the settled
|
|
163
210
|
* outcome and its successor; never a usable successor beside an unknown effect. */
|
|
@@ -291,7 +338,13 @@ function commandWithInvocationId(command, id) {
|
|
|
291
338
|
/** The attempt owns authority; the caller owns presentation-only shell input.
|
|
292
339
|
* Preserve current stdin/heredoc/response-file text while validating that it
|
|
293
340
|
* still names the exact persisted operation and project. */
|
|
341
|
+
function assertRenderableInvocation(row) {
|
|
342
|
+
if (row.status === "expired" || row.status === "issued" && row.deadline !== null && row.deadline <= Date.now()) {
|
|
343
|
+
throw new AppError("invocation_expired", "Retained lifecycle attempt has expired; inspection cannot grant a new attempt", 1, { invocation_id: row.id, status: row.status, outcome: row.outcome_json ? JSON.parse(row.outcome_json) : null });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
294
346
|
function renderInvocationCommand(row, command) {
|
|
347
|
+
assertRenderableInvocation(row);
|
|
295
348
|
const parsed = invocation(command);
|
|
296
349
|
if (commandOption(parsed, "invocation-id"))
|
|
297
350
|
throw new AppError("invocation_command_invalid", "Rendered lifecycle command already has an invocation id", 1);
|
|
@@ -301,6 +354,13 @@ function renderInvocationCommand(row, command) {
|
|
|
301
354
|
}
|
|
302
355
|
/** Native observer and receipt writer commit together. Never await I/O inside
|
|
303
356
|
* this transaction; the callback must write to this same SQLite connection. */
|
|
357
|
+
function checkNativeIdentity(scope, identity, id) {
|
|
358
|
+
if (identity.daemonId !== scope.daemonId || identity.rootSessionId !== scope.rootSessionId
|
|
359
|
+
|| !identity.sessionId || !identity.toolCallId
|
|
360
|
+
|| (identity.sessionId === scope.rootSessionId ? identity.parentSessionId !== null : !identity.parentSessionId)) {
|
|
361
|
+
throw new AppError("invocation_identity_mismatch", "Native identity does not belong to the issued managed scope", 1, { invocation_id: id });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
304
364
|
export function observeLifecycleInvocation(context, input) {
|
|
305
365
|
const initial = load(context, input.id);
|
|
306
366
|
checkCommand(initial, input.command);
|
|
@@ -311,11 +371,7 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
311
371
|
return { eventKey: initial.event_key, duplicate: true };
|
|
312
372
|
const scope = JSON.parse(initial.scope_json);
|
|
313
373
|
const identity = input.identity;
|
|
314
|
-
|
|
315
|
-
|| !identity.sessionId || !identity.toolCallId
|
|
316
|
-
|| (identity.sessionId === scope.rootSessionId ? identity.parentSessionId !== null : !identity.parentSessionId)) {
|
|
317
|
-
throw new AppError("invocation_identity_mismatch", "Native identity does not belong to the issued managed scope", 1, { invocation_id: input.id });
|
|
318
|
-
}
|
|
374
|
+
checkNativeIdentity(scope, identity, input.id);
|
|
319
375
|
const identityJson = JSON.stringify([identity.daemonId, identity.rootSessionId, identity.sessionId, identity.parentSessionId, identity.toolCallId]);
|
|
320
376
|
context.db.exec("BEGIN IMMEDIATE");
|
|
321
377
|
try {
|
|
@@ -325,7 +381,11 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
325
381
|
return { eventKey: row.event_key, duplicate: true };
|
|
326
382
|
}
|
|
327
383
|
if (row.status === "expired" || (row.deadline !== null && row.deadline <= Date.now() && !row.event_key)) {
|
|
328
|
-
|
|
384
|
+
if (scope.runId)
|
|
385
|
+
assertLifecycleInvocationCurrent(context, scope, input.command);
|
|
386
|
+
settleUnadmittedInvocation(context, row, receiptTimeout(input.id));
|
|
387
|
+
context.db.exec("COMMIT");
|
|
388
|
+
return { eventKey: row.event_key, duplicate: true };
|
|
329
389
|
}
|
|
330
390
|
if (row.identity_json) {
|
|
331
391
|
if (row.identity_json !== identityJson)
|
|
@@ -370,10 +430,20 @@ export async function awaitLifecycleInvocation(context, input) {
|
|
|
370
430
|
continue;
|
|
371
431
|
}
|
|
372
432
|
if (row.status === "expired" || (row.deadline !== null && row.deadline <= Date.now())) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
433
|
+
context.db.exec("BEGIN IMMEDIATE");
|
|
434
|
+
try {
|
|
435
|
+
const current = load(context, input.id);
|
|
436
|
+
if (["issued", "expired"].includes(current.status)) {
|
|
437
|
+
input.assertCurrent(JSON.parse(current.scope_json));
|
|
438
|
+
settleUnadmittedInvocation(context, current, receiptTimeout(input.id));
|
|
439
|
+
}
|
|
440
|
+
context.db.exec("COMMIT");
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
context.db.exec("ROLLBACK");
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
continue;
|
|
377
447
|
}
|
|
378
448
|
await delay(Math.min(25, Math.max(1, (row.deadline ?? Date.now() + 25) - Date.now())));
|
|
379
449
|
}
|
|
@@ -8,15 +8,17 @@ export async function cancelControlledNativeSession(input) {
|
|
|
8
8
|
if (input.observe && (reply.operation_id !== input.operationId || reply.operation !== "session.cancel" || reply.session_id !== input.sessionId))
|
|
9
9
|
throw new AppError("controller_operation_mismatch", "Cancellation observation belongs to another operation or Session", 1);
|
|
10
10
|
const result = input.observe ? reply.state === "completed" ? reply.result : null : reply;
|
|
11
|
+
// A completed cancel command is an immutable ACK, not proof that the
|
|
12
|
+
// provider tree has stopped. Callers must use a separate fresh inspection.
|
|
11
13
|
if (!input.observe || reply.state === "completed")
|
|
12
|
-
assertNativeCancellationReceipt(result, input.sessionId
|
|
14
|
+
assertNativeCancellationReceipt(result, input.sessionId);
|
|
13
15
|
input.assertCurrent();
|
|
14
16
|
return { status: result ? "completed" : "outcome_unknown", receipt: (result ?? reply) };
|
|
15
17
|
}
|
|
16
|
-
export function assertNativeCancellationReceipt(value, sessionId
|
|
18
|
+
export function assertNativeCancellationReceipt(value, sessionId) {
|
|
17
19
|
const identity = adapterSessionId(value);
|
|
18
|
-
if (!value || typeof value !== "object" || Array.isArray(value) || identity && identity !== sessionId
|
|
19
|
-
throw new AppError("controller_operation_mismatch",
|
|
20
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || identity && identity !== sessionId)
|
|
21
|
+
throw new AppError("controller_operation_mismatch", "Cancellation receipt has no valid result for its target Session", 1);
|
|
20
22
|
}
|
|
21
23
|
/** Native observation is shared by RUN and non-Work EVAL owners. */
|
|
22
24
|
export async function inspectControlledNativeSession(input) {
|