@deksden-com/dd-flow-cli 0.9.0-beta.152 → 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 +114 -16
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- 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 +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- 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 -7
- 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 +8 -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 +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -5,10 +5,10 @@ import { canonicalNativeOperation } from './adapter-timeouts.mjs';
|
|
|
5
5
|
import { admitNativeRequestBuffer } from "./native-streams.mjs";
|
|
6
6
|
import { prepareDaemonRequest } from "./native-request-proof.mjs";
|
|
7
7
|
import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
|
|
8
|
-
import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
|
|
8
|
+
import { errorRecord, isNativeHookObservationLoss, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
|
|
9
9
|
import { ToolObservations, acpToolObservation } from "./tool-observations.mjs";
|
|
10
|
-
import { acceptNativeHookRequest,
|
|
11
|
-
import { parseLifecycleCommand, continuationCommand } from "./lifecycle-command.mjs";
|
|
10
|
+
import { acceptNativeHookRequest, readNativeHookFailures, writeNativeHookFailure } from "./native-hook-command.mjs";
|
|
11
|
+
import { parseLifecycleCommand, continuationCommand, commandOption } from "./lifecycle-command.mjs";
|
|
12
12
|
import { ObservationClock } from "./observation-clock.mjs";
|
|
13
13
|
import { createHash, randomUUID } from "node:crypto";
|
|
14
14
|
import { chmod, copyFile, cp, lstat, mkdir, open, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
@@ -43,7 +43,8 @@ const errorPayload = errorRecord;
|
|
|
43
43
|
function shellQuote(value) { return `'${String(value).replace(/'/g, "'\\''")}'`; }
|
|
44
44
|
|
|
45
45
|
function executable(command, args = []) { const resolved = command ?? process.env.DD_GROK_BIN ?? "grok"; return /\.[cm]?js$/.test(resolved) ? { command: process.execPath, args: [resolved, ...args] } : { command: resolved, args }; }
|
|
46
|
-
export function isolatedEnv(config) { return { ...process.env, HOME: config.grokHome, GROK_HOME: config.grokHome, ...(config.authPath ? { GROK_AUTH_PATH: config.authPath } : {}), ...managedFlowEnv(config)
|
|
46
|
+
export function isolatedEnv(config) { return { ...process.env, HOME: config.grokHome, GROK_HOME: config.grokHome, ...(config.authPath ? { GROK_AUTH_PATH: config.authPath } : {}), ...managedFlowEnv(config),
|
|
47
|
+
DD_GROK_STATE_DIR: !config.noFlow && config.daemonId ? config.runtime_owner?.state_dir : undefined }; }
|
|
47
48
|
function within(candidate, root) { const resolved = path.resolve(candidate); const parent = path.resolve(root); return resolved === parent || resolved.startsWith(`${parent}${path.sep}`); }
|
|
48
49
|
|
|
49
50
|
async function writeIsolatedConfig(paths) {
|
|
@@ -185,7 +186,7 @@ export async function startDaemon(options) {
|
|
|
185
186
|
export async function stopDaemon(options) { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs, { readState: () => readState(options.stateDir) }); }
|
|
186
187
|
|
|
187
188
|
export class Runtime {
|
|
188
|
-
constructor(paths, state, bridge, initialized) { this.paths = paths; this.state = state; this.bridge = bridge; this.initialized = initialized; this.sessions = new Map((state.sessions ?? []).map((item) => [item.provider_session_id, item])); this.descendants = new Map((state.descendants ?? []).map(child => [child.provider_session_id, child])); for (const session of this.sessions.values()) if (session.parent_provider_session_id && !this.descendants.has(session.provider_session_id)) this.descendants.set(session.provider_session_id, { provider_session_id: session.provider_session_id, parent_provider_session_id: session.parent_provider_session_id, status: "unknown", source: "legacy.daemon_state", outcome_error: { code: "native_child_outcome_unavailable", message: "Retained Grok child has no durable outcome receipt" } }); this.toolUsage = new Map(); this.loadedSessionId = null; this.active = null; this.server = null; this.ready = false; this.persisting = Promise.resolve(); }
|
|
189
|
+
constructor(paths, state, bridge, initialized) { this.paths = paths; this.state = state; this.bridge = bridge; this.initialized = initialized; this.sessions = new Map((state.sessions ?? []).map((item) => [item.provider_session_id, item])); this.descendants = new Map((state.descendants ?? []).map(child => [child.provider_session_id, child])); for (const session of this.sessions.values()) if (session.parent_provider_session_id && !this.descendants.has(session.provider_session_id)) this.descendants.set(session.provider_session_id, { provider_session_id: session.provider_session_id, parent_provider_session_id: session.parent_provider_session_id, status: "unknown", source: "legacy.daemon_state", outcome_error: { code: "native_child_outcome_unavailable", message: "Retained Grok child has no durable outcome receipt" } }); this.toolUsage = new Map(); this.promptCreators = new Map(); this.childCreators = new Map(); this.childSpawnWaiters = new Map(); this.loadedSessionId = null; this.active = null; this.server = null; this.ready = false; this.persisting = Promise.resolve(); }
|
|
189
190
|
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...this.sessions.values()], descendants: [...this.descendants.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.catch(() => {}).then(() => writeState(this.paths.state, snapshot)); return this.persisting; }
|
|
190
191
|
async reduceTerminal(outcome) {
|
|
191
192
|
const session = this.sessions.get(outcome.session_id), turn = session?.native_prompt_operation;
|
|
@@ -234,7 +235,7 @@ export class Runtime {
|
|
|
234
235
|
}
|
|
235
236
|
options(params) { const sessionId = params.sessionId; const session = sessionId ? this.sessions.get(sessionId) : null; const root = session?.root_provider_session_id ?? sessionId; return { ...this.state.config, ...params, cwd: params.cwd ?? session?.cwd ?? this.state.config.cwd, initialized: this.initialized, daemonId: this.state.daemon_id, rootProviderSessionId: root, toolUsage: this.toolUsage, allowBackground: true, rootSettled: id => this.settledRoot(id), liveSession: params.liveSession ?? Boolean(sessionId && sessionId === this.loadedSessionId) }; }
|
|
236
237
|
track(result, before = null) { if (!result?.provider_session_id) return; const session = this.sessions.get(result.provider_session_id); const root = result.parent_provider_session_id ? (this.sessions.get(result.parent_provider_session_id)?.root_provider_session_id ?? result.parent_provider_session_id) : (session?.root_provider_session_id ?? result.provider_session_id); this.sessions.set(result.provider_session_id, { ...session, native_root_receipt: session?.native_root_receipt ?? null, provider_session_id: result.provider_session_id, adapter_session_id: result.adapter_session_id ?? result.provider_session_id, parent_provider_session_id: result.parent_provider_session_id ?? session?.parent_provider_session_id ?? null, root_provider_session_id: root, cwd: result.cwd ?? result.target?.newCwd ?? result.info?.cwd ?? session?.cwd ?? null }); for (const child of this.running(result)) this.recordDescendant({ ...child, stale_activity: Boolean(before && before.get(child.sessionId ?? child.subagentId ?? child.provider_session_id) !== this.descendants.get(child.sessionId ?? child.subagentId ?? child.provider_session_id)) }, root, "x.ai/subagent/list_running", "running"); for (const child of result.descendants ?? []) this.recordDescendant(child, root, child.source ?? "x.ai/subagent/event", child.status ?? "unknown"); }
|
|
237
|
-
recordDescendant(value, root, source, fallback) {
|
|
238
|
+
recordDescendant(value, root, source, fallback, nativeSpawn = false) {
|
|
238
239
|
const id = value?.provider_session_id ?? value?.session_id ?? value?.sessionId ?? value?.childSessionId ?? value?.subagentId ?? value?.child_session_id ?? value?.subagent_id;
|
|
239
240
|
const parent = value?.parent_provider_session_id ?? value?.parentSessionId ?? value?.parent_session_id ?? root;
|
|
240
241
|
if (typeof id !== "string" || !id || typeof parent !== "string" || !parent) return;
|
|
@@ -273,6 +274,14 @@ export class Runtime {
|
|
|
273
274
|
return;
|
|
274
275
|
}
|
|
275
276
|
if (!newAttempt && terminal(descendant?.status) && terminal(status) && descendant.status !== status) throw new DaemonError("native_child_outcome_conflict", "Grok child has contradictory terminal outcomes", false, { session_id: id, previous: descendant.status, observed: status });
|
|
277
|
+
if (nativeSpawn && spawned && typeof attempt === "string" && attempt && source === "x.ai/subagent/event") {
|
|
278
|
+
const promptId = value.parent_prompt_id;
|
|
279
|
+
const creator = typeof promptId === "string" && promptId ? this.promptCreators.get(JSON.stringify([parent, promptId])) : null;
|
|
280
|
+
const old = this.childCreators.get(id);
|
|
281
|
+
if (old && old.attempt === attempt && (old.parent !== parent || typeof promptId === "string" && promptId && old.promptId !== promptId)) throw new DaemonError("native_child_execution_ambiguous", "Grok child attempt has conflicting native creator", false, { session_id: id });
|
|
282
|
+
if (creator) this.childCreators.set(id, { parent, promptId, attempt, creator });
|
|
283
|
+
else if (newAttempt) this.childCreators.delete(id);
|
|
284
|
+
}
|
|
276
285
|
this.sessions.set(id, {
|
|
277
286
|
...prior,
|
|
278
287
|
provider_session_id: id,
|
|
@@ -296,6 +305,7 @@ export class Runtime {
|
|
|
296
305
|
...(newAttempt && descendant?.attempt_id ? { retired_attempt_ids: [...retired, descendant.attempt_id] } : retired.length ? { retired_attempt_ids: retired } : {}),
|
|
297
306
|
...(!newAttempt && !terminal(status) && descendant?.outcome_error ? { outcome_error: descendant.outcome_error } : {})
|
|
298
307
|
});
|
|
308
|
+
if (this.childCreators.has(id)) for (const wake of this.childSpawnWaiters.get(id) ?? []) wake();
|
|
299
309
|
if (spawned && typeof value.model === "string" && value.model.trim()) {
|
|
300
310
|
return { sessionId: id, parentSessionId: parent, attemptId: attempt ?? null, parentPromptId: value.parent_prompt_id ?? null, model: value.model.trim() };
|
|
301
311
|
}
|
|
@@ -307,7 +317,7 @@ export class Runtime {
|
|
|
307
317
|
const modelFacts = [];
|
|
308
318
|
if (/subagent/i.test(kind)) {
|
|
309
319
|
for (const value of [update?.subagent, update?.subagentInfo, update?.child, update?.result, update]) {
|
|
310
|
-
const fact = this.recordDescendant(value, root, "x.ai/subagent/event", /fail|error/i.test(kind) ? "failed" : /cancel/i.test(kind) ? "cancelled" : /complete|finish|done/i.test(kind) ? "completed" : "running");
|
|
320
|
+
const fact = this.recordDescendant(value, root, "x.ai/subagent/event", /fail|error/i.test(kind) ? "failed" : /cancel/i.test(kind) ? "cancelled" : /complete|finish|done/i.test(kind) ? "completed" : "running", value === update);
|
|
311
321
|
if (fact) modelFacts.push(fact);
|
|
312
322
|
}
|
|
313
323
|
}
|
|
@@ -396,7 +406,7 @@ export class Runtime {
|
|
|
396
406
|
assertDaemonDispatch(this.paths?.dir);
|
|
397
407
|
if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true);
|
|
398
408
|
this.active = name;
|
|
399
|
-
const current = this.nativeHookOperation = { operationId, sessionId, generation: (this.hookGeneration = (this.hookGeneration ?? 0) + 1), requests: new Map() };
|
|
409
|
+
const current = this.nativeHookOperation = { operationId, sessionId, generation: (this.hookGeneration = (this.hookGeneration ?? 0) + 1), requests: new Map(), guard: captureDispatchGuard(this) };
|
|
400
410
|
let rejectHook;
|
|
401
411
|
const hookFailed = new Promise((_, reject) => { rejectHook = reject; });
|
|
402
412
|
void hookFailed.catch(() => {});
|
|
@@ -406,15 +416,27 @@ export class Runtime {
|
|
|
406
416
|
// and local archive export must not erase an unrelated Session's receipt.
|
|
407
417
|
if (name === "session.prompt" && this.sessions.has(sessionId)) {
|
|
408
418
|
const session = this.sessions.get(sessionId);
|
|
419
|
+
if (typeof operationId === "string" && operationId) {
|
|
420
|
+
current.nativePromptId = randomUUID();
|
|
421
|
+
// Retain only for this daemon lifetime: a late native spawn or hook
|
|
422
|
+
// cannot be safely distinguished from an expired one by a timer.
|
|
423
|
+
// After restart the in-memory correlation is deliberately unavailable.
|
|
424
|
+
this.promptCreators.set(JSON.stringify([sessionId, current.nativePromptId]), current);
|
|
425
|
+
}
|
|
409
426
|
session.native_root_receipt = null;
|
|
410
427
|
this.bridge.dispatchGeneration = current.generation;
|
|
411
|
-
session.native_prompt_operation = { daemon_id: this.state.daemon_id, connection_id: this.bridge.connectionId, operation_id: operationId, generation: current.generation
|
|
428
|
+
session.native_prompt_operation = { daemon_id: this.state.daemon_id, connection_id: this.bridge.connectionId, operation_id: operationId, generation: current.generation,
|
|
429
|
+
...(current.nativePromptId ? { native_prompt_id: current.nativePromptId } : {}) };
|
|
412
430
|
}
|
|
413
431
|
try {
|
|
414
432
|
await this.persist({ active_tree: true, active_operation: name });
|
|
415
433
|
assertDaemonDispatch(this.paths?.dir);
|
|
416
434
|
const before = new Map(this.descendants);
|
|
417
435
|
const result = await Promise.race([task(), hookFailed]);
|
|
436
|
+
await this.observeHookFailures(current);
|
|
437
|
+
if (current.error) throw current.error;
|
|
438
|
+
const hookDiagnostics = (this.bridge.lifecycleDiagnostics ?? []).filter(item => item.operation_id === current.operationId && item.generation === current.generation);
|
|
439
|
+
if (hookDiagnostics.length) result.native_hook_diagnostics = [...new Map([...(result.native_hook_diagnostics ?? []), ...hookDiagnostics].map(item => [JSON.stringify(item), item])).values()];
|
|
418
440
|
await this.drainTerminal();
|
|
419
441
|
this.track(result, before);
|
|
420
442
|
const session = this.sessions.get(result.provider_session_id);
|
|
@@ -429,41 +451,131 @@ export class Runtime {
|
|
|
429
451
|
// root-turn proof, not proof that any descendants have stopped. A lost
|
|
430
452
|
// response, bridge error, or failure before dispatch earns no receipt.
|
|
431
453
|
try { await this.drainTerminal(); } catch (storage) { error.cleanup_error ??= errorRecord(storage); }
|
|
432
|
-
try { await
|
|
454
|
+
try { await this.persist({ active_tree: true, active_operation: null }); }
|
|
433
455
|
catch (storage) { error.cleanup_error = errorRecord(storage); }
|
|
434
456
|
throw error;
|
|
435
457
|
}
|
|
436
|
-
finally { clearInterval(timer); current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; this.active = null; }
|
|
458
|
+
finally { clearInterval(timer); if (!current.nativePromptId) current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; this.active = null; }
|
|
437
459
|
}
|
|
438
460
|
async observeHookFailures(current = this.nativeHookOperation) {
|
|
439
|
-
if (!current || this.nativeHookOperation !== current
|
|
440
|
-
current.reading
|
|
441
|
-
|
|
442
|
-
if (
|
|
443
|
-
}
|
|
461
|
+
if (!current || this.nativeHookOperation !== current) return;
|
|
462
|
+
while (current.reading) {
|
|
463
|
+
await current.reading;
|
|
464
|
+
if (this.nativeHookOperation !== current) return;
|
|
465
|
+
}
|
|
466
|
+
const reading = readNativeHookFailures(this.paths.dir, current.requests, current, error => this.acceptHookFailure(current, error));
|
|
467
|
+
current.reading = reading;
|
|
468
|
+
try { await reading; } finally { if (current.reading === reading) current.reading = null; }
|
|
469
|
+
}
|
|
470
|
+
acceptHookFailure(current, error) {
|
|
471
|
+
if (this.nativeHookOperation !== current) return;
|
|
472
|
+
const unconfirmed = error.details?.native_hook_binding?.native_creator === "unconfirmed";
|
|
473
|
+
if (!unconfirmed && isProvenShellCorrection(error)) return;
|
|
474
|
+
if (unconfirmed || isNativeHookObservationLoss(error)) {
|
|
475
|
+
const diagnostic = { ...errorRecord(error), session_id: current.sessionId, operation_id: current.operationId,
|
|
476
|
+
generation: current.generation, delivery_stage: "hook_observation", completeness: "incomplete" };
|
|
477
|
+
this.bridge.lifecycleDiagnostics ??= [];
|
|
478
|
+
this.bridge.lifecycleDiagnostics.push(diagnostic);
|
|
479
|
+
try { void Promise.resolve(this.bridge.journal?.write("native_hook_observation_failure", diagnostic)).catch(() => {}); } catch { /* diagnostic is optional */ }
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
current.reject(error);
|
|
444
483
|
}
|
|
445
484
|
async acceptHook(identity, sessionId, payload) {
|
|
446
485
|
const input = payload?.tool_input ?? payload?.toolInput;
|
|
447
486
|
const command = input?.command ?? input?.cmd;
|
|
448
487
|
if ((payload?.hook_event_name ?? payload?.hookEventName) !== "PreToolUse" || typeof command !== "string" || (!["lifecycle", "auxiliary"].includes(parseLifecycleCommand(command).participation) && !continuationCommand(command))) return identity;
|
|
449
|
-
const
|
|
450
|
-
const root =
|
|
488
|
+
const active = this.nativeHookOperation, tool = payload.tool_use_id ?? payload.toolUseId ?? payload.tool_call_id;
|
|
489
|
+
const root = identity.rootProviderSessionId;
|
|
490
|
+
const source = sessionId !== root ? this.childCreators.get(sessionId) : null;
|
|
491
|
+
// An older Grok may not surface creator metadata. Preserve its ordinary
|
|
492
|
+
// stateful handler, but never turn the current prompt into guessed proof.
|
|
493
|
+
if (sessionId !== root && !source) return identity;
|
|
494
|
+
const current = sessionId !== root ? source?.creator : active;
|
|
451
495
|
const pending = [...(this.bridge.pending?.values() ?? [])].some(request => request.method === "session/prompt" && request.sessionId === root);
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
496
|
+
const nativePrompt = payload.prompt_id ?? payload.promptId;
|
|
497
|
+
if (!current || current.sessionId !== root || !tool || sessionId === root && (!pending || active !== current)
|
|
498
|
+
|| sessionId === root && typeof nativePrompt === "string" && nativePrompt && nativePrompt !== current.nativePromptId
|
|
499
|
+
|| sessionId !== root && (source.parent !== identity.parentProviderSessionId || this.promptCreators.get(JSON.stringify([source.parent, source.promptId])) !== current))
|
|
500
|
+
throw new DaemonError("native_hook_unproven", "Native hook has no exact source-bound Session operation");
|
|
501
|
+
current.guard(); assertDaemonDispatch(this.paths.dir);
|
|
502
|
+
const nativePayload = { ...payload, _meta: { ...(payload._meta ?? {}), ddGrok: { ...identity } } };
|
|
503
|
+
const binding = await acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation,
|
|
504
|
+
provider_session_id: sessionId, root_provider_session_id: root, parent_provider_session_id: identity.parentProviderSessionId ?? null,
|
|
505
|
+
tool_call_id: tool, event: "PreToolUse", command_sha256: createHash("sha256").update(command).digest("hex"), native_cwd: payload.cwd ?? null,
|
|
506
|
+
native_creator: source || typeof nativePrompt === "string" && nativePrompt && nativePrompt === current.nativePromptId ? "confirmed" : "unconfirmed",
|
|
507
|
+
...(source ? { source_harness: "grok-acp", native_command: command, native_payload: nativePayload } : {}) }, current);
|
|
508
|
+
current.guard();
|
|
509
|
+
if (sessionId === root && this.nativeHookOperation !== current || sessionId !== root && this.childCreators.get(sessionId) !== source)
|
|
510
|
+
throw new DaemonError("native_hook_unproven", "Native hook operation changed during acceptance");
|
|
455
511
|
current.requests.set(binding.request_id, { current, binding });
|
|
456
|
-
|
|
512
|
+
const parsed = parseLifecycleCommand(command);
|
|
513
|
+
const fastAck = Boolean(source && parsed.kind === "standalone" && parsed.invocation.operation === "work_start" && commandOption(parsed.invocation, "invocation-id"));
|
|
514
|
+
return { ...identity, nativeHookBinding: binding, ...(fastAck ? { fastAck: true } : {}) };
|
|
515
|
+
}
|
|
516
|
+
async waitForChildCreator(sessionId, deadlineAt) {
|
|
517
|
+
const remaining = deadlineAt - Date.now();
|
|
518
|
+
if (this.childCreators.has(sessionId) || !Number.isSafeInteger(deadlineAt) || remaining <= 0 || remaining > 30_000) return;
|
|
519
|
+
// Leave most of the existing host budget to the legacy handler if this
|
|
520
|
+
// provider omits the optional spawn correlation; do not poll the provider.
|
|
521
|
+
const waitMs = Math.floor(remaining / 3);
|
|
522
|
+
if (!waitMs) return;
|
|
523
|
+
await new Promise(resolve => {
|
|
524
|
+
const waiters = this.childSpawnWaiters.get(sessionId) ?? new Set();
|
|
525
|
+
const wake = () => { clearTimeout(timer); waiters.delete(wake); if (!waiters.size) this.childSpawnWaiters.delete(sessionId); resolve(); };
|
|
526
|
+
const timer = setTimeout(wake, waitMs);
|
|
527
|
+
waiters.add(wake); this.childSpawnWaiters.set(sessionId, waiters);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
hookOwner(binding) {
|
|
531
|
+
if (!binding?.request_id) return null;
|
|
532
|
+
const owners = new Set([this.nativeHookOperation, ...this.promptCreators.values()]);
|
|
533
|
+
for (const owner of owners) {
|
|
534
|
+
if (!owner || owner.operationId !== binding.operation_id || owner.generation !== binding.turn_generation) continue;
|
|
535
|
+
const accepted = owner.requests.get(binding.request_id);
|
|
536
|
+
if (accepted?.current === owner && JSON.stringify(accepted.binding) === JSON.stringify(binding)) return owner;
|
|
537
|
+
}
|
|
538
|
+
return null;
|
|
457
539
|
}
|
|
458
540
|
async dispatch(operation, params, operationId = null) {
|
|
459
541
|
const assertDispatch = captureDispatchGuard(this);
|
|
460
542
|
if (["session.create", "session.prompt", "session.fork", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
|
|
461
543
|
if (operation === "daemon.ready") return { ready: this.ready, daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, config_isolation: this.state.config_isolation, shutdown_state: this.state.shutdown_state, recovery_status: this.state.recovery_status, auth_status: this.state.auth_status, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config: this.state.config };
|
|
462
544
|
if (operation === "daemon.status") { let observationError; if (!this.active && !this.state.shutdown?.phases?.tree) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { ...await this.dispatch("daemon.ready", {}), observation_error: observationError ?? this.state.observation_error ?? null }; }
|
|
463
|
-
if (operation === "hook.resolve") { const sessionId = String(params.sessionId ?? ""); if (!sessionId) throw new DaemonError("hook_identity_missing", "hook has no sessionId"); let session = this.sessions.get(sessionId); if (!session) { const roots = [...this.sessions.values()].filter((item) => !item.parent_provider_session_id); if (roots.length !== 1) throw new DaemonError("hook_identity_unknown", "unknown hook Session has no unique root"); const observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId, liveSession: true })); this.track(observed); session = this.sessions.get(sessionId); if (!session || session.root_provider_session_id !== roots[0].provider_session_id) throw new DaemonError("hook_identity_unknown", "hook Session is not a member of the retained native tree"); await this.persist(); } return await this.acceptHook({ daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} }, sessionId, params.payload); }
|
|
464
|
-
if (operation === "hook.
|
|
545
|
+
if (operation === "hook.resolve") { const sessionId = String(params.sessionId ?? ""); if (!sessionId) throw new DaemonError("hook_identity_missing", "hook has no sessionId"); let session = this.sessions.get(sessionId); if ((!session || session.parent_provider_session_id) && !this.childCreators.has(sessionId) && this.promptCreators.size) { await this.waitForChildCreator(sessionId, params.hookDeadlineAt); session = this.sessions.get(sessionId); } if (!session) { const roots = [...this.sessions.values()].filter((item) => !item.parent_provider_session_id); if (roots.length !== 1) throw new DaemonError("hook_identity_unknown", "unknown hook Session has no unique root"); const observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId, liveSession: true })); this.track(observed); session = this.sessions.get(sessionId); if (!session || session.root_provider_session_id !== roots[0].provider_session_id) throw new DaemonError("hook_identity_unknown", "hook Session is not a member of the retained native tree"); await this.persist(); } return await this.acceptHook({ daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} }, sessionId, params.payload); }
|
|
546
|
+
if (operation === "hook.verify") {
|
|
547
|
+
if (params.daemonId !== this.state.daemon_id) throw new DaemonError("native_hook_unproven", "Native hook daemon identity differs");
|
|
548
|
+
const owner = this.hookOwner(params.binding);
|
|
549
|
+
if (!owner) throw new DaemonError("native_hook_unproven", "Native hook has no matching retained operation");
|
|
550
|
+
owner.guard(); assertDaemonDispatch(this.paths.dir);
|
|
551
|
+
return { verified: true, nativeHookBinding: params.binding };
|
|
552
|
+
}
|
|
553
|
+
if (operation === "hook.failed") {
|
|
554
|
+
if (params.binding !== undefined || params.failure !== undefined) {
|
|
555
|
+
const binding = params.binding, failure = params.failure;
|
|
556
|
+
const current = this.hookOwner(binding);
|
|
557
|
+
while (current?.reading) await current.reading;
|
|
558
|
+
const accepted = current?.requests.get(binding?.request_id);
|
|
559
|
+
if (!current || this.hookOwner(binding) !== current || accepted?.current !== current || JSON.stringify(accepted.binding) !== JSON.stringify(binding)
|
|
560
|
+
|| typeof failure?.code !== "string" || typeof failure?.message !== "string")
|
|
561
|
+
throw new DaemonError("native_hook_unproven", "Hook failure has no exact current owned delivery");
|
|
562
|
+
current.guard(); assertDaemonDispatch(this.paths.dir);
|
|
563
|
+
if (this.nativeHookOperation !== current) {
|
|
564
|
+
// The creator has returned. Retain the bound failure under its
|
|
565
|
+
// original operation, without rejecting the newer productive turn.
|
|
566
|
+
await writeNativeHookFailure(this.paths.dir, binding, reportedError(failure, "Native hook failed"));
|
|
567
|
+
return { observed: true, retained: true };
|
|
568
|
+
}
|
|
569
|
+
current.requests.delete(binding.request_id);
|
|
570
|
+
const error = reportedError(failure, "Native hook failed");
|
|
571
|
+
error.details = { ...error.details, native_hook_binding: binding };
|
|
572
|
+
this.acceptHookFailure(current, error);
|
|
573
|
+
return { observed: true };
|
|
574
|
+
}
|
|
575
|
+
await this.observeHookFailures(); return { observed: true };
|
|
576
|
+
}
|
|
465
577
|
if (operation === "session.create") return await this.productive(operation, async () => { const result = await createSessionWithBridge(this.bridge, { ...this.options(params), onSessionCreated: async (session) => { this.track(session); this.loadedSessionId = session.provider_session_id; await this.persist(); } }, this.initialized); this.loadedSessionId = result.provider_session_id; return result; }, null, operationId);
|
|
466
|
-
if (operation === "session.prompt") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await promptSessionWithBridge(this.bridge, { ...this.options(params), assertDispatch, operationId }); this.loadedSessionId = params.sessionId; return result; }, params.sessionId, operationId); }
|
|
578
|
+
if (operation === "session.prompt") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await promptSessionWithBridge(this.bridge, { ...this.options(params), assertDispatch, operationId, nativePromptId: this.nativeHookOperation?.nativePromptId }); this.loadedSessionId = params.sessionId; return result; }, params.sessionId, operationId); }
|
|
467
579
|
if (operation === "session.fork") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await forkSessionWithBridge(this.bridge, this.options(params)); this.loadedSessionId = params.sessionId; return result; }); }
|
|
468
580
|
if (operation === "session.inspect" || operation === "session.resume") { const before = new Map(this.descendants); const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result, before); await this.persist(); const settled = !this.active && !this.treeRunning(result); const descendants = this.topology(result.provider_session_id); const terminal = this.sessions.get(result.provider_session_id)?.native_root_receipt; return nativeExecutionReceipt({ ...result, settled, descendants, ...settledNativeTree(result.provider_session_id, descendants, settled, !this.active && !this.rootRunning(result)), ...(terminal?.kind === "failed" && terminal.request_id !== undefined ? { terminal_turn_id: String(terminal.request_id) } : {}) }, { daemonId: this.state.daemon_id, generation: this.controlGeneration ?? 0 }); }
|
|
469
581
|
if (operation === "session.cancel") { if (!this.sessions.has(params.sessionId)) throw new DaemonError("session_identity_mismatch", "cancellation Session is not owned by this daemon"); cancelPendingDispatch(this); const result = await cancelSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); await this.persist({ active_tree: Boolean(this.active) || !result.settled }); return result; }
|
|
@@ -153,12 +153,14 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
153
153
|
const local = { ...options, rootProviderSessionId: options.rootProviderSessionId ?? sessionId }; const initial_usage_error = await forwardUsageBestEffort(local, sessionId, await sessionUsage(bridge, sessionId), sessionToolCalls(local, bridge, sessionId));
|
|
154
154
|
options.assertDispatch?.();
|
|
155
155
|
bridge.dispatchOperationId = options.operationId;
|
|
156
|
-
const
|
|
156
|
+
const diagnosticsCursor = bridge.lifecycleDiagnostics?.length ?? 0;
|
|
157
|
+
const cursor = bridge.toolCursor(); const textCursor = bridge.assistantTextCursor(sessionId); const turn = await bridge.request("session/prompt", { sessionId, prompt: [{ type: "text", text: prompt }], ...(options.nativePromptId ? { _meta: { promptId: options.nativePromptId } } : {}) }, null, { idleSessionId: sessionId, idleTimeoutMs: options.livenessTimeoutMs ?? options.timeoutMs ?? 600_000 });
|
|
157
158
|
try {
|
|
158
159
|
await bridge.flush();
|
|
159
160
|
const evidence = await inspect(bridge, sessionId, local); evidence.tool_calls = recordToolCalls(local, bridge, sessionId, cursor); const usage_ingest_error = await forwardUsageBestEffort(local, sessionId, evidence.usage, evidence.tool_calls); if (initial_usage_error) evidence.initial_usage_error = initial_usage_error; if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error;
|
|
160
161
|
if (!options.allowBackground && (evidence.subagents?.subagents ?? []).length) { await cancelTree(bridge, sessionId); fail("background Grok Build subagents cannot outlive the one-shot dd-grok connection"); }
|
|
161
|
-
|
|
162
|
+
const diagnostics = (bridge.lifecycleDiagnostics ?? []).slice(diagnosticsCursor).filter(item => item.session_id === sessionId);
|
|
163
|
+
return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, assistant_text: bridge.assistantTextSince(sessionId, textCursor), turn, profile: profileReceipt, evidence, ...(diagnostics.length ? { native_hook_diagnostics: diagnostics } : {}) });
|
|
162
164
|
} catch (error) { throw Object.assign(new Error("Grok native Turn completed but mandatory observation failed", { cause: error }), { code: "native_outcome_observation_failed", retryable: false,
|
|
163
165
|
details: { provider_session_id: sessionId, native_outcome: { status: "completed", turn }, observation_error: errorRecord(error), settled: false, journal_locator: options.journal } }); }
|
|
164
166
|
}
|
|
@@ -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,7 +14,7 @@ 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 { diagnosticJson, errorRecord, isNativeHookObservationLoss, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
|
|
17
18
|
import { checkObservedProfile, observeModel, observedFields, readModelObservations } from "./model-observations.mjs";
|
|
18
19
|
import os from "node:os";
|
|
19
20
|
import path from "node:path";
|
|
@@ -21,7 +22,9 @@ 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);
|
|
@@ -211,13 +216,14 @@ export class Runtime {
|
|
|
211
216
|
throw new OpenCodeError("opencode_unavailable", "OpenCode child directory binding is not yet observable");
|
|
212
217
|
return this.retainSession(info, root);
|
|
213
218
|
}
|
|
214
|
-
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 }); } }
|
|
215
|
-
async childTree(id, seen = new Set()) {
|
|
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 () => {}) {
|
|
216
221
|
if (seen.has(id)) throw new DaemonError("session_identity_mismatch", "OpenCode native topology contains a cycle");
|
|
217
222
|
seen.add(id); const direct = await this.ownedChildren(id), nested = [];
|
|
223
|
+
await onProgress(`children:${id}:${createHash('sha256').update(JSON.stringify(direct)).digest('hex')}`);
|
|
218
224
|
for (const child of direct) {
|
|
219
225
|
if (!child.id || child.parentID !== id) throw new DaemonError("session_identity_mismatch", "OpenCode child has a foreign native parent");
|
|
220
|
-
nested.push(child, ...await this.childTree(child.id, seen));
|
|
226
|
+
nested.push(child, ...await this.childTree(child.id, seen, onProgress));
|
|
221
227
|
}
|
|
222
228
|
return nested;
|
|
223
229
|
}
|
|
@@ -416,8 +422,27 @@ export class Runtime {
|
|
|
416
422
|
current.reading = readNativeHookFailures(this.paths.dir, current.requests, current, error => this.retainHookFailure(current, error));
|
|
417
423
|
try { await current.reading; } finally { current.reading = null; }
|
|
418
424
|
}
|
|
419
|
-
retainHookFailure(current, error) {
|
|
420
|
-
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
|
+
}
|
|
421
446
|
error.details = { ...error.details, provider_session_id: current.sessionId, native_outcome: "unknown", native_turn_items: { observed: true, possible_effects: true, pending: true } };
|
|
422
447
|
current.failure ??= error;
|
|
423
448
|
}
|
|
@@ -425,12 +450,46 @@ export class Runtime {
|
|
|
425
450
|
const assertDispatch = captureDispatchGuard(this);
|
|
426
451
|
if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume", "session.archive"].includes(operation)) assertDaemonDispatch(this.paths.dir);
|
|
427
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
|
+
}
|
|
428
469
|
if (operation === "hook.resolve") {
|
|
429
|
-
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;
|
|
430
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))
|
|
431
489
|
throw new DaemonError("native_hook_unproven", "OpenCode hook has no exact active operation");
|
|
432
490
|
let owned = sessionId === current.sessionId || this.sessions.get(sessionId)?.root_provider_session_id === current.sessionId;
|
|
433
491
|
try {
|
|
492
|
+
const guard = current.guard ?? captureDispatchGuard(this); guard();
|
|
434
493
|
const tree = await this.childTree(current.sessionId);
|
|
435
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");
|
|
436
495
|
owned = true;
|
|
@@ -440,29 +499,66 @@ export class Runtime {
|
|
|
440
499
|
if (payload.input.workdir !== undefined && typeof payload.input.workdir !== "string") throw new DaemonError("native_hook_unproven", "OpenCode hook workdir must be a string");
|
|
441
500
|
if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode operation changed during hook identity observation");
|
|
442
501
|
const accepted = await acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation,
|
|
443
|
-
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",
|
|
444
503
|
command_sha256: createHash("sha256").update(payload.input.command).digest("hex"), native_cwd: path.resolve(native.directory, payload.input.workdir ?? ".") }, current);
|
|
445
504
|
if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "OpenCode operation changed during hook acceptance");
|
|
446
|
-
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: {
|
|
447
506
|
id: native.id, directory: native.directory, parentID: native.parentID, projectID: native.projectID,
|
|
448
507
|
model: { providerID: native.model?.providerID, id: native.model?.id, variant: native.model?.variant }
|
|
449
508
|
} };
|
|
450
509
|
} catch (error) {
|
|
451
|
-
if (owned) this.retainHookFailure(current, error);
|
|
510
|
+
if (owned) await this.retainHookFailure(current, error, true);
|
|
452
511
|
throw error;
|
|
453
512
|
}
|
|
454
513
|
}
|
|
455
514
|
if (operation === "hook.verify") {
|
|
456
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
|
+
}
|
|
457
544
|
if (!current || binding?.operation_id !== current.operationId || binding?.turn_generation !== current.generation
|
|
458
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 });
|
|
459
548
|
return { current: true };
|
|
460
549
|
}
|
|
461
550
|
if (operation === "hook.failed") {
|
|
462
551
|
if (params.failure) {
|
|
463
|
-
await this.dispatch("hook.verify", { binding: params.binding });
|
|
464
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");
|
|
465
|
-
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 };
|
|
466
562
|
}
|
|
467
563
|
await this.observeHookFailures(); return { observed: true };
|
|
468
564
|
}
|
|
@@ -497,8 +593,9 @@ export class Runtime {
|
|
|
497
593
|
if (operation === "session.inspect" || operation === "session.resume") { const result = await this.describe(params.sessionId); await this.persist(); return result; }
|
|
498
594
|
if (operation === "session.prompt") { await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
|
|
499
595
|
this.state.operations ??= {}; const key = operationId ?? randomUUID(), retained = this.state.operations[key];
|
|
500
|
-
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 };
|
|
501
597
|
this.state.hook_generation = current.generation; this.nativeHookOperation = current;
|
|
598
|
+
this.lastNativeHookOperation = current;
|
|
502
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; };
|
|
503
600
|
let reply;
|
|
504
601
|
try { assertDispatch(); reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, messageId: retained?.message_id ?? `msg_${randomUUID().replaceAll("-", "")}`, retained,
|
|
@@ -523,6 +620,7 @@ export class Runtime {
|
|
|
523
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 });
|
|
524
621
|
result.response_message_id = reply.info.id;
|
|
525
622
|
result.observed_profile = { ...reply_profile, mode: result.info.agent ?? reply_profile.mode };
|
|
623
|
+
if (current.hookDiagnostics?.length) result.native_hook_diagnostics = current.hookDiagnostics;
|
|
526
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; }); }
|
|
527
625
|
if (operation === "session.fork") { const source = await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
|
|
528
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");
|
|
@@ -329,9 +328,13 @@ export class OpenCodeClient {
|
|
|
329
328
|
const child = (await childrenOf(id, visited)).find(child => child.id === item.info.metadata.childID);
|
|
330
329
|
if (child) await observe(child.id, visited);
|
|
331
330
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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)))
|
|
335
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 });
|
|
336
339
|
}
|
|
337
340
|
const terminal = rootMessages.slice(deliveredIndex + 1).findLast(item => item.info.role === "idle");
|
|
@@ -344,8 +347,8 @@ export class OpenCodeClient {
|
|
|
344
347
|
// A background child's native completion notification is delivered
|
|
345
348
|
// asynchronously. An idle parent before that delivery is not the
|
|
346
349
|
// completion of the coordinating operation.
|
|
347
|
-
const deliveredNotifications = new Set(rootMessages.filter(item => item.info.role === "synthetic" && item.info.metadata?.source === "subagent").map(item => item.info.metadata.childID));
|
|
348
|
-
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));
|
|
349
352
|
unsettled ||= [...backgroundShells].some(shell => !shellNotifications.has(shell));
|
|
350
353
|
for (const item of current) for (const part of item.parts) {
|
|
351
354
|
if (part.tool === "subagent" && part.state?.input?.background === true) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function callDaemon(stateDir: string, operation: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<Record<string, unknown>>;
|