@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149
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 +130 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +13 -3
- package/dist/cli/command-metadata.js +51 -16
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/help.js +67 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +9 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +77 -68
- package/dist/cli/memory-storage-admission.js +3 -2
- package/dist/cli/memory-storage-qualification.js +105 -0
- package/dist/cli/memory-storage-transition.js +123 -0
- package/dist/cli/memory-upgrade.js +189 -0
- package/dist/cli/native-hook-ingress.js +14 -4
- package/dist/cli/native-hook-preparation.js +55 -0
- package/dist/cli/response-file.js +2 -1
- package/dist/cli/run-cli.js +79 -40
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +18 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -0
- package/dist/domain/stage-handler-identities.js +8 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
- package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
- package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
- package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
- package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
- package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
- package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
- package/dist/harness-runtime/lib/native-children.d.mts +8 -1
- package/dist/harness-runtime/lib/native-children.mjs +114 -8
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
- package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
- package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
- package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
- package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
- package/dist/schemas/diagnostic-work-result.schema.json +296 -0
- package/dist/schemas/execution-bundle.schema.json +7 -1
- package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
- package/dist/schemas/memory-repair-work-result.schema.json +9 -2
- package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
- package/dist/schemas/release-impact.schema.json +3 -3
- package/dist/schemas/stage-start-response.schema.json +4 -0
- package/dist/services/accepted-intent.js +13 -1
- package/dist/services/canon.js +43 -7
- package/dist/services/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +267 -15
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +34 -19
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-execution.js +10 -4
- package/dist/services/controller-fanout.js +34 -10
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +49 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-settlement.js +5 -3
- package/dist/services/descriptor-run-start.js +46 -20
- package/dist/services/descriptor-stage-coordinator.js +66 -13
- package/dist/services/descriptor-stage-settlement.js +2 -1
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/diagnostic-evidence.js +73 -0
- package/dist/services/diagnostic-report-publication.js +72 -0
- package/dist/services/diagnostic-report.js +67 -0
- package/dist/services/diagnostic-run-bundle.js +74 -0
- package/dist/services/diagnostic-snapshot.js +159 -0
- package/dist/services/diagnostic-stage-handlers.js +108 -0
- package/dist/services/diagnostic-stage-results.js +132 -0
- package/dist/services/diagnostic-start.js +84 -0
- package/dist/services/diagnostic-work-batch.js +34 -0
- package/dist/services/diagnostic-work-collection.js +97 -0
- package/dist/services/diagnostic-work-registration.js +101 -0
- package/dist/services/diagnostic-work-results.js +93 -0
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +186 -119
- package/dist/services/execution-bundles.js +19 -7
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +13 -4
- package/dist/services/frozen-engine.js +148 -11
- package/dist/services/hook-memory-routing.js +188 -0
- package/dist/services/hook-recovery-guard.js +14 -0
- package/dist/services/hook-session-observation.js +24 -0
- package/dist/services/hooks.js +27 -8
- package/dist/services/hybrid-contracts.js +4 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +47 -4
- package/dist/services/lifecycle-invocations.js +179 -38
- package/dist/services/memory-accepted-composition.js +5 -4
- package/dist/services/memory-completed-correction-wave.js +23 -15
- package/dist/services/memory-compose-handler.js +163 -21
- package/dist/services/memory-composition-acceptance.js +186 -10
- package/dist/services/memory-composition-graph.js +42 -11
- package/dist/services/memory-composition-registration.js +23 -8
- package/dist/services/memory-composition-work-custody.js +14 -9
- package/dist/services/memory-composition-work-results.js +15 -3
- package/dist/services/memory-corrected-review-batch.js +10 -3
- package/dist/services/memory-corrected-review-findings.js +4 -2
- package/dist/services/memory-corrected-review-inputs.js +10 -5
- package/dist/services/memory-corrected-review-receipt.js +17 -9
- package/dist/services/memory-corrected-review-registration.js +3 -2
- package/dist/services/memory-correction-check-receipt.js +20 -4
- package/dist/services/memory-custody-snapshot.js +36 -0
- package/dist/services/memory-finish-evidence.js +44 -22
- package/dist/services/memory-finish-handler.js +37 -23
- package/dist/services/memory-finish-integration.js +218 -48
- package/dist/services/memory-finish-readiness.js +66 -19
- package/dist/services/memory-git-integration.js +87 -18
- package/dist/services/memory-historical-corrected-review.js +21 -15
- package/dist/services/memory-historical-original-review.js +22 -9
- package/dist/services/memory-historical-repair-registration.js +14 -8
- package/dist/services/memory-historical-second-review.js +30 -11
- package/dist/services/memory-init-contexts.js +44 -2
- package/dist/services/memory-init-controller-start.js +27 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +24 -6
- package/dist/services/memory-init-native-capacity.js +17 -4
- package/dist/services/memory-init-preflight.js +21 -17
- package/dist/services/memory-init-request-candidate.js +73 -17
- package/dist/services/memory-init-request-input.js +23 -7
- package/dist/services/memory-init-run-bundle.js +49 -12
- package/dist/services/memory-init-storage.js +17 -7
- package/dist/services/memory-init-worktrunk-capability.js +3 -7
- package/dist/services/memory-layer.js +71 -6
- package/dist/services/memory-lint-config.js +111 -2
- package/dist/services/memory-native-review-copy.js +5 -67
- package/dist/services/memory-normal-flow-entry.js +12 -0
- package/dist/services/memory-ordinary-defaults.js +22 -5
- package/dist/services/memory-preflight-handler.js +15 -0
- package/dist/services/memory-repair-current-view.js +1 -1
- package/dist/services/memory-repair-document-delta.js +70 -33
- package/dist/services/memory-repair-history.js +12 -7
- package/dist/services/memory-repair-physical-binding.js +1 -119
- package/dist/services/memory-repair-publication-plan.js +5 -3
- package/dist/services/memory-repair-publication-source.js +1 -1
- package/dist/services/memory-repair-publication.js +3 -2
- package/dist/services/memory-repair-registration.js +2 -1
- package/dist/services/memory-repair-work-baseline.js +4 -2
- package/dist/services/memory-repair-work-completion.js +32 -12
- package/dist/services/memory-repair-work-results.js +21 -2
- package/dist/services/memory-repair-workspace.js +11 -2
- package/dist/services/memory-repair-writer-copy.js +2 -1
- package/dist/services/memory-result-binding.js +78 -12
- package/dist/services/memory-retained-review-authority.js +17 -8
- package/dist/services/memory-retained-second-baseline.js +1 -1
- package/dist/services/memory-retained-second-repairs.js +12 -6
- package/dist/services/memory-review-correction-plan.js +47 -12
- package/dist/services/memory-review-handler.js +17 -17
- package/dist/services/memory-review-inputs.js +55 -9
- package/dist/services/memory-review-limits.js +10 -0
- package/dist/services/memory-review-receipt.js +2 -2
- package/dist/services/memory-review-registration.js +13 -4
- package/dist/services/memory-review-work-custody.js +3 -1
- package/dist/services/memory-review-work-input.js +26 -7
- package/dist/services/memory-review-work-results.js +26 -4
- package/dist/services/memory-second-corrected-review-inputs.js +5 -3
- package/dist/services/memory-second-correction-plan.js +2 -1
- package/dist/services/memory-second-repair-launch.js +4 -4
- package/dist/services/memory-second-repair-source.js +1 -1
- package/dist/services/memory-stage-dispatch.js +38 -1
- package/dist/services/memory-stage-results.js +113 -22
- package/dist/services/memory-tool-probe.js +25 -0
- package/dist/services/memory-upgrade-analysis-handler.js +243 -0
- package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
- package/dist/services/memory-upgrade-evidence.js +100 -0
- package/dist/services/memory-upgrade-inventory.js +442 -0
- package/dist/services/memory-upgrade-publication-custody.js +73 -0
- package/dist/services/memory-upgrade-publication.js +413 -0
- package/dist/services/memory-upgrade-selection.js +430 -0
- package/dist/services/memory-upgrade-start.js +60 -0
- package/dist/services/memory-upgrade-storage-readiness.js +340 -0
- package/dist/services/memory-verification-acceptance.js +38 -17
- package/dist/services/memory-verification-checks.js +92 -27
- package/dist/services/memory-verification-receipt.js +6 -5
- package/dist/services/memory-verify-handler.js +15 -13
- package/dist/services/memory-work-result-json.js +12 -0
- package/dist/services/memory-work-result-preparation.js +5 -4
- package/dist/services/merge-server.js +4 -2
- package/dist/services/migrations.js +127 -98
- package/dist/services/native-daemon-history.js +2 -1
- package/dist/services/physical-startup.js +45 -0
- package/dist/services/project-registry.js +97 -0
- package/dist/services/project-summary.js +1 -1
- package/dist/services/projects.js +3 -93
- package/dist/services/prompt-delivery.js +71 -0
- package/dist/services/provider-capacity.js +112 -46
- package/dist/services/recovery-snapshot-database.js +32 -4
- package/dist/services/release-cache.js +8 -0
- package/dist/services/repair-intents.js +1 -12
- package/dist/services/repair-receipt.js +13 -0
- package/dist/services/run-control-worker.js +35 -18
- package/dist/services/run-control.js +2 -1
- package/dist/services/run-controller-adapter.js +2 -1
- package/dist/services/run-controller-process.js +4 -7
- package/dist/services/run-controller-recovery.js +8 -2
- package/dist/services/run-controller.js +215 -94
- package/dist/services/run-fork.js +1 -1
- package/dist/services/run-recovery-guard.js +30 -0
- package/dist/services/run-recovery.js +11 -44
- package/dist/services/run-start-custody.js +17 -5
- package/dist/services/runs.js +6 -1
- package/dist/services/runtime-scope-resume.js +12 -6
- package/dist/services/runtime-scope-stop.js +3 -2
- package/dist/services/runtime-scope-worker.js +46 -19
- package/dist/services/runtime-service.js +2 -1
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +22 -7
- package/dist/services/stage-pause.js +111 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code.js +2 -2
- package/dist/services/vnext-fanout.js +51 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +117 -39
- package/dist/services/workspace-bootstrap.js +2 -1
- package/dist/services/workspace-files.js +1 -1
- package/dist/shared/entity-references.js +1 -8
- package/dist/storage/data-preservation.js +65 -0
- package/dist/storage/database.js +53 -19
- package/dist/storage/import-guard.js +84 -27
- package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
- package/dist/storage/paired-writer-migration.js +424 -0
- package/dist/storage/writer-contract.js +11 -4
- package/dist/storage/writer-migration.js +313 -52
- package/dist/storage/writer-restore.js +217 -0
- package/package.json +13 -6
|
@@ -102,7 +102,7 @@ async function identity(bridge, options) {
|
|
|
102
102
|
return sessionId;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
async function cancelTree(bridge, sessionId) {
|
|
105
|
+
async function cancelTree(bridge, sessionId, options = {}) {
|
|
106
106
|
const before = await subagents(bridge, sessionId).catch(error => ({ observation_error: { code: error.code, message: error.message } })); const cancellations = [];
|
|
107
107
|
for (const child of before.subagents ?? []) {
|
|
108
108
|
const childId = child.sessionId ?? child.subagentId;
|
|
@@ -119,7 +119,10 @@ async function cancelTree(bridge, sessionId) {
|
|
|
119
119
|
if (!(after.subagents ?? []).length) {
|
|
120
120
|
const root = await sessionInfo(bridge, sessionId).catch(error => ({ observation_error: { code: error.code, message: error.message } }));
|
|
121
121
|
const status = root?.status ?? root?.state ?? root?.data?.status ?? null;
|
|
122
|
-
|
|
122
|
+
await bridge.terminalOutcomes;
|
|
123
|
+
const retained = options.rootSettled?.(sessionId) === true;
|
|
124
|
+
if (status === null && !retained) continue;
|
|
125
|
+
return { before, after, cancellations, root_status: status, settled: options.rootSettled ? retained : ["completed", "failed", "cancelled", "stopped", "idle"].includes(String(status).toLowerCase()) };
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
const root = await sessionInfo(bridge, sessionId).catch(() => null);
|
|
@@ -163,7 +166,7 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
163
166
|
export async function inspectSession(options) { requireJournal(options); return await withBridge(options, async (bridge, initialized) => await inspectSessionWithBridge(bridge, { ...options, initialized })); }
|
|
164
167
|
export async function inspectSessionWithBridge(bridge, options) { requireJournal(options); const sessionId = await identity(bridge, options); const evidence = await inspect(bridge, sessionId, options); evidence.tool_calls = sessionToolCalls(options, bridge, sessionId); const usage_ingest_error = await forwardUsageBestEffort(options, sessionId, evidence.usage, evidence.tool_calls); if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error; return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, ...evidence }); }
|
|
165
168
|
export async function cancelSession(options) { requireJournal(options); return await withBridge(options, async (bridge) => await cancelSessionWithBridge(bridge, options)); }
|
|
166
|
-
export async function cancelSessionWithBridge(bridge, options) { requireJournal(options); const sessionId = await identity(bridge, options); return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, ...await cancelTree(bridge, sessionId) }); }
|
|
169
|
+
export async function cancelSessionWithBridge(bridge, options) { requireJournal(options); const sessionId = await identity(bridge, options); return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, ...await cancelTree(bridge, sessionId, options) }); }
|
|
167
170
|
export async function forkSession(options) { requireJournal(options); return await withBridge(options, async (bridge) => await forkSessionWithBridge(bridge, options)); }
|
|
168
171
|
export async function forkSessionWithBridge(bridge, options) {
|
|
169
172
|
requireJournal(options); const sessionId = await identity(bridge, options); const target = options.target;
|
|
@@ -10,8 +10,9 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
|
10
10
|
import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, unlink, writeFile } from "node:fs/promises";
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
12
|
import net from "node:net";
|
|
13
|
-
import { durableDaemonDispatch, completeDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
|
|
13
|
+
import { durableDaemonDispatch, completeDaemonShutdown, commitDaemonShutdown, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon, assertDaemonDispatch, assertDaemonRestartable } from "./daemon-operations.mjs";
|
|
14
14
|
import { waitForSettlement } from "./session-settlement.mjs";
|
|
15
|
+
import { nativeExecutionReceipt } from "./native-children.mjs";
|
|
15
16
|
import { errorRecord, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
|
|
16
17
|
import { checkObservedProfile, observeModel } from "./model-observations.mjs";
|
|
17
18
|
import os from "node:os";
|
|
@@ -201,15 +202,35 @@ export class Runtime {
|
|
|
201
202
|
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 })}\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 }); } }
|
|
202
203
|
async childTree(id, seen = new Set()) {
|
|
203
204
|
if (seen.has(id)) throw new DaemonError("session_identity_mismatch", "OpenCode native topology contains a cycle");
|
|
204
|
-
seen.add(id); const direct = await this.
|
|
205
|
+
seen.add(id); const direct = await this.ownedChildren(id), nested = [];
|
|
205
206
|
for (const child of direct) {
|
|
206
207
|
if (!child.id || child.parentID !== id) throw new DaemonError("session_identity_mismatch", "OpenCode child has a foreign native parent");
|
|
207
208
|
nested.push(child, ...await this.childTree(child.id, seen));
|
|
208
209
|
}
|
|
209
210
|
return nested;
|
|
210
211
|
}
|
|
211
|
-
async
|
|
212
|
-
const
|
|
212
|
+
async ownedChildren(id) {
|
|
213
|
+
const direct = await this.client.children(id), children = new Map();
|
|
214
|
+
if (!Array.isArray(direct)) throw new DaemonError("native_child_observation_unavailable", "OpenCode child inventory is unavailable");
|
|
215
|
+
for (const child of direct) children.set(child.id, child);
|
|
216
|
+
for (const record of this.sessions.values()) if (record.parent_provider_session_id === id && !children.has(record.provider_session_id)) {
|
|
217
|
+
let child;
|
|
218
|
+
try { child = await this.client.inspect(record.provider_session_id); }
|
|
219
|
+
catch (cause) { throw new DaemonError("native_child_observation_unavailable", "Retained OpenCode child disappeared without completion proof", false, { provider_session_id: record.provider_session_id, cause: errorPayload(cause) }); }
|
|
220
|
+
if (child?.id !== record.provider_session_id) throw new DaemonError("session_identity_mismatch", "OpenCode retained child read returned another Session");
|
|
221
|
+
children.set(child.id, child);
|
|
222
|
+
}
|
|
223
|
+
for (const child of children.values()) if (!child.id || child.parentID !== id || child.fork) throw new DaemonError("session_identity_mismatch", "OpenCode child has a foreign native parent");
|
|
224
|
+
return [...children.values()];
|
|
225
|
+
}
|
|
226
|
+
async revision(id) {
|
|
227
|
+
if (!this.client.logs) return null;
|
|
228
|
+
const { watermark } = await this.client.logs(id);
|
|
229
|
+
return JSON.stringify([id, watermark]);
|
|
230
|
+
}
|
|
231
|
+
async describe(id, refreshes = 1) {
|
|
232
|
+
const before = new Map([[id, await this.revision(id)]]);
|
|
233
|
+
const [info, messages, children] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id)]);
|
|
213
234
|
const parent = info.fork ? null : info.parentID ?? null, existing = this.sessions.get(id);
|
|
214
235
|
if (info.id !== id || !existing && (!parent || !this.sessions.has(parent) || info.fork)
|
|
215
236
|
|| existing && (existing.parent_provider_session_id ?? null) !== parent) throw new DaemonError("foreign_session", "OpenCode Session is not in the retained owned tree");
|
|
@@ -219,17 +240,31 @@ export class Runtime {
|
|
|
219
240
|
if (child.fork || retained && retained.parent_provider_session_id !== child.parentID) throw new DaemonError("session_identity_mismatch", "OpenCode child ancestry changed");
|
|
220
241
|
this.retainSession(child, record.root_provider_session_id);
|
|
221
242
|
}
|
|
222
|
-
const pending = new Map();
|
|
243
|
+
const pending = new Map(), histories = new Map([[id, messages]]);
|
|
244
|
+
for (const child of children) before.set(child.id, await this.revision(child.id));
|
|
245
|
+
const statuses = await this.client.statuses();
|
|
223
246
|
for (const session of [id, ...children.map(child => child.id)]) {
|
|
247
|
+
if (session !== id) histories.set(session, await this.client.messages(session));
|
|
224
248
|
const value = this.client.pending ? await this.client.pending(session) : { inbox: [], forms: [], permissions: [] };
|
|
225
249
|
pending.set(session, { ...value, shells: this.client.shells ? (await this.client.shells(session)).filter(shell => shell.status === "running") : [] });
|
|
226
250
|
}
|
|
227
|
-
const
|
|
251
|
+
const changed = new Set(), unavailable = new Set();
|
|
252
|
+
for (const [session, seq] of before) {
|
|
253
|
+
const after = await this.revision(session);
|
|
254
|
+
if (seq === null || after === null) { unavailable.add(session); changed.add(session); }
|
|
255
|
+
else if (after !== seq) changed.add(session);
|
|
256
|
+
}
|
|
257
|
+
// Re-read one raced snapshot inside this request, without a new clock or
|
|
258
|
+
// quiet sleep. An absent fence has no reconciliation authority.
|
|
259
|
+
if (changed.size && !unavailable.size && refreshes > 0) return this.describe(id, refreshes - 1);
|
|
260
|
+
const currentMessages = session => { const history = histories.get(session); return history.slice(history.findLastIndex(message => message.info.role === "user") + 1); };
|
|
261
|
+
const busy = session => changed.has(session) || Boolean(statuses[session] && statuses[session].type !== "idle") || Object.values(pending.get(session)).some(items => items.length > 0)
|
|
262
|
+
|| currentMessages(session).some(message => (message.parts ?? []).some(part => part.type === "tool" && ["pending", "running", "streaming"].includes(part.state?.status)));
|
|
228
263
|
for (const child of children) {
|
|
229
|
-
const history =
|
|
264
|
+
const history = histories.get(child.id);
|
|
230
265
|
const index = history.findLastIndex(message => message.info.role === "user" && message.info.sessionID === child.id);
|
|
231
266
|
const terminal = index >= 0 && history.slice(index + 1).findLast(message => message.info.role === "idle" && message.info.sessionID === child.id);
|
|
232
|
-
if (!terminal || busy(child.id)) continue;
|
|
267
|
+
if (!terminal || busy(child.id)) { Object.assign(child, { status: changed.has(child.id) ? "unknown" : busy(child.id) ? "running" : "idle", native_turn_items: { observed: Boolean(terminal) && !changed.has(child.id), possible_effects: true, pending: busy(child.id) } }); continue; }
|
|
233
268
|
const outcome = terminal.info.outcome;
|
|
234
269
|
if (!["succeeded", "failed", "interrupted"].includes(outcome)) continue;
|
|
235
270
|
Object.assign(child, { status: outcome === "succeeded" ? "completed" : outcome === "failed" ? "failed" : "cancelled",
|
|
@@ -237,7 +272,7 @@ export class Runtime {
|
|
|
237
272
|
native_turn_items: { observed: true, possible_effects: true, pending: false },
|
|
238
273
|
...(terminal.info.error ? { provider_error: safeProviderError(terminal.info.error) } : {}) });
|
|
239
274
|
}
|
|
240
|
-
const unsettledChildren = children.filter(child => busy(child.id)).map(child => child.id), status = busy(id) ? "busy" : "idle";
|
|
275
|
+
const unsettledChildren = children.filter(child => busy(child.id)).map(child => child.id), status = changed.has(id) ? "unknown" : busy(id) ? "busy" : "idle";
|
|
241
276
|
const lastAssistant = messages.findLast(message => message.info?.role === "assistant");
|
|
242
277
|
const inputIndex = messages.findLastIndex(message => message.info.role === "user");
|
|
243
278
|
const idle = messages.slice(inputIndex + 1).findLast(message => message.info.role === "idle");
|
|
@@ -245,18 +280,20 @@ export class Runtime {
|
|
|
245
280
|
await this.observeModels(id, messages, parent);
|
|
246
281
|
this.journaledTools ??= new Map();
|
|
247
282
|
if (this.state.config.journal) journalToolObservations(this.state.config.journal, messages.flatMap(message => (message.parts ?? []).map(part => openCodeToolObservation(part, id, message.info?.id))), this.journaledTools);
|
|
248
|
-
|
|
283
|
+
const receipt = { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? info.model?.providerID ?? null, model: lastAssistant?.info?.modelID ?? info.model?.id ?? null, reasoning: lastAssistant?.info?.variant ?? info.model?.variant ?? "default", mode: info.agent ?? null },
|
|
249
284
|
assistant_text: (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""), info, status,
|
|
250
285
|
route_conformance: { matches: [info, ...children].every(session => session.directory && path.resolve(session.directory) === path.resolve(this.state.config.cwd)), expected: this.state.config.cwd, actual: info.directory ?? null, actual_provenance: "native_session_readback" },
|
|
251
286
|
route_violations: [info, ...children].filter(session => !session.directory || path.resolve(session.directory) !== path.resolve(this.state.config.cwd)).map(session => ({ provider_session_id: session.id, expected: this.state.config.cwd, actual: session.directory ?? null, actual_provenance: "native_session_readback" })),
|
|
252
287
|
settled: status === "idle" && !unsettledChildren.length, unsettled_children: unsettledChildren,
|
|
253
288
|
tree_session_ids: treeIds, unsettled_sessions: treeIds.filter(busy),
|
|
254
|
-
tree_observations: treeIds.map(session => ({ provider_session_id: session, status: busy(session) ? "running" : "idle", settled: !busy(session) })),
|
|
289
|
+
tree_observations: treeIds.map(session => ({ provider_session_id: session, status: changed.has(session) ? "unknown" : busy(session) ? "running" : "idle", settled: !busy(session) })),
|
|
290
|
+
...(changed.size ? { native_settlement: { confirmed: false, state: "unproven", reason: "native_revision_fence_unavailable_or_changed" } } : {}),
|
|
255
291
|
terminal_turn_id: idle && inputIndex >= 0 ? messages[inputIndex].info.id : null,
|
|
256
292
|
terminal_status: idle?.info.outcome === "failed" ? "failed" : idle?.info.outcome === "succeeded" ? "completed" : idle?.info.outcome ?? null,
|
|
257
293
|
native_turn_items: { observed: Boolean(idle), possible_effects: messages.slice(inputIndex + 1).some(message => message.parts.some(part => part.type === "tool")),
|
|
258
|
-
pending: busy(id)
|
|
294
|
+
pending: busy(id) },
|
|
259
295
|
children, pending: pending.get(id), usage: usageSnapshot(messages, id), history_sha256: historyDigest(messages), message_count: messages.length };
|
|
296
|
+
return nativeExecutionReceipt(receipt, { daemonId: this.state.daemon_id, revision: before.get(id) ?? 0 });
|
|
260
297
|
}
|
|
261
298
|
async observeModels(id, messages, parent = null) {
|
|
262
299
|
this.modelMessages ??= new Set();
|
|
@@ -301,18 +338,24 @@ export class Runtime {
|
|
|
301
338
|
async cancelTree(id, seen = new Set()) {
|
|
302
339
|
if (!this.sessions.has(id)) throw new DaemonError("foreign_session", "cannot cancel a Session without daemon ownership");
|
|
303
340
|
if (seen.has(id)) return [];
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
341
|
+
const retained = this.sessions.get(id), errors = [];
|
|
342
|
+
let native;
|
|
343
|
+
try { native = await this.client.inspect(id); }
|
|
344
|
+
catch (error) { errors.push({ session_id: id, phase: "inspect", ...errorPayload(error) }); }
|
|
345
|
+
if (native) {
|
|
346
|
+
if (native.id !== id || (retained.parent_provider_session_id ?? null) !== (native.fork ? null : native.parentID ?? null)) throw new DaemonError("session_identity_mismatch", "OpenCode cancellation Session ancestry changed");
|
|
347
|
+
try { this.retainSession(native, retained.root_provider_session_id ?? id); }
|
|
348
|
+
catch (error) { errors.push({ session_id: id, phase: "retain", ...errorPayload(error) }); }
|
|
349
|
+
}
|
|
307
350
|
seen.add(id);
|
|
308
|
-
|
|
309
|
-
try { children = await this.
|
|
351
|
+
let children = [];
|
|
352
|
+
try { children = await this.ownedChildren(id); } catch (error) { errors.push({ session_id: id, phase: "children", ...errorPayload(error) });
|
|
353
|
+
children = [...this.sessions.values()].filter(record => record.parent_provider_session_id === id).map(record => ({ id: record.provider_session_id, parentID: id, directory: record.cwd })); }
|
|
310
354
|
for (const child of children) {
|
|
311
355
|
if (child.parentID !== id || child.fork || this.sessions.has(child.id) && this.sessions.get(child.id).parent_provider_session_id !== id) {
|
|
312
356
|
errors.push({ session_id: child.id, phase: "identity", code: "session_identity_mismatch" }); continue;
|
|
313
357
|
}
|
|
314
|
-
this.retainSession(child, this.sessions.get(id)?.root_provider_session_id ?? id);
|
|
315
|
-
try { errors.push(...await this.cancelTree(child.id, seen)); }
|
|
358
|
+
try { this.retainSession(child, this.sessions.get(id)?.root_provider_session_id ?? id); errors.push(...await this.cancelTree(child.id, seen)); }
|
|
316
359
|
catch (error) { errors.push({ session_id: child.id, phase: "cancel_child", ...errorPayload(error) }); }
|
|
317
360
|
}
|
|
318
361
|
try { await this.client.abort(id); } catch (error) { errors.push({ session_id: id, phase: "abort", ...errorPayload(error) }); }
|
|
@@ -467,7 +510,10 @@ export class Runtime {
|
|
|
467
510
|
},
|
|
468
511
|
...(params.cancelTree ? { cancel: id => this.cancelTree(id) } : {})
|
|
469
512
|
});
|
|
470
|
-
|
|
513
|
+
// Fresh native observation, not a prior productive result, owns the idle proof.
|
|
514
|
+
commitDaemonShutdown(this.paths.dir);
|
|
515
|
+
await this.persist({ active_tree: false });
|
|
516
|
+
return { clean: true, settled: true, native_settled: true };
|
|
471
517
|
}, [
|
|
472
518
|
["provider", () => stopProcessGroup(this.child)],
|
|
473
519
|
["provider_resource", () => finishDaemonProcess(this.state.config, this.providerProcess)],
|
|
@@ -14,6 +14,33 @@ const lost = (message, details) => new OpenCodeError("operation_observation_lost
|
|
|
14
14
|
export const requestDigest = value => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
15
15
|
export const nativeModel = ({ provider, model, variant }) => ({ providerID: provider, id: model, ...(variant && variant !== "default" ? { variant } : {}) });
|
|
16
16
|
|
|
17
|
+
/** Native finite session.log includes a control watermark, not a domain event. */
|
|
18
|
+
export function decodeFiniteLog(items, sessionId, after = 0) {
|
|
19
|
+
const fail = (reason, field, unproven = false) => { throw new OpenCodeError(unproven ? "opencode_observation_unproven" : "opencode_observation_corrupt", "OpenCode finite log has no trustworthy revision", false,
|
|
20
|
+
{ native_outcome: "unknown", session_id: sessionId, cursor: after, reason, field }); };
|
|
21
|
+
if (!Number.isSafeInteger(after) || after < 0) fail("invalid_cursor", "after");
|
|
22
|
+
if (!Array.isArray(items)) fail("invalid_batch", "items");
|
|
23
|
+
const events = []; let watermark, cursor = after;
|
|
24
|
+
for (const item of items) {
|
|
25
|
+
if (watermark !== undefined) fail("frame_after_sync", "type");
|
|
26
|
+
const sync = item?.type === "log.synced", metadata = sync ? item : item?.durable;
|
|
27
|
+
if (!metadata) fail("missing_metadata", "durable");
|
|
28
|
+
if (metadata.aggregateID !== sessionId) fail("foreign_aggregate", sync ? "aggregateID" : "durable.aggregateID");
|
|
29
|
+
if (!Number.isSafeInteger(metadata.seq) || metadata.seq < 0) fail("invalid_sequence", sync ? "seq" : "durable.seq", sync && metadata.seq === undefined);
|
|
30
|
+
if (sync) {
|
|
31
|
+
if (metadata.seq < cursor) fail("watermark_behind_cursor", "seq");
|
|
32
|
+
watermark = metadata.seq;
|
|
33
|
+
} else {
|
|
34
|
+
if (typeof item.type !== "string" || !item.type.startsWith("session.") || !item.data || typeof item.data !== "object") fail("invalid_domain_event", "type/data");
|
|
35
|
+
if (item.data.sessionID !== undefined && item.data.sessionID !== sessionId) fail("foreign_session", "data.sessionID");
|
|
36
|
+
if (metadata.seq <= cursor) fail("non_increasing_sequence", "durable.seq");
|
|
37
|
+
cursor = metadata.seq; events.push(item);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (watermark === undefined) fail("missing_sync", "log.synced", true);
|
|
41
|
+
return { events, watermark };
|
|
42
|
+
}
|
|
43
|
+
|
|
17
44
|
/** info/parts is our normalized projection, not a second v1 native protocol. */
|
|
18
45
|
export function normalizeMessage(message, sessionID) {
|
|
19
46
|
if (!message?.id || !message.type) throw lost("OpenCode message has no native identity");
|
|
@@ -124,7 +151,7 @@ export class OpenCodeClient {
|
|
|
124
151
|
try { for await (const event of api.session.log({ sessionID: id, after, follow: false }, { signal: controller.signal })) events.push(event); }
|
|
125
152
|
catch (cause) { throw Object.assign(lost("OpenCode durable log observation failed", { session_id: id }), { cause }); }
|
|
126
153
|
finally { clearInterval(timer); }
|
|
127
|
-
return events;
|
|
154
|
+
return decodeFiniteLog(events, id, after);
|
|
128
155
|
}
|
|
129
156
|
async abort(id) {
|
|
130
157
|
// Queued input must not become fresh work during interruption cleanup.
|
|
@@ -221,15 +248,15 @@ export class OpenCodeClient {
|
|
|
221
248
|
for (;;) {
|
|
222
249
|
try {
|
|
223
250
|
await checkHookFailures?.();
|
|
224
|
-
const events = await this.logs(id, state.cursor);
|
|
251
|
+
const { events, watermark } = await this.logs(id, state.cursor);
|
|
225
252
|
let delivered = state.delivered === true, terminal = state.terminal;
|
|
226
253
|
for (const event of events) {
|
|
227
|
-
if (!Number.isInteger(event.durable?.seq) || event.durable.seq <= state.cursor) continue;
|
|
228
254
|
if (event.type === "session.inbox.delivered" && event.data.inboxID === messageId) { delivered = true; terminal = null; }
|
|
229
255
|
if (delivered && event.type === "session.execution.started") terminal = null;
|
|
230
256
|
if (delivered && ["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted"].includes(event.type)) terminal = { type: event.type, error: event.data.error };
|
|
231
257
|
state = { ...state, cursor: event.durable.seq, delivered, terminal }; await save?.(state);
|
|
232
258
|
}
|
|
259
|
+
if (state.cursor !== watermark) { state = { ...state, cursor: watermark }; await save?.(state); }
|
|
233
260
|
let runningTools = false;
|
|
234
261
|
const observe = async (session, visited = new Set()) => {
|
|
235
262
|
if (visited.has(session)) throw lost("OpenCode topology contains a cycle"); visited.add(session); known.add(session);
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from "./dd-zcode.mjs";
|
|
31
31
|
|
|
32
32
|
const REQUEST_SCHEMA = "dd-zcode/daemon-request@1";
|
|
33
|
-
import { settledNativeTree } from "./native-children.mjs";
|
|
33
|
+
import { settledNativeTree, nativeExecutionReceipt } from "./native-children.mjs";
|
|
34
34
|
const RESPONSE_SCHEMA = "dd-zcode/daemon-response@1";
|
|
35
35
|
const STATE_SCHEMA = "dd-zcode/daemon-state@1";
|
|
36
36
|
// ACP silence starts diagnostics; it is not a provider-failure deadline.
|
|
@@ -549,7 +549,7 @@ export class DaemonRuntime {
|
|
|
549
549
|
for (const receipt of [previous, closed]) {
|
|
550
550
|
if (receipt?.close?.closed === true && receipt.settled === true
|
|
551
551
|
&& receipt.topology?.complete === true && receipt.topology.root_session_id === params.sessionId
|
|
552
|
-
&& Array.isArray(receipt.topology.child_session_ids)) return receipt;
|
|
552
|
+
&& Array.isArray(receipt.topology.child_session_ids)) return nativeExecutionReceipt(receipt, { daemonId: this.state.daemon_id, generation: this.controlGeneration ?? 0 });
|
|
553
553
|
}
|
|
554
554
|
const known = [...new Set([...topology.child_session_ids, ...(previous?.topology?.child_session_ids ?? [])])];
|
|
555
555
|
const emptyRoot = this.neverPromptedRootProof(params.sessionId);
|
|
@@ -559,7 +559,7 @@ export class DaemonRuntime {
|
|
|
559
559
|
this.state.settlement_observations ??= {};
|
|
560
560
|
this.state.settlement_observations[params.sessionId] = observation;
|
|
561
561
|
await this.persist();
|
|
562
|
-
return observation;
|
|
562
|
+
return nativeExecutionReceipt(observation, { daemonId: this.state.daemon_id, generation: this.controlGeneration ?? 0 });
|
|
563
563
|
}
|
|
564
564
|
const adapter = params.adapterSessionId ?? params.sessionId;
|
|
565
565
|
if (!this.liveAdapters.has(adapter) && this.activeProductive) {
|
|
@@ -584,8 +584,8 @@ export class DaemonRuntime {
|
|
|
584
584
|
const childIds = [...new Set([...(this.sessions.get(result.provider_session_id)?.known_child_session_ids ?? []), ...zcodeChildSessionIds(nativeChildren)])];
|
|
585
585
|
const childStates = new Map([...(nativeChildren?.ended?.items ?? nativeChildren?.completed ?? []), ...(nativeChildren?.running ?? []).map(child => ({ ...child, status: "running" }))].map(child => [child.childSessionId ?? child.sessionId, child.status]));
|
|
586
586
|
const rootSettled = !this.activeProductive && failure?.provider_session_id === result.provider_session_id && (result.evidence ?? result).read?.projection?.status === "idle";
|
|
587
|
-
return { ...result, settled, ...settledNativeTree(result.provider_session_id, childIds.map(childSessionId => ({ childSessionId, status: childStates.get(childSessionId) ?? "unknown" })), settled, rootSettled),
|
|
588
|
-
...(failure?.provider_session_id === result.provider_session_id ? { terminal_turn_id: String(failure.request_id) } : {}) };
|
|
587
|
+
return nativeExecutionReceipt({ ...result, settled, ...settledNativeTree(result.provider_session_id, childIds.map(childSessionId => ({ childSessionId, status: childStates.get(childSessionId) ?? "unknown" })), settled, rootSettled),
|
|
588
|
+
...(failure?.provider_session_id === result.provider_session_id ? { terminal_turn_id: String(failure.request_id) } : {}) }, { daemonId: this.state.daemon_id, generation: this.controlGeneration ?? 0 });
|
|
589
589
|
}
|
|
590
590
|
if (operation === "session.cancel") {
|
|
591
591
|
if (!this.sessions.has(params.sessionId)) throw new DaemonError("session_identity_mismatch", "cancellation Session is not owned by this daemon");
|
|
@@ -604,6 +604,7 @@ export class DaemonRuntime {
|
|
|
604
604
|
return result;
|
|
605
605
|
}
|
|
606
606
|
if (operation === "daemon.stop") return await completeDaemonShutdown(this.paths.dir, this.state, patch => this.persist(patch), () => this.prepareStop(params.cancelTree === true), [
|
|
607
|
+
["provider", () => this.bridge.close?.()],
|
|
607
608
|
["provider_resource", () => finishDaemonProcess(this.state.config, this.state.provider_process)],
|
|
608
609
|
["daemon_resource", () => finishDaemonProcess(this.state.config, this.state.resource_process)]
|
|
609
610
|
]);
|
|
@@ -691,7 +692,7 @@ export class DaemonRuntime {
|
|
|
691
692
|
try { if (!bridgeClosed && typeof this.bridge.close === "function") await this.bridge.close(); }
|
|
692
693
|
catch (error) { throw new DaemonError(error.code ?? "provider_process_stop_incomplete", error.message ?? String(error), false, error.details); }
|
|
693
694
|
await this.persist({ active_tree: false, shutdown_state: "stopping", active_operation: null, active_request: null, physical_settlement: { confirmed_at: new Date().toISOString(), bridge_pid: this.bridge.child?.pid ?? null } });
|
|
694
|
-
return { daemon_id: this.state.daemon_id, stopped: true, clean: true, physical_settlement: this.state.physical_settlement, _shutdown: true };
|
|
695
|
+
return { daemon_id: this.state.daemon_id, stopped: true, clean: true, settled: true, native_settled: true, physical_settlement: this.state.physical_settlement, _shutdown: true };
|
|
695
696
|
}
|
|
696
697
|
|
|
697
698
|
}
|
|
@@ -3,7 +3,7 @@ import { ToolObservations, acpToolObservation, zcodePhysicalSession } from "./to
|
|
|
3
3
|
import { hookBudget, invokeNativeHook } from "./native-hook-command.mjs";
|
|
4
4
|
import { parseLifecycleCommand, continuationCommand, commandOption } from "./lifecycle-command.mjs";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { createReadStream } from 'node:fs';
|
|
8
8
|
import { publishSetup, SETUP_STARTUP_MS } from './setup-progress.mjs';
|
|
9
9
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
@@ -141,6 +141,10 @@ export class AcpBridge {
|
|
|
141
141
|
this.expiredRequests = new Map();
|
|
142
142
|
this.sessionActivity = new Map();
|
|
143
143
|
this.notifications = Promise.resolve();
|
|
144
|
+
this.terminalOutcomes = Promise.resolve();
|
|
145
|
+
this.terminalRequests = new Map();
|
|
146
|
+
this.connectionId = randomUUID();
|
|
147
|
+
this.receivedSequence = 0;
|
|
144
148
|
this.lifecycleNotifications = new Set();
|
|
145
149
|
this.notificationError = null;
|
|
146
150
|
this.bridgeStderr = "";
|
|
@@ -202,16 +206,27 @@ export class AcpBridge {
|
|
|
202
206
|
receive(line) {
|
|
203
207
|
let message;
|
|
204
208
|
try { message = JSON.parse(line); } catch { this.journal.write("malformed", { line }); return; }
|
|
209
|
+
const observation = { connection_id: this.connectionId, sequence: ++this.receivedSequence, productive: isProductiveSessionNotification(message) };
|
|
205
210
|
const request = message.id !== undefined ? this.pending.get(message.id) : null;
|
|
206
211
|
const expired = message.id !== undefined ? this.expiredRequests.get(message.id) : null;
|
|
207
212
|
const isResponse = message.id !== undefined && !message.method;
|
|
208
213
|
const rejected = isResponse && this.rejectedHookRequests?.get(message.id);
|
|
209
|
-
if (rejected) {
|
|
210
|
-
this.
|
|
211
|
-
const terminal = { request_id: message.id, method:
|
|
214
|
+
if (rejected || isResponse && (request?.method === "session/prompt" || this.terminalRequests.has(message.id))) {
|
|
215
|
+
const owner = rejected ?? request ?? this.terminalRequests.get(message.id);
|
|
216
|
+
const terminal = { request_id: message.id, method: owner.method, session_id: owner.sessionId, terminal_response: message,
|
|
217
|
+
connection_id: this.connectionId, operation_id: owner.operationId ?? null, generation: owner.generation ?? null, native_sequence: observation.sequence };
|
|
218
|
+
this.terminalOutcomes = this.terminalOutcomes.then(async () => {
|
|
219
|
+
await this.journal.write(rejected ? "native_terminal_after_hook_failure" : "native_terminal_outcome", terminal);
|
|
220
|
+
await this.journal.flush();
|
|
221
|
+
await this.options.onTerminalOutcome?.(terminal);
|
|
222
|
+
this.terminalRequests.set(message.id, owner);
|
|
223
|
+
while (this.terminalRequests.size > 128) this.terminalRequests.delete(this.terminalRequests.keys().next().value);
|
|
224
|
+
if (rejected) this.rejectedHookRequests.delete(message.id);
|
|
225
|
+
});
|
|
226
|
+
void this.terminalOutcomes.catch(storage => { this.terminalProjectionError ??= storage; if (rejected) rejected.error.cleanup_error ??= errorRecord(storage); });
|
|
227
|
+
if (rejected) {
|
|
212
228
|
rejected.error.details = { ...rejected.error.details, native_terminal_after_hook_failure: terminal };
|
|
213
|
-
|
|
214
|
-
catch (storage) { rejected.error.cleanup_error ??= errorRecord(storage); }
|
|
229
|
+
}
|
|
215
230
|
}
|
|
216
231
|
if (!isResponse || request) this.journal.writeInbound(message, request?.method);
|
|
217
232
|
if (typeof message.params?.sessionId === "string" && isProductiveSessionNotification(message)) this.observeActivity(message.params.sessionId, message.params.update);
|
|
@@ -305,7 +320,7 @@ export class AcpBridge {
|
|
|
305
320
|
return;
|
|
306
321
|
}
|
|
307
322
|
if (message.method) this.notifications = this.notifications
|
|
308
|
-
.then(async () => { await this.observeModelUpdate(message); await this.options.onNotification?.(message); })
|
|
323
|
+
.then(async () => { await this.observeModelUpdate(message); await this.options.onNotification?.(message, observation); })
|
|
309
324
|
.catch(async (error) => {
|
|
310
325
|
const failure = { code: error.code ?? "notification_observation_failed", message: error.message, ...(error.details ? { details: error.details } : {}), session_id: message.params?.sessionId ?? null, tool_call_id: message.params?.update?.toolCallId ?? null, event_time: now(), delivery_stage: "acp_evidence", completeness: "incomplete" };
|
|
311
326
|
try { await this.journal.write("notification_failure", failure); }
|
|
@@ -405,6 +420,7 @@ export class AcpBridge {
|
|
|
405
420
|
}
|
|
406
421
|
|
|
407
422
|
request(method, params = {}, timeoutMs = 30_000, { idleSessionId = null, idleTimeoutMs = null, inactivityMode = "reject" } = {}) {
|
|
423
|
+
if (method === "session/prompt" && (this.rejectedHookRequests?.size ?? 0) + [...this.pending.values()].filter(request => request.method === "session/prompt").length >= 32) return Promise.reject(Object.assign(new Error("Unreconciled native terminal evidence limit reached"), { code: "native_terminal_evidence_pending" }));
|
|
408
424
|
if (timeoutMs !== null) validateObservationDuration(timeoutMs);
|
|
409
425
|
if (idleTimeoutMs !== null) validateObservationDuration(idleTimeoutMs);
|
|
410
426
|
if ((this.lifecycleFailure || this.notificationError?.code === "profile_integrity_violation") && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.lifecycleFailure ?? this.notificationError);
|
|
@@ -467,9 +483,19 @@ export class AcpBridge {
|
|
|
467
483
|
void this.journal.write("inactivity_observation_failure", { method, request_id: id, session_id: idleSessionId, error: { code: error?.code ?? "inactivity_observation_failed", message: error?.message ?? String(error) } }).catch(() => {});
|
|
468
484
|
});
|
|
469
485
|
}, Math.max(10, Math.min(1_000, Math.floor(validIdleTimeout / 4)))) : null;
|
|
470
|
-
this.pending.set(id, { id, resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null, dispatchedAt: Date.now(), publish: operationProgressReporter() });
|
|
471
|
-
|
|
472
|
-
|
|
486
|
+
this.pending.set(id, { id, resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null, operationId: this.dispatchOperationId ?? process.env.DD_EVAL_OPERATION_ID ?? null, generation: this.dispatchGeneration ?? null, dispatchedAt: Date.now(), publish: operationProgressReporter() });
|
|
487
|
+
if (method === "session/prompt") {
|
|
488
|
+
void (async () => {
|
|
489
|
+
const dispatch = { operation_id: this.pending.get(id)?.operationId, session_id: params.sessionId, native_request_id: id,
|
|
490
|
+
connection_id: this.connectionId, generation: this.pending.get(id)?.generation };
|
|
491
|
+
await this.journal.write("native_dispatch_prepared", dispatch);
|
|
492
|
+
await this.journal.flush();
|
|
493
|
+
await this.options.onPromptDispatch?.(dispatch);
|
|
494
|
+
if (!this.pending.has(id) || this.pending.get(id).timedOut || this.lifecycleFailure) return;
|
|
495
|
+
this.send({ jsonrpc: "2.0", id, method, params });
|
|
496
|
+
await this.journal.write("native_dispatch", dispatch);
|
|
497
|
+
})().catch(finish);
|
|
498
|
+
} else this.send({ jsonrpc: "2.0", id, method, params });
|
|
473
499
|
});
|
|
474
500
|
}
|
|
475
501
|
|
|
@@ -488,14 +514,14 @@ export class AcpBridge {
|
|
|
488
514
|
rejectAll(error) {
|
|
489
515
|
for (const [id, pending] of this.pending) {
|
|
490
516
|
if (this.notificationError === error && pending.method === "session/prompt") {
|
|
491
|
-
this.rejectedHookRequests ??= new Map(); this.rejectedHookRequests.set(id, { method: pending.method, sessionId: pending.sessionId, error });
|
|
492
|
-
if (this.rejectedHookRequests.size > 32) this.rejectedHookRequests.delete(this.rejectedHookRequests.keys().next().value);
|
|
517
|
+
this.rejectedHookRequests ??= new Map(); this.rejectedHookRequests.set(id, { method: pending.method, sessionId: pending.sessionId, operationId: pending.operationId, generation: pending.generation, error });
|
|
493
518
|
}
|
|
494
519
|
clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); pending.reject(error);
|
|
495
520
|
}
|
|
496
521
|
this.pending.clear();
|
|
497
522
|
}
|
|
498
523
|
async flush() {
|
|
524
|
+
await this.terminalOutcomes;
|
|
499
525
|
if (this.notificationError) throw this.notificationError;
|
|
500
526
|
try { await Promise.all([...this.lifecycleNotifications]); await this.notifications; await this.journal.flush(); }
|
|
501
527
|
catch (error) { throw this.notificationError ?? error; }
|
|
@@ -507,6 +533,7 @@ export class AcpBridge {
|
|
|
507
533
|
// Draining them must not relabel a successful physical shutdown as failed.
|
|
508
534
|
return await closeBridgeProcess(this, async () => {
|
|
509
535
|
await Promise.all([this.nativeDrain, this.diagnostics]);
|
|
536
|
+
await this.terminalOutcomes;
|
|
510
537
|
await Promise.allSettled([...this.lifecycleNotifications, this.notifications]);
|
|
511
538
|
if (this.notificationError) await hookBudget(1_000, null).run(() => this.journal.flush(), "failure_drain");
|
|
512
539
|
else await this.journal.flush();
|
|
@@ -964,6 +991,7 @@ export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContrac
|
|
|
964
991
|
native_nested_children: false, reason: qualified ? null : "native_event_contract_unqualified" };
|
|
965
992
|
}
|
|
966
993
|
|
|
994
|
+
let doctorDigestScanOrdinal = 0;
|
|
967
995
|
export async function doctor(options = {}) {
|
|
968
996
|
const zcodeAcp = await commandOutput(options.bin, ["--version"]);
|
|
969
997
|
const zcodeAcpCommit = await commandOutput(options.bin, ["--dd-harness-commit"]);
|
|
@@ -971,11 +999,13 @@ export async function doctor(options = {}) {
|
|
|
971
999
|
const zcodePath = options.zcodePath ?? process.env.ZCODE_BIN ?? process.env.ZCODE_PATH ?? "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs";
|
|
972
1000
|
const zcode = await commandOutput(zcodePath, ["--version"]);
|
|
973
1001
|
const hash = createHash('sha256'); let bytes = 0;
|
|
1002
|
+
const scan = `zcode-digest:${zcodePath}:scan-${++doctorDigestScanOrdinal}`;
|
|
1003
|
+
publishSetup('probe_started', scan);
|
|
974
1004
|
for await (const chunk of createReadStream(zcodePath)) {
|
|
975
1005
|
hash.update(chunk); bytes += chunk.length;
|
|
976
|
-
if (bytes % (4 * 1024 * 1024) < chunk.length) publishSetup('identity_digest_progress', bytes);
|
|
1006
|
+
if (bytes % (4 * 1024 * 1024) < chunk.length) publishSetup('identity_digest_progress', `${scan}:${bytes}`);
|
|
977
1007
|
}
|
|
978
|
-
const sha256 = hash.digest('hex'); publishSetup('identity_digest_completed');
|
|
1008
|
+
const sha256 = hash.digest('hex'); publishSetup('identity_digest_completed', scan);
|
|
979
1009
|
const qualification = zcodeLifecycleQualification(sha256, zcodeAcpCommit, ddHarness);
|
|
980
1010
|
return { compatible: qualification.status === "qualified", contract_supported: supportedZcodeHarnessContract(ddHarness), lifecycle_qualification: qualification, observed_runtime: { zcode, zcode_acp: zcodeAcp, zcode_acp_commit: zcodeAcpCommit, dd_harness_contract: ddHarness } };
|
|
981
1011
|
}
|
|
@@ -60,7 +60,7 @@ export function delegationContract(harness) {
|
|
|
60
60
|
return `dd-flow/delegation/${key}@${["codex", "opencode"].includes(key) ? 2 : 1}`;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. If a wrapper returns 'Script running with cell ID …', use that wrapper's wait tool to finish the same cell before expecting an inner shell session handle; do not execute the command again. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending.";
|
|
63
|
+
export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. If a wrapper returns 'Script running with cell ID …', use that wrapper's wait tool to finish the same cell before expecting an inner shell session handle; do not execute the command again. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending. When the result provides prompt_delivery, read its complete plain Markdown file at prompt_delivery.path before working; it contains the current instructions, unlike the historical prompt_path. If delivery publication is pending, stop for reconciliation without repeating the accepted command.";
|
|
64
64
|
|
|
65
65
|
export const lifecycleRetryInstruction = `${lifecycleCommandCompletionInstruction} A failure is not permission to replay a lifecycle command. For proven effect=no_effect with recoverable=true and retry_command, correct the input and execute that exact issued successor. A check failure can retain receipts while leaving Work running: correct the evidenced cause first, then use the CLI's exact retry_command for an intentional fresh check attempt; never replay a check merely because Work is unsettled. If a successful CODE finish returns outcome=verification_required, inspect the retained receipts, update its verification file, and use only next.finish_command: the Stage is still running and the previous invocation ID is consumed. If continuation.kind=repair_required, or a successful result has outcome=repair_required and a repair Work ID, end this Turn for the registered repair child; use any retry_command only after that repair settles and verification is refreshed. Unknown effects require reconciliation, never replay. A committed effect without an explicit continuation is not retryable. A native shell launch refusal before the CLI starts permits correcting only that shell-form error and retrying the same exact command. Otherwise report the primary error and stop; never invent IDs, call hooks directly, or run status as a substitute.`;
|
|
66
66
|
|
|
@@ -6,22 +6,23 @@ import { writeJsonAtomic } from "./runner-events.mjs";
|
|
|
6
6
|
import { reportedError } from "./operation-errors.mjs";
|
|
7
7
|
import { ObservationClock } from "./observation-clock.mjs";
|
|
8
8
|
import { runtimeProcess } from "./managed-daemon.mjs";
|
|
9
|
+
import { nativeShutdownConfirmed } from "./native-children.mjs";
|
|
9
10
|
|
|
10
|
-
function cleanShutdownConfirmed(state) {
|
|
11
|
+
async function cleanShutdownConfirmed(root, state) {
|
|
11
12
|
const proof = state.shutdown;
|
|
13
|
+
let fence;
|
|
14
|
+
try { fence = JSON.parse(await readFile(path.join(root, 'operation-fence.json'), 'utf8')); }
|
|
15
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
12
16
|
return state.shutdown_state === "clean" && state.active_tree === false
|
|
13
|
-
&&
|
|
14
|
-
&& proof
|
|
15
|
-
&& proof.required_phases.length >= 3 && proof.required_phases.includes("tree")
|
|
16
|
-
&& proof.required_phases.includes("daemon_resource")
|
|
17
|
-
&& proof.required_phases.every(phase => typeof phase === "string" && proof.phases?.[phase] === true);
|
|
17
|
+
&& typeof state.daemon_id === 'string' && fence?.daemon_id === state.daemon_id && Number.isSafeInteger(fence.generation) && fence.generation >= 0
|
|
18
|
+
&& nativeShutdownConfirmed(proof, { daemonId: state.daemon_id, generation: fence.generation });
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/** Explicit same-Session restart retains native stores and the old daemon
|
|
21
22
|
* receipt; it does not turn a terminal directory into a new execution. */
|
|
22
23
|
export async function authorizeRetainedDaemonResume(root, previous, sessionId, config) {
|
|
23
24
|
const sessions = [...(previous.sessions ?? []), ...(previous.root_session_id ? [previous.root_session_id] : [])];
|
|
24
|
-
if (!cleanShutdownConfirmed(previous) || typeof sessionId !== "string" || !sessions.some(session => (typeof session === "string" ? session : session.provider_session_id) === sessionId)) throw Object.assign(new Error("terminal daemon restart requires a retained, settled Session identity and complete shutdown proof"), { code: "daemon_state_terminal" });
|
|
25
|
+
if (!await cleanShutdownConfirmed(root, previous) || typeof sessionId !== "string" || !sessions.some(session => (typeof session === "string" ? session : session.provider_session_id) === sessionId)) throw Object.assign(new Error("terminal daemon restart requires a retained, settled Session identity and complete shutdown proof"), { code: "daemon_state_terminal" });
|
|
25
26
|
for (const key of ["cwd", "projectRoot", "ddFlowHome", "ddFlowBin", "provider", "model", "reasoning", "mode", "variant", "agent", "bin", "noFlow"]) {
|
|
26
27
|
if ((previous.config?.[key] ?? null) !== (config[key] ?? null)) throw Object.assign(new Error(`retained daemon configuration changed: ${key}`), { code: "daemon_config_mismatch" });
|
|
27
28
|
}
|
|
@@ -53,7 +54,7 @@ export async function recoverySettlement(driversRoot) {
|
|
|
53
54
|
}
|
|
54
55
|
else if (entry.name === "daemon.json") {
|
|
55
56
|
const state = JSON.parse(await readFile(file, "utf8"));
|
|
56
|
-
if (!cleanShutdownConfirmed(state)) throw Object.assign(new Error("provider tree has no complete clean shutdown receipt"), { code: "recovery_settlement_unconfirmed" });
|
|
57
|
+
if (!await cleanShutdownConfirmed(directory, state)) throw Object.assign(new Error("provider tree has no complete clean shutdown receipt"), { code: "recovery_settlement_unconfirmed" });
|
|
57
58
|
await assertDaemonReplaceable(directory);
|
|
58
59
|
await reconcileDriverReplies(directory);
|
|
59
60
|
if (state.config?.resourceHome && state.resource_process?.id) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Stable model-facing reference for persisted project, RUN and Work IDs. */
|
|
2
|
+
export function shortEntityReference(id) {
|
|
3
|
+
return /^(?:PRJ|RUN|WRK)-\d{3,}(?:-|$)/.exec(id)?.[0]?.replace(/-$/, "") ?? id;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Accepted spellings of one already-resolved entity. Wrong full slugs stay distinct. */
|
|
7
|
+
export function entityReferenceVariants(id) {
|
|
8
|
+
return [...new Set([id, shortEntityReference(id)])];
|
|
9
|
+
}
|
|
@@ -10,7 +10,7 @@ export function heartbeatDaemonProcess(config: RuntimeConfig, record: ProcessBin
|
|
|
10
10
|
export function finishDaemonProcess(config: RuntimeConfig, record: ProcessBinding | null | undefined, state?: string, reason?: string): Promise<void>;
|
|
11
11
|
export function cleanupFailedDaemonStart(config: RuntimeConfig, record: ProcessBinding | null, child: ChildProcess | null, original: Error & { details?: Record<string, unknown> }, readState: () => Promise<Record<string, unknown>>, daemonId: string): Promise<void>;
|
|
12
12
|
export function completeBudgetOperation(stateDir: string, request: { id: string; operation: string; params?: { sessionId?: string } }, result: unknown, observe?: () => Promise<Record<string, unknown>>): Promise<Record<string, unknown>>;
|
|
13
|
-
export function stopProcessGroup(child: ChildProcess | null | undefined, graceMs?: number, options?: { detached?: boolean; settlementMs?: number }): Promise<void>;
|
|
13
|
+
export function stopProcessGroup(child: ChildProcess | { pid: number; started: string } | null | undefined, graceMs?: number, options?: { detached?: boolean; settlementMs?: number; started?: string }): Promise<void>;
|
|
14
14
|
export function assertStartupChild(child: Pick<ChildProcess, "pid" | "exitCode" | "signalCode">, expected: Record<string, unknown>, observed: Record<string, unknown> | null, log: string): void;
|
|
15
15
|
export function retainStartupFailure(input: { state: Record<string, unknown>; publish: (state: Record<string, unknown>) => Promise<unknown>; log: string; error: Error & { code?: string; details?: Record<string, unknown> } }): Promise<void>;
|
|
16
16
|
export function startManagedBridge(input: { config: RuntimeConfig; state: Record<string, unknown>; bridge: { child: ChildProcess | null; options: { onSpawn?: (child: ChildProcess) => Promise<void> }; start: () => Promise<unknown> }; kind: string; publish: (state: Record<string, unknown>) => Promise<unknown>; log?: string }): Promise<unknown>;
|