@zq-silk/yui 0.6.13 → 0.6.14

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.
Files changed (90) hide show
  1. package/README.md +34 -5
  2. package/dist/cli/commandCatalog.js +173 -57
  3. package/dist/cli/helpRenderer.js +3 -1
  4. package/dist/cli.js +99 -11
  5. package/dist/commands/configCommands.js +521 -171
  6. package/dist/commands/deliveryGuardPreflight.js +2 -2
  7. package/dist/commands/executionAuditCommands.js +56 -3
  8. package/dist/commands/projectCommands.js +504 -2
  9. package/dist/commands/releaseCommands.js +0 -1
  10. package/dist/commands/taskActor.js +17 -0
  11. package/dist/commands/taskBaseCommands.js +29 -0
  12. package/dist/commands/taskCommands.js +577 -126
  13. package/dist/commands/taskContextCommand.js +36 -2
  14. package/dist/commands/taskNextActionCommand.js +48 -3
  15. package/dist/commands/taskPublicationCommands.js +319 -0
  16. package/dist/commands/taskRoleRuntimeStatus.js +83 -59
  17. package/dist/commands/telemetryCommands.js +14 -13
  18. package/dist/config/yuiConfig.js +161 -8
  19. package/dist/context/sessionContextBudget.js +68 -0
  20. package/dist/context/wakeNotification.js +65 -0
  21. package/dist/controller/clientRuntime.js +2 -1
  22. package/dist/controller/ephemeralResourceReaper.js +2 -1
  23. package/dist/controller/fileSchedulerStoreAdapter.js +183 -16
  24. package/dist/controller/jobSupervisor.js +5 -4
  25. package/dist/controller/resourceCleanupLinux.js +6 -6
  26. package/dist/controller/resourceInventoryLinux.js +3 -3
  27. package/dist/controller/runtime.js +88 -10
  28. package/dist/controller/updateReconciliation.js +4 -3
  29. package/dist/doctor/doctor.js +26 -7
  30. package/dist/executor/agentConfigurationCatalog.js +18 -0
  31. package/dist/executor/fileRoleLaunchPlanner.js +3 -3
  32. package/dist/lifecycle/contextBudgetRollover.js +81 -0
  33. package/dist/lifecycle/exactRunTerminalization.js +11 -1
  34. package/dist/lifecycle/providerErrorClass.js +33 -12
  35. package/dist/observability/executionAudit.js +214 -6
  36. package/dist/output/table.js +18 -0
  37. package/dist/repository/gitWorkspace.js +92 -0
  38. package/dist/repository/project.js +218 -4
  39. package/dist/repository/taskBaseFreshness.js +318 -0
  40. package/dist/repository/taskWorkspacePreparer.js +16 -2
  41. package/dist/review/deltaRecheck.js +232 -0
  42. package/dist/review/reviewConfig.js +31 -0
  43. package/dist/review/reviewFindingLedger.js +5 -1
  44. package/dist/review/reviewRound.js +156 -1
  45. package/dist/run/providerRetry.js +21 -3
  46. package/dist/run/providerRetryConfig.js +13 -60
  47. package/dist/run/recoveryProjection.js +199 -0
  48. package/dist/runtime/builtinAgentDrivers.js +3 -0
  49. package/dist/runtime/builtinTranscriptUsage.js +76 -32
  50. package/dist/runtime/continuationManager.js +17 -0
  51. package/dist/runtime/index.js +2 -0
  52. package/dist/runtime/launchDiagnostics.js +154 -0
  53. package/dist/runtime/lifecycleReservation.js +13 -0
  54. package/dist/runtime/providerContinuation.js +38 -0
  55. package/dist/runtime/providerContinuationReconciliationService.js +1 -0
  56. package/dist/runtime/providerErrorCodes.js +278 -0
  57. package/dist/runtime/runtimeHealthPolicy.js +20 -0
  58. package/dist/runtime/runtimeObservation.js +1 -0
  59. package/dist/runtime/runtimeProjection.js +115 -23
  60. package/dist/runtime/tmuxAdapters.js +242 -48
  61. package/dist/scheduler/activeRoleRunDelivery.js +139 -3
  62. package/dist/scheduler/activeTaskProgress.js +4 -3
  63. package/dist/scheduler/leaderWakeupProcessor.js +105 -15
  64. package/dist/scheduler/roleRunLiveness.js +2 -1
  65. package/dist/scheduler/roleRunStall.js +3 -2
  66. package/dist/scheduler/taskWake.js +72 -0
  67. package/dist/scheduler/wakeReason.js +64 -0
  68. package/dist/scheduler/wakeupQueue.js +2 -1
  69. package/dist/setup/setupCommand.js +1 -1
  70. package/dist/storage/migration/productionRegistry.js +325 -1
  71. package/dist/storage/sqliteSchema.js +61 -2
  72. package/dist/storage/sqliteStore.js +129 -2
  73. package/dist/storage/storeRpc.js +1 -0
  74. package/dist/storage/taskStore.js +262 -5
  75. package/dist/storage/upgrade/recordVersions.js +6 -1
  76. package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
  77. package/dist/task/completionReadiness.js +282 -0
  78. package/dist/task/publicationReference.js +123 -0
  79. package/dist/task/taskRecordReference.js +3 -1
  80. package/dist/telemetry/telemetryConfig.js +23 -18
  81. package/dist/telemetry/telemetryWiring.js +8 -8
  82. package/dist/tmux/tmuxManager.js +50 -9
  83. package/dist/web/assets/client/i18n.js +4 -0
  84. package/dist/web/assets/client/view.js +18 -0
  85. package/dist/web/webSnapshot.js +100 -10
  86. package/i18n/README.zh-CN.md +5 -5
  87. package/package.json +1 -1
  88. package/skills/yui-leader/SKILL.md +49 -10
  89. package/skills/yui-operator/SKILL.md +13 -3
  90. package/skills/yui-worker/SKILL.md +8 -0
