@zq-silk/yui 0.15.9 → 0.15.12

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 (160) hide show
  1. package/ARCHITECTURE.md +8 -4
  2. package/ARCHITECTURE.zh-CN.md +5 -2
  3. package/README.md +13 -5
  4. package/dist/agent/launchEnvironment.js +7 -0
  5. package/dist/agentRun/agentRun.js +3 -0
  6. package/dist/cli/commandCatalog.js +64 -16
  7. package/dist/cli/interactionPolicy.js +7 -3
  8. package/dist/cli/managedDiagnostics.js +1 -1
  9. package/dist/cli/updateOrchestrator.js +24 -1
  10. package/dist/cli/updatePorts.js +7 -3
  11. package/dist/cli/upgradeCommand.js +42 -2
  12. package/dist/cli.js +381 -107
  13. package/dist/commands/executionAuditCommands.js +10 -0
  14. package/dist/commands/globalRoleCommands.js +339 -4
  15. package/dist/commands/projectCommands.js +50 -22
  16. package/dist/commands/releaseCommands.js +18 -0
  17. package/dist/commands/taskActor.js +25 -0
  18. package/dist/commands/taskCommands.js +586 -96
  19. package/dist/commands/taskIntegrationCommands.js +19 -39
  20. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  21. package/dist/commands/taskOverviewCommand.js +4 -3
  22. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  23. package/dist/commands/taskPublicationCommands.js +11 -2
  24. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  25. package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
  26. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  27. package/dist/context/runContextPack.js +3 -0
  28. package/dist/context/taskCatalog.js +187 -0
  29. package/dist/context/taskContext.js +55 -6
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +14 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
  34. package/dist/controller/globalInputDelivery.js +132 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/providerRetryAdmission.js +100 -0
  37. package/dist/controller/providerRetryDelivery.js +218 -0
  38. package/dist/controller/resourceInventory.js +14 -4
  39. package/dist/controller/resourceInventoryLinux.js +2 -6
  40. package/dist/controller/runtime.js +117 -7
  41. package/dist/controller/runtimeEventInbox.js +32 -3
  42. package/dist/controller/runtimeEventProcessor.js +26 -6
  43. package/dist/controller/runtimeHookRunFence.js +75 -19
  44. package/dist/controller/structuredProviderObservation.js +133 -70
  45. package/dist/coordination/workMailboxQueue.js +5 -0
  46. package/dist/execution/workItemExecutionProjection.js +1 -1
  47. package/dist/executor/agentExecutor.js +64 -4
  48. package/dist/executor/executorRegistry.js +3 -0
  49. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  50. package/dist/integration/deliveryObligation.js +2 -1
  51. package/dist/integration/gitIntegrationService.js +329 -386
  52. package/dist/integration/integrationAttempt.js +30 -4
  53. package/dist/integration/integrationQueueService.js +7 -7
  54. package/dist/integration/integrationSourceApplication.js +323 -0
  55. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  56. package/dist/message/globalInterrupt.js +33 -0
  57. package/dist/message/globalProviderRetry.js +15 -0
  58. package/dist/message/inputControlResolution.js +106 -0
  59. package/dist/message/message.js +367 -0
  60. package/dist/message/messageContinuation.js +126 -3
  61. package/dist/message/taskInterrupt.js +34 -0
  62. package/dist/observability/executionAudit.js +19 -0
  63. package/dist/observability/orchestrationMetrics.js +1 -1
  64. package/dist/release/releaseHandover.js +22 -0
  65. package/dist/release/releaseWorkflowPorts.js +15 -7
  66. package/dist/repository/gitWorkspace.js +430 -107
  67. package/dist/repository/projectMaintenanceLock.js +75 -18
  68. package/dist/repository/taskWorkspaceCoordinator.js +182 -101
  69. package/dist/repository/taskWorkspacePreparer.js +205 -72
  70. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  71. package/dist/repository/workspaceCleanupInspection.js +187 -0
  72. package/dist/resources/resourceDiscovery.js +3 -2
  73. package/dist/runtime/agentError.js +5 -3
  74. package/dist/runtime/agentHost.js +179 -82
  75. package/dist/runtime/agentHostCompatibility.js +127 -0
  76. package/dist/runtime/agentHostProtocol.js +53 -0
  77. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  78. package/dist/runtime/codexAppServerRuntime.js +34 -3
  79. package/dist/runtime/executionEnvironment.js +0 -19
  80. package/dist/runtime/launchBroker.js +6 -0
  81. package/dist/runtime/providerControl.js +5 -1
  82. package/dist/runtime/providerRetry.js +198 -0
  83. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  84. package/dist/runtime/sessionReconciliation.js +4 -4
  85. package/dist/runtime/sessionTokenMetrics.js +15 -5
  86. package/dist/runtime/structuredProviderHost.js +6 -2
  87. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  88. package/dist/runtime/taskUsageMetrics.js +275 -0
  89. package/dist/runtime/tmuxAdapters.js +5 -3
  90. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  91. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  92. package/dist/scheduler/operatorEvent.js +4 -0
  93. package/dist/scheduler/taskExecutionProjection.js +38 -6
  94. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  95. package/dist/scheduler/wakeReason.js +7 -1
  96. package/dist/scheduler/wakeupQueue.js +2 -0
  97. package/dist/setup/setupCommand.js +26 -8
  98. package/dist/storage/homeLayout.js +130 -0
  99. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  100. package/dist/storage/migrations/integrationContinuation.js +104 -0
  101. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  102. package/dist/storage/sqliteSchema.js +167 -4
  103. package/dist/storage/sqliteStore.js +57 -1
  104. package/dist/storage/storageVersions.js +1 -1
  105. package/dist/storage/storeRpc.js +2 -0
  106. package/dist/storage/taskCatalog.js +123 -0
  107. package/dist/storage/taskStore.js +2 -0
  108. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  109. package/dist/task/archiveDiagnostics.js +129 -0
  110. package/dist/task/archivePreflight.js +124 -0
  111. package/dist/task/nextAction.js +44 -11
  112. package/dist/task/publicationAdoption.js +56 -0
  113. package/dist/task/publicationReference.js +10 -0
  114. package/dist/task/remoteDelivery.js +31 -16
  115. package/dist/web/assets/client/app.js +147 -17
  116. package/dist/web/assets/client/components.js +56 -13
  117. package/dist/web/assets/client/i18n.js +78 -4
  118. package/dist/web/assets/client/taskSurface.js +108 -1
  119. package/dist/web/assets/client/view.js +39 -8
  120. package/dist/web/assets/shell.js +29 -0
  121. package/dist/web/assets/styles/layout.js +8 -1
  122. package/dist/web/assets/styles/widgets.js +12 -0
  123. package/dist/web/webServer.js +131 -4
  124. package/dist/web/webSnapshot.js +16 -6
  125. package/dist/web/webTaskSurface.js +222 -5
  126. package/dist/workspace/cleanupInspection.js +63 -0
  127. package/dist/workspace/workItemChangeSetManager.js +111 -35
  128. package/docs/agent-result-consumption.md +4 -0
  129. package/docs/agent-result-consumption.zh-CN.md +3 -0
  130. package/docs/agent-runtime-drivers.md +7 -0
  131. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  132. package/docs/architecture/README.md +2 -0
  133. package/docs/architecture/README.zh-CN.md +3 -1
  134. package/docs/architecture/capabilities-and-resources.md +30 -5
  135. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  136. package/docs/managed-turn-and-session-runtime.md +47 -0
  137. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  138. package/docs/observability/README.md +62 -0
  139. package/docs/observability/README.zh-CN.md +47 -0
  140. package/docs/project-refresh.md +77 -0
  141. package/docs/project-refresh.zh-CN.md +59 -0
  142. package/docs/provider-retry.md +70 -0
  143. package/docs/release-workflow.md +39 -0
  144. package/docs/release-workflow.zh-CN.md +29 -0
  145. package/docs/sqlite-control-plane-design.md +223 -1
  146. package/docs/task-delivery.md +133 -13
  147. package/docs/task-delivery.zh-CN.md +99 -10
  148. package/docs/task-discovery.md +102 -0
  149. package/docs/task-discovery.zh-CN.md +86 -0
  150. package/docs/testing/verification-levels.md +40 -0
  151. package/docs/testing/verification-levels.zh-CN.md +23 -0
  152. package/i18n/README.zh-CN.md +13 -7
  153. package/package.json +1 -1
  154. package/skills/yui-leader/references/execution.md +154 -51
  155. package/skills/yui-leader/references/integration.md +52 -2
  156. package/skills/yui-operator/SKILL.md +19 -3
  157. package/skills/yui-reviewer/SKILL.md +4 -0
  158. package/skills/yui-runtime/SKILL.md +42 -0
  159. package/skills/yui-runtime/references/publication.md +42 -0
  160. package/skills/yui-runtime/references/recovery.md +24 -0
