@deksden-com/dd-flow-cli 0.9.0-beta.151 → 0.9.0-beta.155
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 +23 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +273 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +170 -25
- package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +77 -10
- package/dist/services/check-runner-proof.js +69 -1
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +56 -23
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +214 -24
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -4
- package/dist/services/retained-native-hook.js +245 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +12 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +209 -43
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +9 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +252 -66
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +4 -3
- package/tools/verification-jobs.mjs +342 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { managedFlowEnv } from './managed-flow-env.mjs';
|
|
2
2
|
import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
|
|
3
3
|
import { callDaemonObserved } from './daemon-observation.mjs';
|
|
4
|
+
import { operationProgressReporter } from './operation-progress.mjs';
|
|
4
5
|
import { admitNativeRequestBuffer } from "./native-streams.mjs";
|
|
5
6
|
import { prepareDaemonRequest } from "./native-request-proof.mjs";
|
|
6
7
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
@@ -13,15 +14,17 @@ import net from "node:net";
|
|
|
13
14
|
import { durableDaemonDispatch, completeDaemonShutdown, commitDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
|
|
14
15
|
import { waitForSettlement } from "./session-settlement.mjs";
|
|
15
16
|
import { nativeExecutionReceipt } from "./native-children.mjs";
|
|
16
|
-
import { errorRecord, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
|
|
17
|
-
import { checkObservedProfile, observeModel } from "./model-observations.mjs";
|
|
17
|
+
import { diagnosticJson, errorRecord, isNativeHookObservationLoss, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
|
|
18
|
+
import { checkObservedProfile, observeModel, observedFields, readModelObservations } from "./model-observations.mjs";
|
|
18
19
|
import os from "node:os";
|
|
19
20
|
import path from "node:path";
|
|
20
21
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
21
22
|
import { writeJsonAtomic } from "./runner-events.mjs";
|
|
22
23
|
import { OpenCodeClient, OpenCodeError, historyDigest, usageSnapshot, nativeModel } from "./dd-opencode.mjs";
|
|
23
24
|
import { safeProviderError } from "./provider-overload-policy.mjs";
|
|
24
|
-
import { acceptNativeHookRequest, hookBudget, readNativeHookFailures } from "./native-hook-command.mjs";
|
|
25
|
+
import { acceptNativeHookRequest, hookBudget, readNativeHookFailures, writeNativeHookFailure, retainNativeWorkStart } from "./native-hook-command.mjs";
|
|
26
|
+
import { isDeepStrictEqual } from "node:util";
|
|
27
|
+
import { parseLifecycleCommand } from "./lifecycle-command.mjs";
|
|
25
28
|
import { assertStartupChild, retainStartupFailure, prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
26
29
|
|
|
27
30
|
const REQUEST_SCHEMA = "dd-opencode/daemon-request@1", RESPONSE_SCHEMA = "dd-opencode/daemon-response@1", STATE_SCHEMA = "dd-opencode/daemon-state@2", ARCHIVE_SCHEMA = "dd-opencode/session-archive@2";
|
|
@@ -68,7 +71,7 @@ const errorPayload = errorRecord;
|
|
|
68
71
|
|
|
69
72
|
async function writePlugin(paths, config) {
|
|
70
73
|
const directory = path.join(paths.config, "opencode", "plugins"); await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
71
|
-
const source = `import { makeOpenCodePlugin } from ${JSON.stringify(new URL("./opencode-plugin.mjs", import.meta.url).href)};\nexport default makeOpenCodePlugin(${JSON.stringify({ stateDir: paths.dir, daemonId: config.daemonId, version: config.version, ddFlowBin: config.ddFlowBin, nodeBin: process.execPath, ddFlowHome: config.ddFlowHome, projectRoot: config.projectRoot, resourceHome: config.resourceHome })});\n`;
|
|
74
|
+
const source = `import { makeOpenCodePlugin } from ${JSON.stringify(new URL("./opencode-plugin.mjs", import.meta.url).href)};\nexport default makeOpenCodePlugin(${JSON.stringify({ stateDir: paths.dir, daemonId: config.daemonId, version: config.version, ddFlowBin: config.ddFlowBin, nodeBin: process.execPath, ddFlowHome: config.ddFlowHome, projectRoot: config.projectRoot, resourceHome: config.resourceHome, nativeWorkStartAdmission: true })});\n`;
|
|
72
75
|
const checksum = createHash("sha256").update(source).digest("hex"); await writeFile(path.join(directory, "dd-flow.js"), source, { mode: 0o600 }); return checksum;
|
|
73
76
|
}
|
|
74
77
|
async function prepare(paths, config, authSource) {
|
|
@@ -188,7 +191,9 @@ export class Runtime {
|
|
|
188
191
|
throw new DaemonError("session_identity_mismatch", "OpenCode retained Session identity is invalid or duplicated");
|
|
189
192
|
this.sessions.set(record.provider_session_id, record);
|
|
190
193
|
}
|
|
191
|
-
|
|
194
|
+
// ponytail: retained participating hooks live until daemon retirement;
|
|
195
|
+
// add durable per-tool retirement only if long-lived Sessions need it.
|
|
196
|
+
this.active = null; this.persisting = Promise.resolve(); this.retainedHookRequests = new Map(); }
|
|
192
197
|
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.catch(() => {}).then(() => writeJson(this.paths.state, snapshot)); return this.persisting; }
|
|
193
198
|
retainSession(info, root) {
|
|
194
199
|
const id = info.id, parent = info.fork ? null : info.parentID ?? null, retained = this.sessions.get(id);
|
|
@@ -202,13 +207,23 @@ export class Runtime {
|
|
|
202
207
|
const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent, root_provider_session_id: root, cwd: info.directory };
|
|
203
208
|
this.sessions.set(id, record); return record;
|
|
204
209
|
}
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
registerObservedChild(info, root) {
|
|
211
|
+
const owner = this.sessions.get(root);
|
|
212
|
+
if (!owner || owner.parent_provider_session_id || owner.root_provider_session_id !== root
|
|
213
|
+
|| info.id === root || info.parentID !== root || info.fork)
|
|
214
|
+
throw new DaemonError("session_identity_mismatch", "OpenCode child is not a direct member of the owned root");
|
|
215
|
+
if (typeof info.directory !== "string" || !path.isAbsolute(info.directory))
|
|
216
|
+
throw new OpenCodeError("opencode_unavailable", "OpenCode child directory binding is not yet observable");
|
|
217
|
+
return this.retainSession(info, root);
|
|
218
|
+
}
|
|
219
|
+
async journal(operation, result) { const line = `${JSON.stringify({ schema_id: "dd-opencode/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), operation, active_tree: this.state.active_tree, session_id: result?.provider_session_id ?? null, ...(result?.native_hook_diagnostic ? { native_hook_diagnostic: JSON.parse(diagnosticJson(result.native_hook_diagnostic)) } : {}) })}\n`; for (const journal of new Set([this.paths.journal, this.state.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
|
|
220
|
+
async childTree(id, seen = new Set(), onProgress = async () => {}) {
|
|
207
221
|
if (seen.has(id)) throw new DaemonError("session_identity_mismatch", "OpenCode native topology contains a cycle");
|
|
208
222
|
seen.add(id); const direct = await this.ownedChildren(id), nested = [];
|
|
223
|
+
await onProgress(`children:${id}:${createHash('sha256').update(JSON.stringify(direct)).digest('hex')}`);
|
|
209
224
|
for (const child of direct) {
|
|
210
225
|
if (!child.id || child.parentID !== id) throw new DaemonError("session_identity_mismatch", "OpenCode child has a foreign native parent");
|
|
211
|
-
nested.push(child, ...await this.childTree(child.id, seen));
|
|
226
|
+
nested.push(child, ...await this.childTree(child.id, seen, onProgress));
|
|
212
227
|
}
|
|
213
228
|
return nested;
|
|
214
229
|
}
|
|
@@ -299,17 +314,52 @@ export class Runtime {
|
|
|
299
314
|
return nativeExecutionReceipt(receipt, { daemonId: this.state.daemon_id, revision: before.get(id) ?? 0 });
|
|
300
315
|
}
|
|
301
316
|
async observeModels(id, messages, parent = null) {
|
|
302
|
-
this.
|
|
317
|
+
const journal = this.state.config.journal ?? this.paths.journal;
|
|
318
|
+
if (!this.modelMessages) {
|
|
319
|
+
this.modelMessages = new Map();
|
|
320
|
+
for (const event of journal ? await readModelObservations(journal) : []) {
|
|
321
|
+
if (event.harness !== 'opencode-server' || event.source?.method !== 'session/message' || !event.source.message_id) continue;
|
|
322
|
+
const key = `${event.session_id}:${event.source.message_id}`;
|
|
323
|
+
if (event.evidence_conflict) { const prior = this.modelMessages.get(key); if (prior) prior.conflicted = true; continue; }
|
|
324
|
+
if (!this.modelMessages.has(key)) this.modelMessages.set(key, { source: event.source,
|
|
325
|
+
known: { parent: event.parent_session_id, root: event.native_agent_role?.launch?.root_session_id,
|
|
326
|
+
role: event.native_agent_role ?? null, requested: observedFields(event.requested), observed: observedFields(event.observed) } });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
303
329
|
for (const message of messages ?? []) {
|
|
304
330
|
const info = message.info;
|
|
305
|
-
if (info?.role !== "assistant" || !info.id
|
|
331
|
+
if (info?.role !== "assistant" || !info.id) continue;
|
|
306
332
|
if (info.sessionID && info.sessionID !== id) throw new DaemonError("session_identity_mismatch", "native message belongs to another Session");
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
333
|
+
const record = this.sessions.get(id);
|
|
334
|
+
if (record && (record.parent_provider_session_id ?? null) !== parent)
|
|
335
|
+
throw new DaemonError("session_identity_mismatch", "OpenCode observed message parent changed");
|
|
336
|
+
if (!record?.root_provider_session_id || !info.mode) {
|
|
337
|
+
await observeModel({ journal, harness: "opencode-server", sessionId: id, parentSessionId: parent,
|
|
338
|
+
source: { channel: "native.message_observer", scope: "response", non_asserting: true }, evidence: "unavailable", reason: "native_role_binding_unavailable" });
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const requested = { ...this.state.config, reasoning: this.state.config.variant, mode: parent ? "dd-flow-worker" : this.state.config.agent };
|
|
342
|
+
const nativeAgentRole = this.state.config.workerLaunchConfig ? { name: info.mode, kind: parent ? 'subagent' : 'primary', parent_session_id: parent,
|
|
343
|
+
launch: { ...this.state.config.workerLaunchConfig, root_session_id: record.root_provider_session_id } } : null;
|
|
344
|
+
const observed = observedFields({ provider: info.providerID, model: info.modelID, reasoning: info.variant, mode: info.mode });
|
|
345
|
+
const tuple = { parent, root: record.root_provider_session_id, role: nativeAgentRole, requested: observedFields(requested), observed };
|
|
346
|
+
const key = `${id}:${info.id}`, previous = this.modelMessages.get(key);
|
|
347
|
+
if (previous?.conflicted) throw new DaemonError("model_observation_identity_conflict", "OpenCode native message has a retained identity/profile conflict", false, { session_id: id, message_id: info.id });
|
|
348
|
+
const conflicts = (before, after) => before != null && after != null && (typeof before === "object" && typeof after === "object"
|
|
349
|
+
? [...new Set([...Object.keys(before), ...Object.keys(after)])].some(field => conflicts(before[field], after[field])) : before !== after);
|
|
350
|
+
if (previous && !conflicts(previous.known, tuple)) {
|
|
351
|
+
// Optional disclosure can arrive late. Do not rewrite the old native fact;
|
|
352
|
+
// remember known values only to detect a later positive contradiction.
|
|
353
|
+
for (const [field, value] of Object.entries(observed)) if (value != null) previous.known.observed[field] = value;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const source = previous?.source ?? { method: "session/message", message_id: info.id, event_sha256: createHash("sha256").update(`${id}:${info.id}`).digest("hex") };
|
|
357
|
+
const published = await observeModel({ journal, harness: "opencode-server", sessionId: id, parentSessionId: parent, requested, nativeAgentRole, observed, source, evidence: "response" });
|
|
358
|
+
if (previous || published?.evidence_conflict) {
|
|
359
|
+
if (previous) previous.conflicted = true;
|
|
360
|
+
throw new DaemonError("model_observation_identity_conflict", "OpenCode native message changed its published identity/profile", false, { session_id: id, message_id: info.id });
|
|
361
|
+
}
|
|
362
|
+
this.modelMessages.set(key, { known: structuredClone(tuple), source });
|
|
313
363
|
}
|
|
314
364
|
}
|
|
315
365
|
async requireSettled(id) {
|
|
@@ -372,8 +422,27 @@ export class Runtime {
|
|
|
372
422
|
current.reading = readNativeHookFailures(this.paths.dir, current.requests, current, error => this.retainHookFailure(current, error));
|
|
373
423
|
try { await current.reading; } finally { current.reading = null; }
|
|
374
424
|
}
|
|
375
|
-
retainHookFailure(current, error) {
|
|
376
|
-
if (this.nativeHookOperation !== current
|
|
425
|
+
async retainHookFailure(current, error, unconfirmed = false) {
|
|
426
|
+
if (this.nativeHookOperation !== current) return;
|
|
427
|
+
if (unconfirmed || error.details?.native_hook_binding?.native_creator === 'unconfirmed') {
|
|
428
|
+
// A Session/tree association is not the native creator Turn. Preserve
|
|
429
|
+
// the real refusal, but do not abort the prompt merely being observed.
|
|
430
|
+
const diagnostic = { code: 'native_hook_failed', native_creator: 'unconfirmed', observer_operation_id: current.operationId, observer_turn_generation: current.generation, primary_error: errorRecord(error) };
|
|
431
|
+
(current.hookDiagnostics ??= []).push(diagnostic);
|
|
432
|
+
try { await this.journal('native_hook_observation_unavailable', { provider_session_id: current.sessionId, native_hook_diagnostic: diagnostic }); }
|
|
433
|
+
catch (storage) { process.stderr.write(`${diagnosticJson({ native_hook_diagnostic: diagnostic, journal_error: errorRecord(storage) })}\n`); }
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (isProvenShellCorrection(error)) return;
|
|
437
|
+
if (isNativeHookObservationLoss(error)) {
|
|
438
|
+
// A timed-out observer has not established whether the owned CLI ran.
|
|
439
|
+
// The CLI still owns admission and the native turn owns its outcome.
|
|
440
|
+
const diagnostic = errorRecord(error);
|
|
441
|
+
(current.hookDiagnostics ??= []).push(diagnostic);
|
|
442
|
+
try { await this.journal("native_hook_observation_unavailable", { provider_session_id: current.sessionId, native_hook_diagnostic: diagnostic }); }
|
|
443
|
+
catch (storage) { process.stderr.write(`${diagnosticJson({ native_hook_diagnostic: diagnostic, journal_error: errorRecord(storage) })}\n`); }
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
377
446
|
error.details = { ...error.details, provider_session_id: current.sessionId, native_outcome: "unknown", native_turn_items: { observed: true, possible_effects: true, pending: true } };
|
|
378
447
|
current.failure ??= error;
|
|
379
448
|
}
|
|
@@ -381,12 +450,46 @@ export class Runtime {
|
|
|
381
450
|
const assertDispatch = captureDispatchGuard(this);
|
|
382
451
|
if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
|
|
383
452
|
if (operation === "daemon.status") { const { password, ...config } = this.state.config; return { daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, auth_status: this.state.auth_status, shutdown_state: this.state.shutdown_state, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config }; }
|
|
453
|
+
if (operation === "hook.register") {
|
|
454
|
+
if (!this.state.config.runtime_owner || !this.state.config.ddFlowBin) return {};
|
|
455
|
+
assertDaemonDispatch(this.paths.dir);
|
|
456
|
+
const payload = params.payload, observer = this.nativeHookOperation ?? this.lastNativeHookOperation;
|
|
457
|
+
if (typeof params.sessionId !== "string" || !params.sessionId || typeof payload?.message_id !== "string" || !payload.message_id
|
|
458
|
+
|| typeof payload?.tool_call_id !== "string" || !payload.tool_call_id || !observer?.sessionId) throw new DaemonError("native_hook_unproven", "Native Work start lacks physical callback identity");
|
|
459
|
+
this.retainedWorkStarts ??= new Map();
|
|
460
|
+
const tool = `${payload.message_id}:${payload.tool_call_id}`;
|
|
461
|
+
const accepted = await retainNativeWorkStart(this.paths.dir, { daemon_id: this.state.daemon_id, source_harness: "opencode-server",
|
|
462
|
+
root_provider_session_id: observer.sessionId, provider_session_id: params.sessionId, parent_provider_session_id: null,
|
|
463
|
+
tool_call_id: tool, event: "PreToolUse", native_command: payload.input?.command, native_cwd: null,
|
|
464
|
+
native_payload: { schema_id: "dd-flow/opencode-tool-event@1", phase: "before", event_id: [this.state.daemon_id, params.sessionId, payload.message_id, payload.tool_call_id].join(":"),
|
|
465
|
+
daemon_id: this.state.daemon_id, message_id: payload.message_id, tool_call_id: tool, tool: "shell", input: payload.input,
|
|
466
|
+
session: { provider_session_id: params.sessionId } } }, observer, this.retainedWorkStarts);
|
|
467
|
+
return accepted ? { fastAck: true, nativeHookBinding: accepted } : {};
|
|
468
|
+
}
|
|
384
469
|
if (operation === "hook.resolve") {
|
|
385
|
-
const
|
|
470
|
+
const payload = params.payload;
|
|
471
|
+
if (params.deferWorkStart === true && !this.state.config.noFlow && this.state.config.runtime_owner && this.state.config.ddFlowBin
|
|
472
|
+
&& ["before", "after"].includes(payload?.phase) && typeof params.sessionId === "string" && params.sessionId
|
|
473
|
+
&& typeof payload.message_id === "string" && payload.message_id && typeof payload.tool_call_id === "string" && payload.tool_call_id
|
|
474
|
+
&& typeof payload.input?.command === "string") {
|
|
475
|
+
const analysis = parseLifecycleCommand(payload.input.command);
|
|
476
|
+
if (analysis.participation === "none" && !analysis.unresolved) {
|
|
477
|
+
// No lifecycle authority is involved. Native SSE/history still own
|
|
478
|
+
// source-bound progress; an unknown child callback is only a hint.
|
|
479
|
+
return { observationOnly: true, daemonId: this.state.daemon_id, sessionId: params.sessionId,
|
|
480
|
+
messageId: payload.message_id, toolCallId: payload.tool_call_id, command: payload.input.command };
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (params.deferWorkStart === true && params.payload?.phase === "before") {
|
|
484
|
+
const pending = await this.dispatch("hook.register", params);
|
|
485
|
+
if (pending.fastAck === true) return pending;
|
|
486
|
+
}
|
|
487
|
+
const current = this.nativeHookOperation, sessionId = params.sessionId;
|
|
386
488
|
if (!current || !sessionId || typeof payload?.input?.command !== "string" || typeof payload.tool_call_id !== "string" || !payload.tool_call_id || typeof payload.message_id !== "string" || !payload.message_id || !["before", "after"].includes(payload.phase))
|
|
387
489
|
throw new DaemonError("native_hook_unproven", "OpenCode hook has no exact active operation");
|
|
388
490
|
let owned = sessionId === current.sessionId || this.sessions.get(sessionId)?.root_provider_session_id === current.sessionId;
|
|
389
491
|
try {
|
|
492
|
+
const guard = current.guard ?? captureDispatchGuard(this); guard();
|
|
390
493
|
const tree = await this.childTree(current.sessionId);
|
|
391
494
|
if (sessionId !== current.sessionId && !tree.some(child => child.id === sessionId && !child.fork)) throw new DaemonError("foreign_session", "OpenCode hook Session is outside the owned native tree");
|
|
392
495
|
owned = true;
|
|
@@ -396,29 +499,66 @@ export class Runtime {
|
|
|
396
499
|
if (payload.input.workdir !== undefined && typeof payload.input.workdir !== "string") throw new DaemonError("native_hook_unproven", "OpenCode hook workdir must be a string");
|
|
397
500
|
if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode operation changed during hook identity observation");
|
|
398
501
|
const accepted = await acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation,
|
|
399
|
-
provider_session_id: sessionId, root_provider_session_id: current.sessionId, tool_call_id: `${payload.message_id}:${payload.tool_call_id}`, event: payload.phase === "after" ? "PostToolUse" : "PreToolUse",
|
|
502
|
+
provider_session_id: sessionId, root_provider_session_id: current.sessionId, native_creator: 'unconfirmed', tool_call_id: `${payload.message_id}:${payload.tool_call_id}`, event: payload.phase === "after" ? "PostToolUse" : "PreToolUse",
|
|
400
503
|
command_sha256: createHash("sha256").update(payload.input.command).digest("hex"), native_cwd: path.resolve(native.directory, payload.input.workdir ?? ".") }, current);
|
|
401
504
|
if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode operation changed during hook acceptance");
|
|
402
|
-
current.requests.set(accepted.request_id, { current, binding: accepted }); return { nativeHookBinding: accepted, nativeSession: {
|
|
505
|
+
guard(); current.requests.set(accepted.request_id, { current, binding: accepted, guard }); return { nativeHookBinding: accepted, nativeSession: {
|
|
403
506
|
id: native.id, directory: native.directory, parentID: native.parentID, projectID: native.projectID,
|
|
404
507
|
model: { providerID: native.model?.providerID, id: native.model?.id, variant: native.model?.variant }
|
|
405
508
|
} };
|
|
406
509
|
} catch (error) {
|
|
407
|
-
if (owned) this.retainHookFailure(current, error);
|
|
510
|
+
if (owned) await this.retainHookFailure(current, error, true);
|
|
408
511
|
throw error;
|
|
409
512
|
}
|
|
410
513
|
}
|
|
411
514
|
if (operation === "hook.verify") {
|
|
412
515
|
const current = this.nativeHookOperation, binding = params.binding;
|
|
516
|
+
if (binding?.native_admission === "assigned_work_start") {
|
|
517
|
+
const retained = this.retainedWorkStarts?.get(params.requestId);
|
|
518
|
+
if (!retained || !isDeepStrictEqual(retained.binding, binding)) throw new DaemonError("native_hook_unproven", "Unknown native Work start callback");
|
|
519
|
+
retained.guard(); assertDaemonDispatch(this.paths.dir);
|
|
520
|
+
const root = binding.root_provider_session_id, id = binding.provider_session_id;
|
|
521
|
+
const report = operationProgressReporter();
|
|
522
|
+
const checkpoint = cursor => report('verification_progress', { native_request_id: cursor, session_id: id });
|
|
523
|
+
const tree = await this.childTree(root, new Set(), checkpoint), child = tree.find(node => node.id === id);
|
|
524
|
+
if (id !== root && (!child || child.fork)) throw new DaemonError("foreign_session", "Native Work start is outside its controlled tree");
|
|
525
|
+
const native = await this.client.inspect(id);
|
|
526
|
+
await checkpoint(`session:${id}:${createHash('sha256').update(JSON.stringify(native)).digest('hex')}`);
|
|
527
|
+
if (native.id !== id || native.fork || !path.isAbsolute(native.directory ?? "") || child && native.parentID !== child.parentID
|
|
528
|
+
|| id === root && native.parentID) throw new DaemonError("native_hook_identity_conflict", "Native Work start ancestry is contradictory");
|
|
529
|
+
const input = binding.native_payload.input;
|
|
530
|
+
if (input.workdir !== undefined && typeof input.workdir !== "string") throw new DaemonError("native_hook_unproven", "Native Work start has invalid workdir");
|
|
531
|
+
retained.guard();
|
|
532
|
+
const cwd = path.resolve(native.directory, input.workdir ?? "."), parent = native.parentID ?? null;
|
|
533
|
+
const verified = { ...binding, parent_provider_session_id: parent, native_cwd: cwd, native_ancestry: "confirmed",
|
|
534
|
+
native_payload: { ...binding.native_payload, input: { ...input, workdir: cwd }, session: { provider_session_id: id, parent_provider_session_id: parent, directory: cwd },
|
|
535
|
+
profile: { provider: native.model?.providerID, model: native.model?.id, variant: native.model?.variant ?? "default" } } };
|
|
536
|
+
return { verified: true, registeredHookBinding: binding, nativeHookBinding: verified };
|
|
537
|
+
}
|
|
538
|
+
if (params.phase === "after") {
|
|
539
|
+
const retained = this.retainedHookRequests.get(binding?.request_id);
|
|
540
|
+
if (!retained || JSON.stringify(retained.binding) !== JSON.stringify(binding)) throw new DaemonError("native_hook_unproven", "OpenCode after hook has no retained original delivery");
|
|
541
|
+
assertDaemonDispatch(this.paths.dir); retained.guard();
|
|
542
|
+
return { current: true, retained: true };
|
|
543
|
+
}
|
|
413
544
|
if (!current || binding?.operation_id !== current.operationId || binding?.turn_generation !== current.generation
|
|
414
545
|
|| JSON.stringify(current.requests.get(binding?.request_id)?.binding) !== JSON.stringify(binding)) throw new DaemonError("native_hook_unproven", "OpenCode hook generation is no longer current");
|
|
546
|
+
const accepted = current.requests.get(binding.request_id); assertDaemonDispatch(this.paths.dir); accepted.guard();
|
|
547
|
+
if (params.retain_after === true && binding.event === "PreToolUse") this.retainedHookRequests.set(binding.request_id, { binding, guard: accepted.guard });
|
|
415
548
|
return { current: true };
|
|
416
549
|
}
|
|
417
550
|
if (operation === "hook.failed") {
|
|
418
551
|
if (params.failure) {
|
|
419
|
-
await this.dispatch("hook.verify", { binding: params.binding });
|
|
420
552
|
if (JSON.stringify(params.failure.details?.native_hook_binding) !== JSON.stringify(params.binding) || !params.failure.details?.receipt_error || typeof params.failure.code !== "string" || typeof params.failure.message !== "string") throw new DaemonError("native_hook_receipt_invalid", "OpenCode fallback hook error is not bound");
|
|
421
|
-
this.
|
|
553
|
+
const current = this.nativeHookOperation, binding = params.binding;
|
|
554
|
+
const isCurrent = current && binding?.operation_id === current.operationId && binding?.turn_generation === current.generation
|
|
555
|
+
&& JSON.stringify(current.requests.get(binding?.request_id)?.binding) === JSON.stringify(binding);
|
|
556
|
+
await this.dispatch("hook.verify", isCurrent ? { binding } : { binding, phase: "after" });
|
|
557
|
+
// The caller's write failed. Do not acknowledge the fallback until the
|
|
558
|
+
// same owner-bound failure is durable; a late after never poisons the
|
|
559
|
+
// currently active, different prompt.
|
|
560
|
+
await writeNativeHookFailure(this.paths.dir, binding, reportedError(params.failure, "Native hook failed"));
|
|
561
|
+
if (!isCurrent) return { observed: true, retained: true };
|
|
422
562
|
}
|
|
423
563
|
await this.observeHookFailures(); return { observed: true };
|
|
424
564
|
}
|
|
@@ -453,8 +593,9 @@ export class Runtime {
|
|
|
453
593
|
if (operation === "session.inspect" || operation === "session.resume") { const result = await this.describe(params.sessionId); await this.persist(); return result; }
|
|
454
594
|
if (operation === "session.prompt") { await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
|
|
455
595
|
this.state.operations ??= {}; const key = operationId ?? randomUUID(), retained = this.state.operations[key];
|
|
456
|
-
const current = { operationId: key, sessionId: params.sessionId, generation: (this.state.hook_generation ?? 0) + 1, requests: new Map(), failure: null };
|
|
596
|
+
const current = { operationId: key, sessionId: params.sessionId, generation: (this.state.hook_generation ?? 0) + 1, requests: new Map(), failure: null, guard: assertDispatch };
|
|
457
597
|
this.state.hook_generation = current.generation; this.nativeHookOperation = current;
|
|
598
|
+
this.lastNativeHookOperation = current;
|
|
458
599
|
const checkHookFailures = async () => { await this.observeHookFailures(current); if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode prompt generation changed"); if (current.failure) throw current.failure; };
|
|
459
600
|
let reply;
|
|
460
601
|
try { assertDispatch(); reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, messageId: retained?.message_id ?? `msg_${randomUUID().replaceAll("-", "")}`, retained,
|
|
@@ -462,6 +603,9 @@ export class Runtime {
|
|
|
462
603
|
assertDispatch, checkHookFailures, livenessTimeoutMs: params.livenessTimeoutMs ?? this.client.timeoutMs,
|
|
463
604
|
onDispatched: () => this.journal("native_dispatch", { operation_id: operationId, provider_session_id: params.sessionId }),
|
|
464
605
|
onModelMessages: (id, messages, parent) => this.observeModels(id, messages, parent),
|
|
606
|
+
onNativeSession: (info, root) => this.registerObservedChild(info, root),
|
|
607
|
+
rootSessionId: this.sessions.get(params.sessionId)?.root_provider_session_id ?? params.sessionId,
|
|
608
|
+
parentSessionId: this.sessions.get(params.sessionId)?.parent_provider_session_id ?? null,
|
|
465
609
|
onObservationError: error => observeModel({ journal: this.state.config.journal ?? this.paths.journal, harness: "opencode-server", sessionId: params.sessionId, requested: this.state.config, source: "native.message_observer", evidence: "unavailable", reason: error.code ?? "observation_unavailable" })
|
|
466
610
|
}); await checkHookFailures(); }
|
|
467
611
|
finally { current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; }
|
|
@@ -476,6 +620,7 @@ export class Runtime {
|
|
|
476
620
|
if (!result.route_conformance.matches) throw new DaemonError("invocation_directory_mismatch", "Owned OpenCode Session moved during execution", false, { ...result.route_violations[0], native_outcome: result.terminal_status, settled: result.settled, result });
|
|
477
621
|
result.response_message_id = reply.info.id;
|
|
478
622
|
result.observed_profile = { ...reply_profile, mode: result.info.agent ?? reply_profile.mode };
|
|
623
|
+
if (current.hookDiagnostics?.length) result.native_hook_diagnostics = current.hookDiagnostics;
|
|
479
624
|
checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: expectedAgent }, result.observed_profile); result.usage_ingest_errors = await this.reconcileUsage(result); return result; }); }
|
|
480
625
|
if (operation === "session.fork") { const source = await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
|
|
481
626
|
const info = await this.client.fork(params.sessionId, params.messageId);
|
|
@@ -2,7 +2,6 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
3
|
import { ToolObservations, openCodeToolObservation } from "./tool-observations.mjs";
|
|
4
4
|
import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
|
|
5
|
-
import { assertNativeLine } from "./native-streams.mjs";
|
|
6
5
|
import { operationProgressReporter } from "./operation-progress.mjs";
|
|
7
6
|
import { classifyRecoveryError as classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
|
|
8
7
|
|
|
@@ -98,7 +97,7 @@ export class OpenCodeClient {
|
|
|
98
97
|
const options = { signal: controller.signal };
|
|
99
98
|
const pending = input === undefined ? domain[name](options) : domain[name](input, options);
|
|
100
99
|
void pending.catch(() => {}); await onDispatched?.(); const result = await pending;
|
|
101
|
-
|
|
100
|
+
return result;
|
|
102
101
|
} catch (cause) {
|
|
103
102
|
if (cause instanceof OpenCodeError || cause.code === "operation_observation_lost") throw cause;
|
|
104
103
|
if (cause._tag === "SessionNotFoundError" || cause._tag === "NotFoundError") throw new OpenCodeError("session_not_found", "OpenCode native Session is unavailable");
|
|
@@ -169,13 +168,32 @@ export class OpenCodeClient {
|
|
|
169
168
|
import(data) { return this.call("session.import", data, { productive: true }); }
|
|
170
169
|
|
|
171
170
|
async prompt(id, { prompt, messageId = `msg_${randomUUID().replaceAll("-", "")}`, retained,
|
|
172
|
-
save, onModelMessages,
|
|
171
|
+
save, onModelMessages, onNativeSession, rootSessionId = id, parentSessionId = null,
|
|
172
|
+
onObservationError, assertDispatch, onDispatched, checkHookFailures, livenessTimeoutMs = this.timeoutMs }) {
|
|
173
173
|
validateObservationDuration(livenessTimeoutMs);
|
|
174
174
|
const request = { sessionID: id, id: messageId, text: prompt, delivery: "queue", resume: true }, digest = requestDigest(request);
|
|
175
175
|
if (retained && (retained.request_sha256 !== digest || retained.message_id !== messageId)) throw new OpenCodeError("operation_request_mismatch", "OpenCode operation already binds another prompt", false, { effect: "no_effect" });
|
|
176
176
|
let state = retained ?? { message_id: messageId, request_sha256: digest, phase: "prepared", cursor: 0 };
|
|
177
|
-
const previous = await this.messages(id), baseline = new Map([[id, new Set(previous.map(item => item.info.id))]]), parents = new Map();
|
|
178
|
-
|
|
177
|
+
const previous = await this.messages(id), baseline = new Map([[id, new Set(previous.map(item => item.info.id))]]), parents = new Map([[id, parentSessionId]]);
|
|
178
|
+
// The same native lineage guard applies before dispatch and while polling.
|
|
179
|
+
// Registration precedes message publication; a list ID alone is not ownership.
|
|
180
|
+
const childrenOf = async (session, visited) => {
|
|
181
|
+
const children = await this.children(session);
|
|
182
|
+
for (const child of children) {
|
|
183
|
+
if (!child?.id || child.parentID !== session || child.fork || session !== rootSessionId || visited.has(child.id)
|
|
184
|
+
|| parents.has(child.id) && parents.get(child.id) !== session || visited.size >= 1024)
|
|
185
|
+
throw new OpenCodeError("session_identity_mismatch", "OpenCode native child lineage is invalid", false, { session_id: child?.id, parent_session_id: session });
|
|
186
|
+
visited.add(child.id); parents.set(child.id, session);
|
|
187
|
+
await onNativeSession?.(child, rootSessionId);
|
|
188
|
+
}
|
|
189
|
+
return children;
|
|
190
|
+
};
|
|
191
|
+
const capture = async (session, visited = new Set([id])) => {
|
|
192
|
+
for (const child of await childrenOf(session, visited)) {
|
|
193
|
+
baseline.set(child.id, new Set((await this.messages(child.id)).map(item => item.info.id)));
|
|
194
|
+
await capture(child.id, visited);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
179
197
|
await capture(id);
|
|
180
198
|
if (retained) {
|
|
181
199
|
const index = previous.findIndex(item => item.info.id === messageId);
|
|
@@ -258,8 +276,8 @@ export class OpenCodeClient {
|
|
|
258
276
|
}
|
|
259
277
|
if (state.cursor !== watermark) { state = { ...state, cursor: watermark }; await save?.(state); }
|
|
260
278
|
let runningTools = false;
|
|
261
|
-
const observe = async (session, visited = new Set()) => {
|
|
262
|
-
|
|
279
|
+
const observe = async (session, visited = new Set([id])) => {
|
|
280
|
+
known.add(session);
|
|
263
281
|
const messages = await this.messages(session);
|
|
264
282
|
await onModelMessages?.(session, messages, parents.get(session) ?? null);
|
|
265
283
|
const floor = baseline.get(session) ?? new Set(), current = messages.filter(item => item.info.role === "assistant" && !floor.has(item.info.id));
|
|
@@ -291,10 +309,7 @@ export class OpenCodeClient {
|
|
|
291
309
|
}
|
|
292
310
|
}
|
|
293
311
|
}
|
|
294
|
-
for (const child of await
|
|
295
|
-
if (child.parentID !== session) throw lost("OpenCode child has a foreign parent");
|
|
296
|
-
parents.set(child.id, session); await observe(child.id, visited);
|
|
297
|
-
}
|
|
312
|
+
for (const child of await childrenOf(session, visited)) await observe(child.id, visited);
|
|
298
313
|
return current;
|
|
299
314
|
};
|
|
300
315
|
const current = await observe(id), rootMessages = await this.messages(id);
|
|
@@ -309,12 +324,17 @@ export class OpenCodeClient {
|
|
|
309
324
|
// Native messages and topology are separate reads: a child can
|
|
310
325
|
// finish between them. Reconcile its ancestry before rejecting
|
|
311
326
|
// the notification, never trust the notification alone.
|
|
312
|
-
const
|
|
313
|
-
|
|
327
|
+
const visited = new Set([id]);
|
|
328
|
+
const child = (await childrenOf(id, visited)).find(child => child.id === item.info.metadata.childID);
|
|
329
|
+
if (child) await observe(child.id, visited);
|
|
314
330
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
331
|
+
// Native continuation messages are synthetic, including retries
|
|
332
|
+
// without metadata. Only actual user input crosses the owned
|
|
333
|
+
// operation boundary. Claimed child/shell delivery is validated
|
|
334
|
+
// separately before it can settle a background task.
|
|
335
|
+
if (item.info.role === "user" || item.info.role === "synthetic" && (
|
|
336
|
+
item.info.metadata?.source === "subagent" && parents.get(item.info.metadata.childID) !== id
|
|
337
|
+
|| item.info.metadata?.source === "shell" && !shellCursors.has(item.info.metadata.shellID)))
|
|
318
338
|
throw new OpenCodeError("opencode_foreign_input", "OpenCode received input outside the owned operation", false, { session_id: id, message_id: item.info.id, role: item.info.role, source: item.info.metadata?.source });
|
|
319
339
|
}
|
|
320
340
|
const terminal = rootMessages.slice(deliveredIndex + 1).findLast(item => item.info.role === "idle");
|
|
@@ -327,8 +347,8 @@ export class OpenCodeClient {
|
|
|
327
347
|
// A background child's native completion notification is delivered
|
|
328
348
|
// asynchronously. An idle parent before that delivery is not the
|
|
329
349
|
// completion of the coordinating operation.
|
|
330
|
-
const deliveredNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "subagent").map(item => item.info.metadata.childID));
|
|
331
|
-
const shellNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "shell").map(item => item.info.metadata.shellID));
|
|
350
|
+
const deliveredNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "subagent" && parents.get(item.info.metadata.childID) === id).map(item => item.info.metadata.childID));
|
|
351
|
+
const shellNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "shell" && shellCursors.has(item.info.metadata.shellID)).map(item => item.info.metadata.shellID));
|
|
332
352
|
unsettled ||= [...backgroundShells].some(shell => !shellNotifications.has(shell));
|
|
333
353
|
for (const item of current) for (const part of item.parts) {
|
|
334
354
|
if (part.tool === "subagent" && part.state?.input?.background === true) {
|
|
@@ -353,7 +373,8 @@ export class OpenCodeClient {
|
|
|
353
373
|
}
|
|
354
374
|
} catch (error) {
|
|
355
375
|
await checkHookFailures?.();
|
|
356
|
-
if (error instanceof OpenCodeError && !["opencode_unavailable", "operation_observation_lost"].includes(error.code)
|
|
376
|
+
if (error instanceof OpenCodeError && !["opencode_unavailable", "operation_observation_lost"].includes(error.code)
|
|
377
|
+
|| ["session_identity_mismatch", "model_observation_identity_conflict"].includes(error.code)) throw error;
|
|
357
378
|
await onObservationError?.(error);
|
|
358
379
|
}
|
|
359
380
|
if (clock.sample(marker)) throw lost("OpenCode native progress stopped or observation could not recover", { session_id: id, message_id: messageId, observation: clock.state(), ...(observationErrors.count ? { observation_errors: observationErrors } : {}) });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function callDaemon(stateDir: string, operation: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<Record<string, unknown>>;
|