@@ -0,0 +1,199 @@
1
+ import { createHash } from "node:crypto";
2
+ import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
3
+ export const RUN_RECOVERY_ACTIONS = [
4
+ "diagnose",
5
+ "retry",
6
+ "replace-session",
7
+ "terminate"
8
+ ];
9
+ /**
10
+ * Reads every durable record the recovery projection needs. Returns null
11
+ * only when the Run itself is absent.
12
+ */
13
+ export function readRunRecoveryFacts(store, taskId, runId) {
14
+ const run = store.getAgentRun(taskId, runId);
15
+ if (run === null || run.taskId !== taskId)
16
+ return null;
17
+ const task = store.getTask(taskId);
18
+ const sessionSet = store.getTaskRoleSessionSet(taskId, run.roleName);
19
+ const progress = latestRunDurableProgressAt(store, taskId, run.roleName, runId);
20
+ return {
21
+ run,
22
+ task: task === null ? null : { id: task.id, status: task.status },
23
+ sessionSet,
24
+ progress,
25
+ latestProviderObservation: latestRunProviderObservation(store.listEvents(taskId), runId)
26
+ };
27
+ }
28
+ /**
29
+ * Latest Provider observation for a Run. Provider timestamps are evidence:
30
+ * they explain why a stale fence was supplied but never authorize recovery.
31
+ */
32
+ function latestRunProviderObservation(events, runId) {
33
+ let latest = null;
34
+ for (const event of events) {
35
+ if (event.type !== "runtime.observation")
36
+ continue;
37
+ if (event.payload.runId !== runId)
38
+ continue;
39
+ const kind = typeof event.payload.kind === "string" ? event.payload.kind : "unknown";
40
+ const receivedAt = typeof event.payload.receivedAt === "string"
41
+ && Number.isFinite(Date.parse(event.payload.receivedAt))
42
+ ? event.payload.receivedAt
43
+ : event.createdAt;
44
+ const at = Date.parse(receivedAt);
45
+ if (latest === null || at > latest.at) {
46
+ latest = { kind, receivedAt, at };
47
+ }
48
+ }
49
+ return latest === null ? null : { kind: latest.kind, receivedAt: latest.receivedAt };
50
+ }
51
+ export function projectRunRecovery(facts) {
52
+ const { run, task, sessionSet, progress } = facts;
53
+ const session = activeSession(facts);
54
+ const canonicalProgressAt = progress?.progressAt ?? null;
55
+ const accepted = run.deliveredAt !== undefined;
56
+ const acceptanceOptions = accepted
57
+ ? ["accepted", "ambiguous"]
58
+ : ["rejected", "ambiguous"];
59
+ const blocked = recoveryBlocker(facts, session, canonicalProgressAt);
60
+ const actions = blocked === null
61
+ ? RUN_RECOVERY_ACTIONS.map((action) => buildActionPlan(facts, action, session, canonicalProgressAt))
62
+ : [];
63
+ const judgmentRequired = blocked === null && actions.some((plan) => plan.argv.includes(PROVIDER_ACCEPTANCE_PLACEHOLDER))
64
+ ? "Provider acceptance is not durably determined for every action; pass --provider-acceptance explicitly."
65
+ : undefined;
66
+ return {
67
+ taskId: run.taskId,
68
+ runId: run.id,
69
+ roleName: run.roleName,
70
+ runStatus: run.status,
71
+ recoverable: blocked === null,
72
+ canonicalProgressAt,
73
+ ...(progress?.evidence === undefined ? {} : { canonicalProgressEvidence: progress.evidence }),
74
+ provider: {
75
+ acceptedAt: run.deliveredAt ?? null,
76
+ observedAt: facts.latestProviderObservation?.receivedAt ?? null,
77
+ observationKind: facts.latestProviderObservation?.kind ?? null
78
+ },
79
+ providerAcceptance: {
80
+ accepted,
81
+ options: acceptanceOptions
82
+ },
83
+ session: session === null ? null : {
84
+ status: session.status,
85
+ ...(session.nativeSessionId === undefined ? {} : { nativeSessionId: session.nativeSessionId }),
86
+ ...(session.launchId === undefined ? {} : { launchId: session.launchId })
87
+ },
88
+ actions,
89
+ ...(judgmentRequired === undefined ? {} : { judgmentRequired }),
90
+ ...(blocked === null ? {} : { reason: blocked })
91
+ };
92
+ }
93
+ const PROVIDER_ACCEPTANCE_PLACEHOLDER = "<accepted|rejected|ambiguous>";
94
+ function activeSession(facts) {
95
+ const sessions = facts.sessionSet;
96
+ if (sessions === null)
97
+ return null;
98
+ const session = sessions.sessions[sessions.activeAgentId];
99
+ if (session === undefined)
100
+ return null;
101
+ if (session.agentId !== facts.run.effective.agentId)
102
+ return null;
103
+ if (session.adapterId !== facts.run.effective.adapterId)
104
+ return null;
105
+ return session;
106
+ }
107
+ /**
108
+ * Mirrors the fail-closed checks of `recoverExactAgentRun` that are visible
109
+ * from durable records. A non-null result means recovery cannot currently be
110
+ * applied; the canonical fence is still projected for diagnosis.
111
+ */
112
+ function recoveryBlocker(facts, session, canonicalProgressAt) {
113
+ const { run, task } = facts;
114
+ if (task === null)
115
+ return "task-missing";
116
+ if (task.status !== "active")
117
+ return "task-terminal";
118
+ if (run.status !== "active")
119
+ return "run-terminal";
120
+ if (canonicalProgressAt === null)
121
+ return "progress-unavailable";
122
+ if (session === null)
123
+ return "session-missing";
124
+ if (session.status === "stopped")
125
+ return "session-stopped";
126
+ if (session.status === "broken")
127
+ return "session-broken";
128
+ return null;
129
+ }
130
+ function buildActionPlan(facts, action, session, canonicalProgressAt) {
131
+ const { run } = facts;
132
+ const acceptance = actionAcceptance(facts, action);
133
+ const argv = [
134
+ "task",
135
+ "run",
136
+ "recover",
137
+ `${run.taskId}/${run.id}`,
138
+ "--action",
139
+ action,
140
+ "--expected-progress-at",
141
+ canonicalProgressAt,
142
+ "--provider-acceptance",
143
+ acceptance,
144
+ "--reason",
145
+ "<text>",
146
+ "--agent-id",
147
+ run.effective.agentId,
148
+ "--adapter-id",
149
+ run.effective.adapterId,
150
+ ...(session.nativeSessionId === undefined
151
+ ? []
152
+ : ["--native-session-id", session.nativeSessionId]),
153
+ ...(session.launchId === undefined
154
+ ? []
155
+ : ["--launch-id", session.launchId])
156
+ ];
157
+ const command = `yui ${argv
158
+ .map((part) => (part === "<text>" ? '"<text>"' : part))
159
+ .join(" ")}`;
160
+ const fingerprintSource = [
161
+ run.id,
162
+ action,
163
+ canonicalProgressAt,
164
+ run.effective.agentId,
165
+ run.effective.adapterId,
166
+ session.nativeSessionId ?? "",
167
+ session.launchId ?? ""
168
+ ].join("|");
169
+ return {
170
+ action,
171
+ reason: ACTION_REASONS[action],
172
+ expectedProgressAt: canonicalProgressAt,
173
+ agentId: run.effective.agentId,
174
+ adapterId: run.effective.adapterId,
175
+ ...(session.nativeSessionId === undefined
176
+ ? {}
177
+ : { nativeSessionId: session.nativeSessionId }),
178
+ ...(session.launchId === undefined ? {} : { launchId: session.launchId }),
179
+ command,
180
+ argv,
181
+ fingerprint: createHash("sha256").update(fingerprintSource).digest("hex")
182
+ };
183
+ }
184
+ /**
185
+ * The acceptance value for the copy-paste command. When exactly one value is
186
+ * durably valid it is filled in (the durable record, not a guess); otherwise
187
+ * the Leader must choose and the command carries an explicit placeholder.
188
+ */
189
+ function actionAcceptance(facts, action) {
190
+ if (action === "diagnose")
191
+ return PROVIDER_ACCEPTANCE_PLACEHOLDER;
192
+ return facts.run.deliveredAt === undefined ? "rejected" : "accepted";
193
+ }
194
+ const ACTION_REASONS = {
195
+ diagnose: "Collect bounded diagnostics before any state-changing recovery.",
196
+ retry: "Request another provider turn on the same native Session when the failure is transient.",
197
+ "replace-session": "Request a fresh native Session when the current one is unusable.",
198
+ terminate: "Fail the Run explicitly when recovery is not viable."
199
+ };
@@ -1,5 +1,6 @@
1
1
  import { AgentDriverRegistry } from "./agentDriver.js";