@@ -0,0 +1,53 @@
1
+ import { createSessionOwnerIdentity } from "./sessionOwnerIdentity.js";
2
+ /** Independent of CLI/RPC and Home schema. Keep v1 readable while v1 Hosts live. */
3
+ export const AGENT_HOST_EVENT_PROTOCOL = "yui-agent-host-events/v1";
4
+ export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v5";
5
+ export function readAgentHostObservationSource(value) {
6
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
7
+ throw new Error("Agent Host observation source is invalid.");
8
+ }
9
+ const source = value;
10
+ if (source.protocol !== AGENT_HOST_EVENT_PROTOCOL) {
11
+ throw new Error("Agent Host event protocol is unsupported.");
12
+ }
13
+ if (source.connection !== undefined && (source.connection === null
14
+ || typeof source.connection !== "object" || Array.isArray(source.connection))) {
15
+ throw new Error("Agent Host connection evidence is invalid.");
16
+ }
17
+ if (source.connection?.processOwner !== undefined
18
+ && (source.connection.processOwner.schemaVersion !== 2
19
+ || source.connection.processOwner.kind !== "yui-session-owner")) {
20
+ throw new Error("Agent Host process custody wire contract is invalid.");
21
+ }
22
+ const text = (value) => {
23
+ if (typeof value !== "string" || !value.trim() || value.includes("\0") || value.length > 4096) {
24
+ throw new Error("Agent Host observation identity is invalid.");
25
+ }
26
+ return value;
27
+ };
28
+ return {
29
+ protocol: AGENT_HOST_EVENT_PROTOCOL,
30
+ adapterId: text(source.adapterId),
31
+ workspace: text(source.workspace),
32
+ ...(source.startupRunId === undefined ? {} : { startupRunId: text(source.startupRunId) }),
33
+ ...(source.connection === undefined ? {} : {
34
+ connection: {
35
+ ...(source.connection.processOwner === undefined ? {} : {
36
+ processOwner: createSessionOwnerIdentity({
37
+ ...source.connection.processOwner,
38
+ recordedAt: new Date(source.connection.processOwner.recordedAt)
39
+ })
40
+ }),
41
+ ...(source.connection.account === undefined ? {} : {
42
+ account: {
43
+ home: text(source.connection.account.home),
44
+ codexHome: text(source.connection.account.codexHome),
45
+ ...(source.connection.account.nativeAccountHome === undefined ? {} : {
46
+ nativeAccountHome: text(source.connection.account.nativeAccountHome)
47
+ })
48
+ }
49
+ })
50
+ }
51
+ })
52
+ };
53
+ }
@@ -1,5 +1,8 @@
1
1
  /** Provider-specific Claude Code failure recognition. */