2
2
  import { claudeTranscriptObserver, codexTranscriptObserver, transcriptObserverSource } from "./builtinTranscriptObserver.js";
3
+ import { parseClaudeError } from "./providerErrorCodes.js";
3
4
  export const CODEX_DRIVER_ID = "openai/codex";
4
5
  export const CLAUDE_CODE_DRIVER_ID = "anthropic/claude-code";
5
6
  export function builtinDriverIdForAdapter(adapterId) {
@@ -352,8 +353,10 @@ function claudeFailure(payload) {
352
353
  const code = firstIdentity(payload, ["error"], "Claude StopFailure error");
353
354
  const details = optionalText(payload.error_details);
354
355
  const lastOutput = optionalText(payload.last_assistant_message);
356
+ const parsed = parseClaudeError(code, details);
355
357
  return {
356
358
  failure: {
359
+ ...(parsed.code !== "unknown" ? { errorCode: parsed.code } : {}),
357
360
  code,
358
361
  ...(details === undefined ? {} : { details }),
359
362
  ...(lastOutput === undefined ? {} : { lastOutput }),
@@ -1,5 +1,26 @@
1
1
  export function codexTranscriptUsage(transcript) {
2
- let latest = null;
2
+ const report = codexTranscriptUsageReport(transcript);
3
+ if (report === null)
4
+ return null;
5
+ return Object.freeze({
6
+ inputTokens: report.uncachedInputTokens + report.cacheReadTokens + report.cacheCreatedTokens,
7
+ outputTokens: report.outputTokens,
8
+ ...(report.cacheReadTokens + report.cacheCreatedTokens === 0
9
+ ? {}
10
+ : { cachedInputTokens: report.cacheReadTokens + report.cacheCreatedTokens })
11
+ });
12
+ }
13
+ export function codexTranscriptUsageReport(transcript) {
14
+ // Codex token_count events are cumulative session snapshots. Per-request
15
+ // input is the delta between consecutive snapshots; the first snapshot is
16
+ // treated as one full request.
17
+ let latestInput = 0;
18
+ let latestCached = 0;
19
+ let latestOutput = 0;
20
+ let previous = 0;
21
+ let peak = 0;
22
+ let requests = 0;
23
+ let seen = false;
3
24
  for (const line of transcript.split("\n")) {
4
25
  const entry = parseLine(line);
5
26
  if (entry?.type !== "event_msg")
@@ -13,18 +34,44 @@ export function codexTranscriptUsage(transcript) {
13
34
  const outputTokens = integer(usage?.output_tokens);
14
35
  if (inputTokens === null || outputTokens === null)
15
36
  continue;
16
- const cachedInputTokens = integer(usage?.cached_input_tokens);
17
- const reasoningTokens = integer(usage?.reasoning_output_tokens);
18
- latest = Object.freeze({
19
- inputTokens,
20
- outputTokens,
21
- ...(cachedInputTokens === null ? {} : { cachedInputTokens }),
22
- ...(reasoningTokens === null ? {} : { reasoningTokens })
23
- });
37
+ const cachedInputTokens = integer(usage?.cached_input_tokens) ?? 0;
38
+ requests += 1;
39
+ seen = true;
40
+ const total = inputTokens + cachedInputTokens;
41
+ const delta = Math.max(0, total - previous);
42
+ if (delta > peak)
43
+ peak = delta;
44
+ previous = total;
45
+ latestInput = inputTokens;
46
+ latestCached = cachedInputTokens;
47
+ latestOutput = outputTokens;
24
48
  }
25
- return latest;
49
+ if (!seen)
50
+ return null;
51
+ return Object.freeze({
52
+ requests,
53
+ uncachedInputTokens: Math.max(0, latestInput - latestCached),
54
+ cacheReadTokens: latestCached,
55
+ cacheCreatedTokens: 0,
56
+ outputTokens: latestOutput,
57
+ peakRequestTokens: peak
58
+ });
26
59
  }
27
60
  export function claudeTranscriptUsage(transcript) {
61
+ const report = claudeTranscriptUsageReport(transcript);
62
+ if (report === null)
63
+ return null;
64
+ return Object.freeze({
65
+ inputTokens: report.uncachedInputTokens + report.cacheReadTokens + report.cacheCreatedTokens,
66
+ outputTokens: report.outputTokens,
67
+ ...(report.cacheReadTokens + report.cacheCreatedTokens === 0
68
+ ? {}
69
+ : { cachedInputTokens: report.cacheReadTokens + report.cacheCreatedTokens })
70
+ });
71
+ }
72
+ export function claudeTranscriptUsageReport(transcript) {
73
+ // Each Claude assistant message carries its own per-request usage, so the
74
+ // report is a direct aggregation; the peak is the largest single message.
28
75
  const messages = new Map();
29
76
  for (const line of transcript.split("\n")) {
30
77
  const entry = parseLine(line);
@@ -33,13 +80,11 @@ export function claudeTranscriptUsage(transcript) {
33
80
  const message = object(entry.message);
34
81
  const usage = object(message?.usage);
35
82
  const directInput = integer(usage?.input_tokens);
36
- const outputTokens = integer(usage?.output_tokens);
37
- if (directInput === null || outputTokens === null)
83
+ const output = integer(usage?.output_tokens);
84
+ if (directInput === null || output === null)
38
85
  continue;
39
86
  const cacheRead = integer(usage?.cache_read_input_tokens) ?? 0;
40
87
  const cacheCreated = integer(usage?.cache_creation_input_tokens) ?? 0;
41
- const details = object(usage?.output_tokens_details);
42
- const reasoningTokens = integer(details?.thinking_tokens);
43
88
  const key = typeof message?.id === "string" && message.id.length > 0
44
89
  ? `message:${message.id}`
45
90
  : typeof entry.uuid === "string" && entry.uuid.length > 0
@@ -49,32 +94,31 @@ export function claudeTranscriptUsage(transcript) {
49
94
  // stable provider or entry identity, summing them would fabricate growth.
50
95
  if (key === null)
51
96
  continue;
52
- messages.set(key, Object.freeze({
53
- // Normalize inputTokens as the complete input total. cachedInputTokens is
54
- // a breakdown and must not be added to it again by the projection.
55
- inputTokens: directInput + cacheRead + cacheCreated,
56
- outputTokens,
57
- cachedInputTokens: cacheRead + cacheCreated,
58
- ...(reasoningTokens === null ? {} : { reasoningTokens })
59
- }));
97
+ messages.set(key, { directInput, cacheRead, cacheCreated, output });
60
98
  }
61
99
  if (messages.size === 0)
62
100
  return null;
63
- let inputTokens = 0;
101
+ let uncachedInputTokens = 0;
102
+ let cacheReadTokens = 0;
103
+ let cacheCreatedTokens = 0;
64
104
  let outputTokens = 0;
65
- let cachedInputTokens = 0;
66
- let reasoningTokens = 0;
105
+ let peak = 0;
67
106
  for (const usage of messages.values()) {
68
- inputTokens += usage.inputTokens;
69
- outputTokens += usage.outputTokens;
70
- cachedInputTokens += usage.cachedInputTokens ?? 0;
71
- reasoningTokens += usage.reasoningTokens ?? 0;
107
+ uncachedInputTokens += usage.directInput;
108
+ cacheReadTokens += usage.cacheRead;
109
+ cacheCreatedTokens += usage.cacheCreated;
110
+ outputTokens += usage.output;
111
+ const total = usage.directInput + usage.cacheRead + usage.cacheCreated;
112
+ if (total > peak)
113
+ peak = total;
72
114
  }
73
115
  return Object.freeze({
74
- inputTokens,
116
+ requests: messages.size,
117
+ uncachedInputTokens,
118
+ cacheReadTokens,
119
+ cacheCreatedTokens,
75
120
  outputTokens,
76
- cachedInputTokens,
77
- ...(reasoningTokens === 0 ? {} : { reasoningTokens })
121
+ peakRequestTokens: peak
78
122
  });
79
123
  }
80
124
  function parseLine(line) {
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { createProviderContinuation, observeProviderContinuation, providerContinuationKey, recordProviderReport } from "./providerContinuation.js";
2
3
  import { createRuntimeObservation } from "./runtimeObservation.js";
3
4
  export function foldContinuationObservation(existing, raw) {
@@ -30,12 +31,14 @@ export function foldContinuationObservation(existing, raw) {
30
31
  throw new Error("Continuation observation identity does not match the existing projection.");
31
32
  }
32
33
  if (observation.kind === "continuation.reported") {
34
+ const resultReceipt = durableResultReceipt(payload.summary);
33
35
  const next = recordProviderReport(base, {
34
36
  reportId: payload.reportId,
35
37
  ...(payload.resultRef === undefined ? {} : { resultRef: payload.resultRef }),
36
38
  ...(payload.providerDeliveryRef === undefined
37
39
  ? {}
38
40
  : { providerDeliveryRef: payload.providerDeliveryRef }),
41
+ ...resultReceipt,
39
42
  observedAt: observation.observedAt ?? observation.receivedAt
40
43
  }, observation.sequence);
41
44
  return {
@@ -62,6 +65,20 @@ export function foldContinuationObservation(existing, raw) {
62
65
  continuation: next
63
66
  };
64
67
  }
68
+ /**
69
+ * The durable-result receipt for a native child report. Yui only claims
70
+ * "durable-result" durability when it has actually persisted result content;
71
+ * the sha256 digest makes replays idempotent and lets recovery verify the
72
+ * result by digest. A report without persisted content stays best-effort.
73
+ */
74
+ function durableResultReceipt(summary) {
75
+ if (summary === undefined || summary.trim().length === 0)
76
+ return {};
77
+ return Object.freeze({
78
+ resultDigest: createHash("sha256").update(summary).digest("hex"),
79
+ resultSize: summary.length
80
+ });
81
+ }
65
82
  /**
66
83
  * Commits the semantic continuation fact and its Leader notification before
67
84
  * releasing child ownership. The surrounding TaskStore transaction is the
@@ -1,4 +1,5 @@
1
1
  export { createPromptEnvelope } from "./promptEnvelope.js";
2
+ export { codexTranscriptUsage, codexTranscriptUsageReport, claudeTranscriptUsage, claudeTranscriptUsageReport } from "./builtinTranscriptUsage.js";
2
3
  export { createRuntimeBinding } from "./runtimeBinding.js";
3
4
  export { normalizeRuntimeOwner } from "./runtimeOwner.js";
4
5
  export { createSessionLaunchRequest } from "./sessionLaunchRequest.js";
@@ -8,6 +9,7 @@ export { TmuxPromptPushAdapter, TmuxSessionHost } from "./tmuxAdapters.js";
8
9
  export { FileTaskRuntimeIsolation, YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR, YUI_TASK_RUNTIME_SERVICE_NAMESPACE, assertTaskRuntimeIsolationPreflight, createTaskRuntimeIsolationDescriptor, parseTaskRuntimeIsolationDescriptor, planTaskRuntimeCleanup, taskRuntimeIsolationEnvironment, taskRuntimeIsolationFingerprint } from "./taskRuntimeIsolation.js";
9
10
  export { createSessionOwnerIdentity, discoverProviderRootByLaunchEnv, isLinuxProcessLive, listLaunchFencedProcesses, listOwnedProcessTree, readLinuxProcessIdentity } from "./sessionOwnerIdentity.js";
10
11
  export { FileSessionOwnerRegistry } from "./sessionOwnerRegistry.js";
12
+ export { formatRuntimeLaunchDiagnostic, redactLaunchArgument, redactLaunchText, RuntimeLaunchFailure, toRuntimeLaunchFailure } from "./launchDiagnostics.js";
11
13
  export { DEFAULT_FORCED_GRACE_MS, DEFAULT_GRACEFUL_GRACE_MS, terminateSessionOwners } from "./sessionTerminationGuard.js";
12
14
  export { ProviderContinuationReconciliationService } from "./providerContinuationReconciliationService.js";
13
15
  export { codexNotificationBoundary, CodexAppServerRequestError, CodexAppServerRuntime } from "./codexAppServerRuntime.js";
@@ -0,0 +1,154 @@
1
+ import { CommandExecutionError } from "../tmux/commandExecutor.js";
2
+ export const RUNTIME_LAUNCH_PHASES = [
3
+ "validation",
4
+ "host-start",
5
+ "host-started",
6
+ "native-session-discovery",
7
+ "host-stop",
8
+ "delivery"
9
+ ];
10
+ export const RUNTIME_LAUNCH_KINDS = [
11
+ "config",
12
+ "auth",
13
+ "executable",
14
+ "tmux",
15
+ "timeout",
16
+ "provider",
17
+ "unknown"
18
+ ];
19
+ const MAX_ARGUMENT_CHARS = 1_000;
20
+ const MAX_STDERR_CHARS = 4_000;
21
+ const MAX_DETAIL_CHARS = 1_000;
22
+ const SECRET_VALUE_PATTERN = /(api[_-]?key|access[_-]?token|refresh[_-]?token|secret|password|passwd|cookie|authorization)(\s*[=:]\s*)([^\s,;]+)/gi;
23
+ // Word boundary keeps "task-5-…" workspace paths from being mistaken for keys.
24
+ const OPENAI_KEY_PATTERN = /\bsk-[A-Za-z0-9_-]{6,}/gu;
25
+ /** A bounded, single-line, secret-redacted launch failure for Run summaries. */
26
+ export class RuntimeLaunchFailure extends Error {
27
+ diagnostic;
28
+ name = "RuntimeLaunchFailure";
29
+ constructor(diagnostic) {
30
+ super(formatRuntimeLaunchDiagnostic(diagnostic));
31
+ this.diagnostic = diagnostic;
32
+ }
33
+ }
34
+ export function toRuntimeLaunchFailure(error, phase, context = {}) {
35
+ if (error instanceof RuntimeLaunchFailure)
36
+ return error;
37
+ const commandError = error instanceof CommandExecutionError ? error : undefined;
38
+ const stderrTail = tail(redactLaunchText(context.stderrTail ?? commandError?.stderr ?? ""), MAX_STDERR_CHARS);
39
+ const kind = classifyLaunchFailure(phase, error, stderrTail);
40
+ const exitStatus = context.exitStatus ?? commandError?.exitStatus;
41
+ const detail = tail(redactLaunchText(error instanceof Error ? error.message : String(error)), MAX_DETAIL_CHARS);
42
+ return new RuntimeLaunchFailure({
43
+ phase,
44
+ kind,
45
+ ...(context.command === undefined ? {} : { command: redactLaunchText(context.command) }),
46
+ ...(context.argv === undefined
47
+ ? {}
48
+ : { argv: context.argv.map((argument) => redactLaunchArgument(argument)) }),
49
+ ...(context.cwd === undefined ? {} : { cwd: context.cwd }),
50
+ ...(exitStatus === undefined
51
+ ? {}
52
+ : { exitStatus }),
53
+ ...(context.signal === undefined ? {} : { signal: context.signal }),
54
+ ...(stderrTail.length === 0 ? {} : { stderrTail }),
55
+ ...(context.pane === undefined ? {} : { pane: context.pane }),
56
+ ...(detail.length === 0 ? {} : { detail }),
57
+ ...(launchHint(kind, context.agentId) === undefined
58
+ ? {}
59
+ : { hint: launchHint(kind, context.agentId) })
60
+ });
61
+ }
62
+ export function formatRuntimeLaunchDiagnostic(diagnostic) {
63
+ const fields = [
64
+ `failurePhase=${diagnostic.phase}`,
65
+ `failureKind=${diagnostic.kind}`
66
+ ];
67
+ if (diagnostic.command !== undefined)
68
+ fields.push(`command=${JSON.stringify(diagnostic.command)}`);
69
+ if (diagnostic.argv !== undefined)
70
+ fields.push(`argv=${JSON.stringify(diagnostic.argv)}`);
71
+ if (diagnostic.cwd !== undefined)
72
+ fields.push(`cwd=${JSON.stringify(diagnostic.cwd)}`);
73
+ if (diagnostic.exitStatus !== undefined)
74
+ fields.push(`exitStatus=${diagnostic.exitStatus}`);
75
+ if (diagnostic.signal !== undefined)
76
+ fields.push(`signal=${JSON.stringify(diagnostic.signal)}`);
77
+ if (diagnostic.stderrTail !== undefined) {
78
+ fields.push(`stderrTail=${JSON.stringify(tail(diagnostic.stderrTail, MAX_STDERR_CHARS))}`);
79
+ }
80
+ if (diagnostic.pane !== undefined)
81
+ fields.push(`pane=${JSON.stringify(diagnostic.pane)}`);
82
+ if (diagnostic.detail !== undefined) {
83
+ fields.push(`detail=${JSON.stringify(tail(diagnostic.detail, MAX_DETAIL_CHARS))}`);
84
+ }
85
+ if (diagnostic.hint !== undefined)
86
+ fields.push(`hint=${JSON.stringify(diagnostic.hint)}`);
87
+ return `Role Run could not start: ${fields.join(" ")}`;
88
+ }
89
+ export function redactLaunchArgument(value) {
90
+ return tail(redactLaunchText(value), MAX_ARGUMENT_CHARS);
91
+ }
92
+ export function redactLaunchText(value) {
93
+ return value
94
+ .replace(OPENAI_KEY_PATTERN, "[REDACTED]")
95
+ .replace(SECRET_VALUE_PATTERN, "$1$2[REDACTED]");
96
+ }
97
+ function classifyLaunchFailure(phase, error, stderrTail) {
98
+ if ((error instanceof CommandExecutionError && error.code === "COMMAND_NOT_FOUND")
99
+ || /command not found|no such file or directory/i.test(stderrTail)) {
100
+ return "executable";
101
+ }
102
+ if (isConfigFailure(stderrTail))
103
+ return "config";
104
+ if (isAuthFailure(stderrTail))
105
+ return "auth";
106
+ if (phase === "host-start")
107
+ return "tmux";
108
+ if (phase === "native-session-discovery")
109
+ return "timeout";
110
+ if (phase === "validation" || phase === "delivery")
111
+ return "config";
112
+ return "provider";
113
+ }
114
+ function isConfigFailure(stderrTail) {
115
+ return /unknown model|invalid effort|unknown option|invalid value|unexpected argument|invalid .*config|model .*not (?:found|supported)|effort .*not (?:valid|supported)/i
116
+ .test(stderrTail);
117
+ }
118
+ function isAuthFailure(stderrTail) {
119
+ return /\bauth(?:entication|orization)?\b|unauthorized|forbidden|credential|api[_-]?key|login|sign in|token/i
120
+ .test(stderrTail);
121
+ }
122
+ /**
123
+ * Conservative detection of fatal error signatures in agent output during
124
+ * launch. These are errors the agent cannot recover from without
125
+ * intervention (missing executable, invalid configuration, authentication
126
+ * failure). Transient errors (network retries, temporary blips) are not
127
+ * matched. Failure-kind classification uses the broader patterns in
128
+ * classifyLaunchFailure once a fatal output is confirmed.
129
+ */
130
+ export function hasFatalLaunchOutput(output) {
131
+ return /command not found|no such file or directory/i.test(output)
132
+ || /unknown model|invalid effort|unknown option|invalid value|unexpected argument/i.test(output)
133
+ || /401\s+unauthorized|403\s+forbidden|authentication failed|not logged in|sign in with/i
134
+ .test(output);
135
+ }
136
+ function launchHint(kind, agentId) {
137
+ switch (kind) {
138
+ case "config":
139
+ return agentId === undefined
140
+ ? "Verify the Provider model and effort configuration."
141
+ : `Verify custom model/effort with yui agent capabilities ${agentId}.`;
142
+ case "auth":
143
+ return "Verify Provider authentication and the Agent environment.";
144
+ case "executable":
145
+ return "Verify the Provider command is installed and on PATH.";
146
+ case "timeout":
147
+ return "Verify Provider lifecycle hooks and Controller connectivity.";
148
+ default:
149
+ return undefined;
150
+ }
151
+ }
152
+ function tail(value, maxChars) {
153
+ return value.length <= maxChars ? value : value.slice(value.length - maxChars);
154
+ }
@@ -2,6 +2,19 @@ import { mailboxHasWork } from "../coordination/workMailbox.js";
2
2
  export const RUNTIME_LIFECYCLE_OWNER = "runtime-lifecycle";
3
3
  export const RUNTIME_LAUNCH_RESERVED_REASON = "runtime-launch-reserved";
4
4
  export const RUNTIME_CLEANUP_REQUIRED_REASON = "runtime-cleanup-required";
5
+ /**
6
+ * A Role runtime lifecycle lane already holds an in-flight operation (a
7
+ * launch reservation or a cleanup obligation). This is scheduler
8
+ * backpressure: the equivalent wake/Run must be retried after the lane
9
+ * settles. It is never grounds to terminalize a Run, because the contention
10
+ * happens before (or outside) any semantic Run launch.
11
+ */
12
+ export class RuntimeLifecycleBusyError extends Error {
13
+ name = "RuntimeLifecycleBusyError";
14
+ constructor(message) {
15
+ super(message);
16
+ }
17
+ }
5
18
  export function runtimeLifecycleTarget(owner) {
6
19
  return owner.scope === "task"
7
20
  ? {
@@ -24,6 +24,7 @@ export function createProviderContinuation(input) {
24
24
  attachment: input.attachment,
25
25
  observation: input.observation,
26
26
  mayWriteWorkspace: input.mayWriteWorkspace,
27
+ durability: "best-effort",
27
28
  reports: [],
28
29
  ...(input.providerSequence === undefined ? {} : { lastProviderSequence: input.providerSequence }),
29
30
  identityConflict: false,
@@ -40,9 +41,17 @@ export function recordProviderReport(raw, report, providerSequence) {
40
41
  const normalized = validateProviderReport(report);
41
42
  if (current.reports.some((entry) => entry.reportId === normalized.reportId))
42
43
  return current;
44
+ // Idempotent durable-result receipt: a replay carrying the same content
45
+ // digest must not create a second report even when the Provider assigns a
46
+ // fresh report id.
47
+ if (normalized.resultDigest !== undefined
48
+ && current.reports.some((entry) => entry.resultDigest === normalized.resultDigest)) {
49
+ return current;
50
+ }
43
51
  return validateProviderContinuation({
44
52
  ...current,
45
53
  reports: [...current.reports, normalized],
54
+ ...(normalized.resultDigest === undefined ? {} : { durability: "durable-result" }),
46
55
  ...(providerSequence === undefined ? {} : { lastProviderSequence: providerSequence }),
47
56
  updatedAt: normalized.observedAt
48
57
  });
@@ -160,10 +169,21 @@ export function validateProviderContinuation(value) {
160
169
  if (typeof value.mayWriteWorkspace !== "boolean" || typeof value.identityConflict !== "boolean") {
161
170
  throw new Error("Provider Continuation flags are invalid.");
162
171
  }
172
+ if (value.durability !== "best-effort" && value.durability !== "durable-result") {
173
+ throw new Error("Provider Continuation durability is invalid.");
174
+ }
163
175
  const reports = value.reports.map(validateProviderReport);
164
176
  if (new Set(reports.map((entry) => entry.reportId)).size !== reports.length) {
165
177
  throw new Error("Provider Continuation reports contain duplicate identity.");
166
178
  }
179
+ if (value.durability === "durable-result"
180
+ && !reports.some((entry) => entry.resultDigest !== undefined)) {
181
+ throw new Error("Durable-result Provider Continuation requires a result digest receipt.");
182
+ }
183
+ if (value.durability === "best-effort"
184
+ && reports.some((entry) => entry.resultDigest !== undefined)) {
185
+ throw new Error("Best-effort Provider Continuation must not carry a result digest receipt.");
186
+ }
167
187
  timestamp(value.createdAt, "Provider Continuation createdAt");
168
188
  timestamp(value.updatedAt, "Provider Continuation updatedAt");
169
189
  if (value.settledAt !== undefined) {
@@ -197,9 +217,27 @@ function validateProviderReport(value) {
197
217
  ...(value.providerDeliveryRef === undefined
198
218
  ? {}
199
219
  : { providerDeliveryRef: text(value.providerDeliveryRef, "Provider delivery ref") }),
220
+ ...(value.resultDigest === undefined
221
+ ? {}
222
+ : { resultDigest: digest(value.resultDigest) }),
223
+ ...(value.resultSize === undefined
224
+ ? {}
225
+ : { resultSize: reportSize(value.resultSize) }),
200
226
  observedAt: timestamp(value.observedAt, "Provider report observedAt")
201
227
  };
202
228
  }
229
+ function digest(value) {
230
+ if (!/^[0-9a-f]{64}$/.test(value)) {
231
+ throw new Error("Provider report result digest must be a sha256 hex string.");
232
+ }
233
+ return value;
234
+ }
235
+ function reportSize(value) {
236
+ if (!Number.isSafeInteger(value) || value < 0) {
237
+ throw new Error("Provider report result size is invalid.");
238
+ }
239
+ return value;
240
+ }
203
241
  function providerSequenceRegresses(current, incoming) {
204
242
  if (incoming === undefined)
205
243
  return false;