2
2
  export function mapClaudeAgentError(input) {
3
+ return withTransientEvidence(mapClaudeDisplayError(input), input);
4
+ }
5
+ function mapClaudeDisplayError(input) {
3
6
  const text = `${input.message}\n${input.raw}`;
4
7
  if (/^No conversation found with session ID: [A-Za-z0-9-]+\.?$/imu.test(input.message)) {
5
8
  return sessionUnavailable("provider.session-not-found");
@@ -23,6 +26,9 @@ export function mapClaudeAgentError(input) {
23
26
  }
24
27
  /** Provider-specific Codex failure recognition. */
25
28
  export function mapCodexAgentError(input) {
29
+ return withTransientEvidence(mapCodexDisplayError(input), input);
30
+ }
31
+ function mapCodexDisplayError(input) {
26
32
  const text = `${input.message}\n${input.raw}`;
27
33
  if (/^server_error$/iu.test(input.message)) {
28
34
  return recoverable("availability", "provider.server-error");
@@ -200,3 +206,88 @@ function sessionUnavailable(code) {
200
206
  sessionDisposition: "unrecoverable"
201
207
  });
202
208
  }
209
+ /** Replay qualification is stricter than the readable, best-effort category.
210
+ * Never promote a number occurring in provider prose to submission safety. */
211
+ function structuredTransientError(input) {
212
+ if (/insufficient[_ -]quota|quota.{0,25}(exhaust|exceed)|billing[_ -]hard[_ -]limit|usageLimitExceeded|credits?.{0,20}(exhaust|insufficient)/iu.test(`${input.message}\n${input.raw}`))
213
+ return { category: "access", code: "provider.quota-exhausted", retryable: false };
214
+ const nodes = [];
215
+ const visit = (value, depth) => {
216
+ if (depth > 8 || value === null || typeof value !== "object" || Array.isArray(value))
217
+ return;
218
+ const node = value;
219
+ nodes.push(node);
220
+ for (const key of ["cause", "error", "data", "codexErrorInfo", "responseTooManyFailedAttempts",
221
+ "httpConnectionFailed", "responseStreamConnectionFailed", "responseStreamDisconnected", "error_details"]) {
222
+ visit(node[key], depth + 1);
223
+ }
224
+ };
225
+ try {
226
+ visit(JSON.parse(input.raw), 0);
227
+ }
228
+ catch { /* Display classification may still use prose. */ }
229
+ const codes = nodes.flatMap(n => [n.code, n.type, n.error, n.codexErrorInfo]).filter(v => typeof v === "string");
230
+ if (codes.some(c => ["authentication_error", "permission_error", "invalid_api_key", "invalid_model",
231
+ "model_not_found", "usage_limit_exceeded", "insufficient_quota"].includes(c))) {
232
+ return { category: "access", code: "provider.access-denied", retryable: false };
233
+ }
234
+ const statuses = nodes.flatMap(n => [n.httpStatusCode, n.statusCode, n.status]).filter(v => typeof v === "number");
235
+ if (statuses.some(s => s === 401 || s === 403 || s === 400 || s === 404)) {
236
+ return { category: "invalid-request", code: "provider.http-4xx", retryable: false };
237
+ }
238
+ const nativeCode = input.message.trim();
239
+ let result;
240
+ if (statuses.includes(429) || codes.includes("rate_limit_error") || nativeCode === "rate_limit_error") {
241
+ result = recoverable("rate-limit", "provider.rate-limit");
242
+ }
243
+ else if (statuses.some(s => [500, 502, 503, 504].includes(s))
244
+ || codes.some(c => ["server_error", "overloaded_error"].includes(c))
245
+ || ["server_error", "overloaded_error"].includes(nativeCode)) {
246
+ result = recoverable("availability", "provider.server-error");
247
+ }
248
+ else if (codes.some(c => ["ECONNRESET", "ECONNREFUSED", "EAI_AGAIN", "ETIMEDOUT", "EPIPE"].includes(c))) {
249
+ result = recoverable("transport", "transport.temporary-failure");
250
+ }
251
+ if (result === undefined)
252
+ return undefined;
253
+ const waits = [];
254
+ for (const node of nodes) {
255
+ for (const value of [node.retry_after_ms, node.retryAfterMs]) {
256
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0)
257
+ waits.push(value);
258
+ }
259
+ const headers = node.headers;
260
+ if (headers === null || typeof headers !== "object" || Array.isArray(headers))
261
+ continue;
262
+ const entries = Object.entries(headers);
263
+ const value = entries.find(([k]) => k.toLowerCase() === "retry-after")?.[1];
264
+ if (typeof value !== "string")
265
+ continue;
266
+ if (/^\d+(?:\.\d+)?$/u.test(value.trim()))
267
+ waits.push(Math.ceil(Number(value) * 1_000));
268
+ else {
269
+ const date = Date.parse(value);
270
+ if (Number.isFinite(date))
271
+ waits.push(Math.max(0, date - Date.now()));
272
+ }
273
+ }
274
+ const retryAfterMs = Math.max(...waits.filter(Number.isSafeInteger));
275
+ return { ...result, retryable: true,
276
+ ...(Number.isFinite(retryAfterMs) ? { retryAfterMs } : {}) };
277
+ }
278
+ /** Add positive evidence without renaming established diagnostics or letting
279
+ * a transient nested cause override a known fatal Session/request failure. */
280
+ function withTransientEvidence(base, input) {
281
+ const evidence = structuredTransientError(input);
282
+ if (evidence === undefined)
283
+ return base;
284
+ const possiblyTransient = ["rate-limit", "availability", "transport", "unknown"].includes(base.category);
285
+ if (evidence.retryable === false)
286
+ return possiblyTransient ? evidence : { ...base, retryable: false };
287
+ if (!possiblyTransient || base.sessionDisposition === "unrecoverable")
288
+ return base;
289
+ return {
290
+ ...(base.category === "unknown" ? evidence : base), retryable: true,
291
+ ...(evidence.retryAfterMs === undefined ? {} : { retryAfterMs: evidence.retryAfterMs })
292
+ };
293
+ }
@@ -1,4 +1,5 @@
1
1
  import { YUI_VERSION } from "../version.js";
2
+ import { serializeAgentErrorRaw } from "./agentError.js";
2
3
  /**
3
4
  * Preserve the native non-interactive Codex identity used by our historical
4
5
  * execution adapter. A proxy's environment cannot set the daemon's originator;
@@ -94,6 +95,10 @@ export class CodexAppServerRuntime {
94
95
  conversationId: input.conversationId,
95
96
  text: input.text,
96
97
  expectedNoActiveTurn: input.expectedNoActiveTurn,
98
+ ...(input.requireQuiescent ? { requireQuiescent: true } : {}),
99
+ ...(input.expectedFailedNativeTurnId === undefined ? {} : {
100
+ expectedFailedNativeTurnId: input.expectedFailedNativeTurnId
101
+ }),
97
102
  clientUserMessageId: input.attemptId
98
103
  });
99
104
  }
@@ -101,11 +106,13 @@ export class CodexAppServerRuntime {
101
106
  const requestedThreadId = text(input.conversationId, "Codex thread id");
102
107
  let threadId = requestedThreadId;
103
108
  try {
109
+ const expectedFailedTurn = input.expectedFailedNativeTurnId === undefined ? undefined
110
+ : text(input.expectedFailedNativeTurnId, "Expected failed Codex Turn id");
104
111
  // Submission needs current availability, not historical Turns. Some
105
112
  // App Server transports expose status without supporting history reads.
106
113
  const snapshot = await this.readConversation(requestedThreadId, { includeTurns: false });
107
114
  threadId = snapshot.threadId;
108
- if (input.expectedNoActiveTurn
115
+ if ((input.expectedNoActiveTurn || expectedFailedTurn !== undefined || input.requireQuiescent)
109
116
  && (snapshot.status === "active" || snapshot.activeTurnId !== undefined)) {
110
117
  return {
111
118
  status: "busy",
@@ -115,9 +122,30 @@ export class CodexAppServerRuntime {
115
122
  : `active-turn:${snapshot.activeTurnId}`
116
123
  };
117
124
  }
118
- if (snapshot.status !== "idle" && snapshot.status !== "active") {
125
+ if (snapshot.status !== "idle" && snapshot.status !== "active"
126
+ && !(snapshot.status === "systemError" && expectedFailedTurn !== undefined)) {
119
127
  throw new Error(`Codex Session availability is ${snapshot.status}; no input was submitted.`);
120
128
  }
129
+ if (expectedFailedTurn !== undefined) {
130
+ // systemError alone proves neither terminal execution nor permission
131
+ // to replay. Only the exact latest failed Turn permits this recovery.
132
+ const page = await this.transport.request("thread/turns/list", {
133
+ threadId, limit: 1, sortDirection: "desc", itemsView: "notLoaded"
134
+ });
135
+ const latest = object(arrayMember(page, "data")[0]);
136
+ if (latest?.id !== expectedFailedTurn || latest.status !== "failed") {
137
+ throw new Error("Codex retry has no exact failed latest Turn proof; no input was submitted.");
138
+ }
139
+ }
140
+ if (expectedFailedTurn !== undefined || input.requireQuiescent) {
141
+ const backgrounds = await this.transport.request("thread/backgroundTerminals/list", {
142
+ threadId, limit: 1
143
+ });
144
+ if (!Array.isArray(backgrounds.data) || backgrounds.data.length !== 0
145
+ || backgrounds.nextCursor !== null) {
146
+ throw new Error("Codex retry background execution is not proven empty; no input was submitted.");
147
+ }
148
+ }
121
149
  }
122
150
  catch (error) {
123
151
  throw new CodexPreSubmissionError("Codex Session inspection failed before input submission.", { cause: error });
@@ -304,7 +332,10 @@ function parseThreadSnapshot(result, expectedThreadId, loaded) {
304
332
  turnId,
305
333
  ...(status === undefined ? {} : { status }),
306
334
  ...(output === undefined ? {} : { output }),
307
- ...(error === undefined ? {} : { error })
335
+ ...(error === undefined ? {} : { error }),
336
+ ...(turn.error === undefined || turn.error === null ? {} : {
337
+ rawError: serializeAgentErrorRaw(turn.error)
338
+ })
308
339
  }];
309
340
  });
310
341
  return {
@@ -1,5 +1,4 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
- import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
3
2
  import { createProjectResources } from "../resources/projectResourceService.js";
4
3
  /** Revalidate the adopted owner; a saved path alone never authorizes execution. */
5
4
  export function assertExecutionEnvironmentCurrent(store, taskId, snapshot) {
@@ -10,21 +9,3 @@ export function assertExecutionEnvironmentCurrent(store, taskId, snapshot) {
10
9
  throw new Error("Execution environment changed; select an adopted environment and start a new Session.");
11
10
  }
12
11
  }
13
- /** Last local check before native process creation or another Provider input. */
14
- export function assertAgentExecutionEnvironment(home, payload) {
15
- const snapshot = payload.executionEnvironment;
16
- if (snapshot === undefined)
17
- return;
18
- if (payload.environment.YUI_SESSION_SCOPE !== "task"
19
- || payload.environment.YUI_TASK_ID !== snapshot.taskId
20
- || payload.cwd !== snapshot.directory.path) {
21
- throw new Error("Agent launch does not match its adopted execution environment.");
22
- }
23
- const store = openCurrentTaskStore(home);
24
- try {
25
- assertExecutionEnvironmentCurrent(store, snapshot.taskId, snapshot);
26
- }
27
- finally {
28
- store.close();
29
- }
30
- }
@@ -60,6 +60,12 @@ function validatePayload(payload) {
60
60
  throw new Error("Agent Host launch payload version is invalid.");
61
61
  text(payload.command, "command");
62
62
  text(payload.cwd, "cwd");
63
+ if (payload.startupRunId !== undefined) {
64
+ text(payload.startupRunId, "startup Run id");
65
+ if (payload.environment?.YUI_SESSION_SCOPE !== "task") {
66
+ throw new Error("Only a Task launch may carry a startup Run identity.");
67
+ }
68
+ }
63
69
  if (!Array.isArray(payload.args))
64
70
  throw new Error("Agent Host launch args must be an array.");
65
71
  payload.args.forEach((value) => text(value, "argument"));
@@ -14,7 +14,11 @@ export class FencedProviderControl {
14
14
  conversationId: input.fence.conversationId,
15
15
  attemptId: identity(input.attemptId, "Provider input attempt id"),
16
16
  text: text(input.text, "Provider Turn input"),
17
- expectedNoActiveTurn: input.expectedNoActiveTurn ?? true
17
+ expectedNoActiveTurn: input.expectedNoActiveTurn ?? true,
18
+ ...(input.requireQuiescent ? { requireQuiescent: true } : {}),
19
+ ...(input.expectedFailedNativeTurnId === undefined ? {} : {
20
+ expectedFailedNativeTurnId: identity(input.expectedFailedNativeTurnId, "Expected failed Provider Turn id")
21
+ })
18
22
  });
19
23
  }
20
24
  async interruptTurn(input) {
@@ -0,0 +1,198 @@
1
+ export const PROVIDER_RETRY_LIMIT = 5;
2
+ export const PROVIDER_RETRY_BUDGET_MS = 600_000;
3
+ export function providerRetryPending(binding) {
4
+ return binding?.retry?.status === "waiting" || binding?.retry?.status === "in-flight";
5
+ }
6
+ /** Called only in the transaction recording the exact failed/rejected input. */
7
+ export function recordProviderFailure(binding, input) {
8
+ let turn = binding.run;
9
+ const previous = binding.retry;
10
+ if (turn === null || !["failed", "rejected", "delivery-unknown"].includes(turn.status))
11
+ return binding;
12
+ turn = { ...turn, failure: { error: input.error, ref: input.failureRef } };
13
+ binding = { ...binding, run: turn };
14
+ if (turn.status === "delivery-unknown")
15
+ return binding;
16
+ if (previous?.currentAttemptId === turn.attemptId
17
+ && ["cancelled", "exhausted", "needs-attention"].includes(previous.status))
18
+ return binding;
19
+ if (previous?.failedAttemptId === turn.attemptId && previous.status !== "in-flight")
20
+ return binding;
21
+ if (input.error.retryable !== true || input.error.inputDisposition === "unknown"
22
+ || input.error.sessionDisposition !== "recoverable"
23
+ || binding.authority.owner !== "controller" || binding.retryDisabled === true
24
+ || turn.status === "failed" && (turn.nativeTurnId === undefined || input.error.inputDisposition !== "accepted")) {
25
+ return providerRetryPending(binding)
26
+ ? cancelProviderRetry(binding, "failure-not-safely-retryable", input.at) : binding;
27
+ }
28
+ const continuing = previous?.status === "in-flight" && previous.currentAttemptId === turn.attemptId;
29
+ const originalInput = continuing ? previous.input : turn.input;
30
+ if (originalInput === undefined)
31
+ return binding; // no owned input: never replay arbitrary native chat
32
+ const current = {
33
+ policyVersion: 1,
34
+ chainId: continuing ? previous.chainId : turn.attemptId,
35
+ status: "waiting",
36
+ input: originalInput,
37
+ nativeSessionId: binding.conversations.find(c => c.epoch === binding.currentConversationEpoch).conversationId,
38
+ authorityEpoch: binding.authority.epoch,
39
+ attempts: continuing ? previous.attempts - (input.noProviderWrite && previous.successorAutomatic !== false ? 1 : 0) : 0,
40
+ transportSequence: continuing ? previous.transportSequence : 0,
41
+ startedAt: continuing ? previous.startedAt : iso(input.at),
42
+ intentAt: continuing ? previous.intentAt : turn.submittedAt,
43
+ deadline: continuing ? previous.deadline : iso(input.at + PROVIDER_RETRY_BUDGET_MS),
44
+ nextEligibleAt: iso(input.at),
45
+ updatedAt: iso(input.at),
46
+ failedAttemptId: turn.attemptId,
47
+ failureAttemptId: turn.nativeTurnId !== undefined || !continuing ? turn.attemptId : previous.failureAttemptId,
48
+ ...((turn.nativeTurnId ?? (continuing ? previous.failedNativeTurnId : undefined)) === undefined ? {}
49
+ : { failedNativeTurnId: turn.nativeTurnId ?? previous.failedNativeTurnId }),
50
+ ...(turn.runId === undefined ? {} : { previousRunId: turn.runId }),
51
+ failureRef: input.failureRef,
52
+ error: input.error
53
+ };
54
+ const random = unitRandom(input.random ?? Math.random);
55
+ const window = Math.min(60_000, 5_000 * 2 ** current.attempts);
56
+ const backoff = Math.ceil(window * (0.5 + random / 2));
57
+ const serverWait = input.error.retryAfterMs;
58
+ const delay = serverWait === undefined ? backoff
59
+ : Math.max(backoff, serverWait + 1 + Math.ceil(random * 1_000));
60
+ const eligible = input.at + delay;
61
+ const exhausted = current.attempts >= PROVIDER_RETRY_LIMIT || eligible >= Date.parse(current.deadline);
62
+ return {
63
+ ...binding,
64
+ ...(!continuing && previous !== undefined
65
+ ? { retryHistory: [...(binding.retryHistory ?? []), previous] } : {}),
66
+ retry: {
67
+ // A terminal chain has no next submission. Keep its bounded deadline
68
+ // representable even for huge hints; the unshortened hint stays in error.
69
+ ...current, nextEligibleAt: iso(exhausted ? Math.min(eligible, Date.parse(current.deadline)) : eligible),
70
+ ...(turn.retrySupported !== true ? { status: "needs-attention", reason: "host-safe-retry-unsupported" }
71
+ : exhausted ? { status: "exhausted", reason: current.attempts >= PROVIDER_RETRY_LIMIT
72
+ ? "automatic-attempt-limit" : "retry-after-exceeds-deadline" } : {})
73
+ }
74
+ };
75
+ }
76
+ /** Atomically called at the shared native write admission, never by a timer alone. */
77
+ export function admitProviderRetry(binding, input) {
78
+ const retry = binding.retry;
79
+ if (retry?.status === "in-flight" && retry.currentAttemptId === input.attemptId)
80
+ return binding;
81
+ if (retry?.status !== "waiting"
82
+ || input.at < Date.parse(retry.nextEligibleAt)
83
+ || retry.successorAutomatic !== false && (binding.retryDisabled === true
84
+ || input.at >= Date.parse(retry.deadline) || retry.attempts >= PROVIDER_RETRY_LIMIT)
85
+ || binding.authority.owner !== "controller" || binding.authority.epoch !== retry.authorityEpoch
86
+ || binding.conversations.find(c => c.epoch === binding.currentConversationEpoch)?.conversationId !== retry.nativeSessionId
87
+ || binding.run?.attemptId !== retry.failedAttemptId
88
+ || !["failed", "rejected", "deferred"].includes(binding.run.status)) {
89
+ throw new Error("Provider retry is not eligible under the current input, Session, authority or budget.");
90
+ }
91
+ return { ...binding, retry: {
92
+ ...retry, status: "in-flight", attempts: retry.attempts + (retry.successorAutomatic === false ? 0 : 1),
93
+ transportSequence: retry.transportSequence + 1,
94
+ currentAttemptId: input.attemptId, updatedAt: iso(input.at)
95
+ } };
96
+ }
97
+ /** Busy/preflight refusal is not a model attempt. Keep its exact receipt visible. */
98
+ export function deferProviderRetry(binding, at, reason) {
99
+ const retry = binding.retry;
100
+ if (retry?.status !== "in-flight" || retry.currentAttemptId !== binding.run?.attemptId)
101
+ return binding;
102
+ const { currentAttemptId: _attempt, successorRunId, ...remaining } = retry;
103
+ return { ...binding, retry: {
104
+ ...remaining, attempts: retry.attempts - (retry.successorAutomatic === false ? 0 : 1),
105
+ failedAttemptId: binding.run.attemptId,
106
+ ...(reason === undefined && successorRunId !== undefined ? { successorRunId } : {}),
107
+ ...(reason !== undefined && binding.run?.runId !== undefined ? { previousRunId: binding.run.runId } : {}),
108
+ status: reason === undefined ? "waiting" : "needs-attention",
109
+ nextEligibleAt: iso(Math.min(at + 1_000, Date.parse(retry.deadline))),
110
+ updatedAt: iso(at), ...(reason === undefined ? {} : { reason })
111
+ } };
112
+ }
113
+ export function cancelProviderRetry(binding, reason, at) {
114
+ return !providerRetryPending(binding) ? binding : {
115
+ ...binding, retry: { ...binding.retry, status: "cancelled", reason, updatedAt: iso(at) }
116
+ };
117
+ }
118
+ export function completeProviderRetry(binding, turn) {
119
+ const retry = binding.retry;
120
+ return retry === undefined || !["in-flight", "cancelled", "needs-attention"].includes(retry.status)
121
+ || retry.currentAttemptId !== turn.attemptId
122
+ || turn.status !== "completed" ? binding : {
123
+ ...binding, retry: { ...retry, status: "recovered", updatedAt: turn.updatedAt }
124
+ };
125
+ }
126
+ export function providerRetryAttemptId(retry) {
127
+ return `provider-retry:${retry.chainId}/${retry.transportSequence + 1}`;
128
+ }
129
+ export function providerRetryPrompt(retry, original) {
130
+ if (retry.failedNativeTurnId === undefined)
131
+ return original;
132
+ return [
133
+ "Yui infrastructure recovery, not new user authorization.",
134
+ `Continue the existing input after failed attempt ${retry.failureAttemptId}`,
135
+ `(native Turn ${retry.failedNativeTurnId}; failure ${retry.failureRef}).`,
136
+ "Read current authorized context and the original input. Inspect existing work and receipts first;",
137
+ "continue only unfinished work. Preserve local changes and do not repeat confirmed side effects.",
138
+ original
139
+ ].join("\n");
140
+ }
141
+ /** The same presentation is consumed by CLI, Context and Web. */
142
+ export function providerRetryProjection(binding) {
143
+ const retry = binding?.retry;
144
+ if (retry === undefined)
145
+ return null;
146
+ return {
147
+ chainId: retry.chainId,
148
+ status: retry.status === "in-flight" && binding?.run?.status === "delivery-unknown"
149
+ ? "delivery-unknown" : retry.status,
150
+ attempts: retry.status === "recovered" ? 0 : retry.attempts,
151
+ limit: PROVIDER_RETRY_LIMIT, nextEligibleAt: retry.nextEligibleAt, deadline: retry.deadline,
152
+ category: retry.error.category, preservesWork: true, nativeSessionId: retry.nativeSessionId,
153
+ ...(retry.reason === undefined ? {} : { reason: retry.reason }),
154
+ failureRef: retry.failureRef,
155
+ ...(retry.previousRunId === undefined ? {} : { previousRunId: retry.previousRunId }),
156
+ ...(retry.successorRunId === undefined ? {} : { successorRunId: retry.successorRunId })
157
+ };
158
+ }
159
+ export function controlProviderRetry(binding, action, at) {
160
+ if (action === "enable")
161
+ return { ...binding, retryDisabled: false };
162
+ return { ...cancelProviderRetry(binding, `user-${action}`, at),
163
+ ...(action === "disable" ? { retryDisabled: true } : {}) };
164
+ }
165
+ export function renderProviderRetry(binding) {
166
+ const view = providerRetryProjection(binding);
167
+ return view === null ? `Provider retry: ${binding?.retryDisabled ? "disabled" : "none"}`
168
+ : `Provider retry: ${view.category}/${view.status}; ${view.attempts}/${view.limit}; next=${view.nextEligibleAt}; `
169
+ + `deadline=${view.deadline}; existing work preserved${view.reason === undefined ? "" : `; ${view.reason}`}`;
170
+ }
171
+ export function validateProviderRetry(retry) {
172
+ if (retry.policyVersion !== 1 || !["waiting", "in-flight", "recovered", "cancelled", "exhausted", "needs-attention"].includes(retry.status)
173
+ || !Number.isSafeInteger(retry.attempts) || retry.attempts < 0 || retry.attempts > PROVIDER_RETRY_LIMIT
174
+ || !Number.isSafeInteger(retry.transportSequence) || retry.transportSequence < retry.attempts
175
+ || !Number.isSafeInteger(retry.authorityEpoch) || retry.authorityEpoch < 1
176
+ || [retry.chainId, retry.nativeSessionId, retry.failedAttemptId, retry.failureRef].some(v => typeof v !== "string" || v.length === 0)
177
+ || [retry.startedAt, retry.intentAt, retry.deadline, retry.nextEligibleAt, retry.updatedAt].some(v => !Number.isFinite(Date.parse(v)))
178
+ || Date.parse(retry.deadline) - Date.parse(retry.startedAt) !== PROVIDER_RETRY_BUDGET_MS
179
+ || retry.status === "in-flight" && !retry.currentAttemptId)
180
+ throw new Error("Invalid Provider retry record.");
181
+ validateProviderInput(retry.input);
182
+ }
183
+ export function validateProviderInput(input) {
184
+ const fields = input.kind === "run" ? [input.taskId, input.runId]
185
+ : input.kind === "wake" ? [input.taskId, input.wakeId]
186
+ : input.kind === "message" ? [input.roleName, input.messageId]
187
+ : input.kind === "text" ? [input.text] : [];
188
+ if (fields.length === 0 || fields.some(v => typeof v !== "string" || !v.trim())) {
189
+ throw new Error("Invalid owned Provider input.");
190
+ }
191
+ }
192
+ function iso(at) { return new Date(at).toISOString(); }
193
+ function unitRandom(random) {
194
+ const value = random();
195
+ if (!Number.isFinite(value) || value < 0 || value > 1)
196
+ throw new Error("Retry RNG must return a unit fraction.");
197
+ return value;
198
+ }
@@ -1,3 +1,5 @@
1
+ import { cancelProviderRetry, completeProviderRetry, validateProviderInput, validateProviderRetry } from "./providerRetry.js";
2
+ import { isStandardAgentError } from "./agentError.js";
1
3
  export function createProviderRuntimeBinding(input) {
2
4
  const startedAt = timestamp(input.startedAt, "Provider Conversation startedAt");
3
5
  return validateProviderRuntimeBinding({
@@ -84,7 +86,7 @@ export function transferProviderAuthority(raw, input) {
84
86
  });
85
87
  }
86
88
  export function beginProviderTurn(raw, input) {
87
- const binding = validateProviderRuntimeBinding(raw);
89
+ let binding = validateProviderRuntimeBinding(raw);
88
90
  const attemptId = identity(input.attemptId, "Provider input attempt id");
89
91
  const runId = input.runId === undefined ? undefined : identity(input.runId, "AgentRun id");
90
92
  const currentRun = binding.run;
@@ -104,6 +106,9 @@ export function beginProviderTurn(raw, input) {
104
106
  throw new Error("Provider Conversation already has an unsettled AgentRun.");
105
107
  }
106
108
  const submittedAt = timestamp(input.submittedAt, "Provider Turn submittedAt");
109
+ if (binding.retry?.currentAttemptId !== attemptId) {
110
+ binding = cancelProviderRetry(binding, "superseded-by-explicit-input", Date.parse(submittedAt));
111
+ }
107
112
  return validateProviderRuntimeBinding({
108
113
  ...binding,
109
114
  run: {
@@ -111,6 +116,8 @@ export function beginProviderTurn(raw, input) {
111
116
  attemptId,
112
117
  authorityEpoch: input.authorityEpoch,
113
118
  status: "submitting",
119
+ ...(input.input === undefined ? {} : { input: input.input }),
120
+ ...(input.retrySupported === undefined ? {} : { retrySupported: input.retrySupported }),
114
121
  submittedAt,
115
122
  updatedAt: submittedAt
116
123
  }
@@ -221,7 +228,7 @@ export function settleProviderTurn(raw, input) {
221
228
  throw new Error("Provider Turn settlement does not match the current AgentRun.");
222
229
  }
223
230
  const settledAt = orderedRunTimestamp(run, input.settledAt, "Provider Turn settledAt");
224
- return validateProviderRuntimeBinding({
231
+ const settled = validateProviderRuntimeBinding({
225
232
  ...binding,
226
233
  run: {
227
234
  ...run,
@@ -233,6 +240,7 @@ export function settleProviderTurn(raw, input) {
233
240
  : { terminalReason: identity(input.reason, "Provider Turn terminal reason") })
234
241
  }
235
242
  });
243
+ return completeProviderRetry(settled, settled.run);
236
244
  }
237
245
  /** Explicitly abandon an engineering input only after its execution resources
238
246
  * are quiescent. This does not invent native acceptance, a Turn id or success. */
@@ -367,6 +375,24 @@ export function validateProviderRuntimeBinding(value) {
367
375
  throw new Error("Provider Runtime Binding requires Goal state.");
368
376
  if (value.goal !== null)
369
377
  validateProviderGoal(value.goal);
378
+ if (value.retry !== undefined)
379
+ validateProviderRetry(value.retry);
380
+ if (value.retryHistory !== undefined) {
381
+ if (!Array.isArray(value.retryHistory))
382
+ throw new Error("Provider retry audit must be an array.");
383
+ for (const retry of value.retryHistory) {
384
+ validateProviderRetry(retry);
385
+ if (retry.status === "waiting" || retry.status === "in-flight")
386
+ throw new Error("Provider retry audit cannot schedule work.");
387
+ }
388
+ }
389
+ if (value.retryDisabled !== undefined && typeof value.retryDisabled !== "boolean")
390
+ throw new Error("Provider retry switch must be boolean.");
391
+ if (value.run?.input !== undefined)
392
+ validateProviderInput(value.run.input);
393
+ if (value.run?.failure !== undefined
394
+ && (!isStandardAgentError(value.run.failure.error) || !value.run.failure.ref))
395
+ throw new Error("Invalid Provider input failure.");
370
396
  return value;
371
397
  }
372
398
  function validateProviderGoal(goal) {
@@ -8,8 +8,8 @@
8
8
  * live Session can always be re-attributed and reported.
9
9
  *
10
10
  * Pure: all I/O is injected. Unknown owners are reported, never cleaned.
11
- * An entry is archive-blocking when its durable state is terminal but the
12
- * physical Provider root is still live, and the Task is terminal/archived.
11
+ * An entry blocks ordinary archive / workspace cleanup when a terminal Task
12
+ * still owns a live or unverified physical root, regardless of Session status.
13
13
  */
14
14
  export function reconcileSessionOwners(input) {
15
15
  const entries = input.records.map((record) => reconcileOne(record, input));
@@ -54,8 +54,8 @@ function reconcileOne(record, input) {
54
54
  const terminalTask = taskStatus === "completed"
55
55
  || taskStatus === "cancelled"
56
56
  || taskStatus === "archived";
57
- const archiveBlocked = mismatch === "durable-terminal-physical-live"
58
- && terminalTask;
57
+ const archiveBlocked = terminalTask && (physical === undefined
58
+ || physical.alive || physical.identityConflict);
59
59
  return {
60
60
  owner: {
61
61
  scope: owner.scope,
@@ -26,11 +26,21 @@ export function resolveSessionTokenIdentity(input) {
26
26
  export function projectSessionTokenMetrics(events, identity) {
27
27
  if (identity === null)
28
28
  return unobservedSessionTokenMetrics();
29
- const observations = events
30
- .map(runtimeObservationFromTaskEvent)
31
- .filter((observation) => (observation !== null
32
- && isRuntimeTokenEvidence(observation)
33
- && matchesSessionIdentity(observation, identity)))
29
+ return projectSessionTokenObservations(events.flatMap((event) => {
30
+ const observation = runtimeObservationFromTaskEvent(event);
31
+ return observation === null ? [] : [observation];
32
+ }), identity);
33
+ }
34
+ /** Shared reducer for authorized, already decoded canonical observations. */
35
+ export function projectSessionTokenObservations(input, identity) {
36
+ const observations = input
37
+ .filter((observation) => (isRuntimeTokenEvidence(observation)
38
+ && matchesSessionIdentity(observation, identity)
39
+ && (observation.authority === "provider-structured" || observation.authority === "driver-inferred")
40
+ // Child counters have no declared inclusion relationship with the
41
+ // Session's parent counter. They must never be added as extra requests.
42
+ && observation.fence.continuationId === undefined
43
+ && observation.fence.parentContinuationId === undefined))
34
44
  .sort(compareObservations);
35
45
  const incompleteBoundaries = observations.filter(({ payload }) => (payload.usage === undefined));
36
46
  const request = observations.filter(({ payload }) => (payload.usage?.semantics === "request-context"));
@@ -462,7 +462,7 @@ class CodexStructuredProviderSession {
462
462
  ? {}
463
463
  : {
464
464
  error: recovered.error ?? "Provider Turn failed.",
465
- rawError: serializeAgentErrorRaw(recovered)
465
+ ...(recovered.rawError === undefined ? {} : { rawError: recovered.rawError })
466
466
  })
467
467
  };
468
468
  session.#clientOwnedTurnId = undefined;
@@ -594,7 +594,11 @@ class CodexStructuredProviderSession {
594
594
  conversationId: this.conversationId,
595
595
  attemptId: turn.attemptId,
596
596
  text: turn.boundedText,
597
- expectedNoActiveTurn: true
597
+ expectedNoActiveTurn: true,
598
+ ...(turn.requireQuiescent ? { requireQuiescent: true } : {}),
599
+ ...(turn.expectedFailedNativeTurnId === undefined ? {} : {
600
+ expectedFailedNativeTurnId: turn.expectedFailedNativeTurnId
601
+ })
598
602
  });
599
603
  if (acceptance.status === "unknown") {
600
604
  throw new ProviderDeliveryUnknownError(acceptance.reason, turn.attemptId);