@zq-silk/yui 0.16.1 → 1.0.0-alpha

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 (193) hide show
  1. package/ARCHITECTURE.md +12 -0
  2. package/ARCHITECTURE.zh-CN.md +8 -0
  3. package/README.md +9 -4
  4. package/dist/agent/agent.js +4 -9
  5. package/dist/agent/executionComponents.js +4 -4
  6. package/dist/agent/managedRuntimeEnvironment.js +0 -4
  7. package/dist/agentRun/agentRun.js +13 -23
  8. package/dist/artifacts/managedGit.js +3 -56
  9. package/dist/brief/taskBrief.js +3 -3
  10. package/dist/cli/agentConfigurationPicker.js +24 -21
  11. package/dist/cli/commandCatalog.js +18 -10
  12. package/dist/cli/roleWizard.js +41 -30
  13. package/dist/cli/updateCommand.js +35 -13
  14. package/dist/cli/updateOrchestrator.js +73 -44
  15. package/dist/cli/updatePorts.js +76 -86
  16. package/dist/cli/upgradeCommand.js +4 -8
  17. package/dist/cli.js +39 -33
  18. package/dist/commands/controllerCommands.js +2 -2
  19. package/dist/commands/executionAuditCommands.js +2 -2
  20. package/dist/commands/projectCommands.js +1 -1
  21. package/dist/commands/releaseCommands.js +9 -38
  22. package/dist/commands/taskCommands.js +91 -81
  23. package/dist/commands/taskCompletionGate.js +0 -117
  24. package/dist/commands/taskRoleRuntimeStatus.js +2 -2
  25. package/dist/commands/taskUpstreamCommands.js +93 -54
  26. package/dist/context/runContextPack.js +30 -54
  27. package/dist/context/runInputContract.js +9 -0
  28. package/dist/context/sessionBootstrapManifest.js +11 -19
  29. package/dist/controller/agentHostObservation.js +4 -2
  30. package/dist/controller/agentRuntimeObserver.js +4 -4
  31. package/dist/controller/clientRuntime.js +19 -44
  32. package/dist/controller/controller.js +24 -10
  33. package/dist/controller/fileSchedulerStoreAdapter.js +91 -24
  34. package/dist/controller/globalInputDelivery.js +31 -8
  35. package/dist/controller/globalRuntimeAttention.js +34 -0
  36. package/dist/controller/jobSupervisor.js +3 -3
  37. package/dist/controller/operatorNotification.js +31 -0
  38. package/dist/controller/providerRetryAdmission.js +3 -1
  39. package/dist/controller/providerRetryDelivery.js +136 -119
  40. package/dist/controller/runtime.js +2 -28
  41. package/dist/controller/sessionOwnerReconciliation.js +42 -24
  42. package/dist/controller/structuredProviderObservation.js +28 -1
  43. package/dist/controller/updateReconciliation.js +72 -20
  44. package/dist/coordination/workMailbox.js +4 -4
  45. package/dist/core/controllerIdentity.js +25 -0
  46. package/dist/core/controllerProcessIdentity.js +2 -2
  47. package/dist/core/controllerServer.js +4 -2
  48. package/dist/core/protocol.js +1 -1
  49. package/dist/doctor/doctor.js +2 -1
  50. package/dist/domain/validation.js +6 -0
  51. package/dist/event/taskEvent.js +3 -3
  52. package/dist/execution/workItemExecution.js +2 -2
  53. package/dist/executor/agentAdapter.js +28 -96
  54. package/dist/executor/agentConfigurationCatalog.js +13 -68
  55. package/dist/executor/agentConfigurationFields.js +120 -0
  56. package/dist/executor/agentConfigurationProbe.js +55 -80
  57. package/dist/executor/agentExecutor.js +11 -8
  58. package/dist/executor/effectiveLaunch.js +3 -3
  59. package/dist/executor/executorRegistry.js +0 -9
  60. package/dist/executor/fileRoleLaunchPlanner.js +12 -11
  61. package/dist/grant/capabilityGrant.js +2 -2
  62. package/dist/input/inputRequest.js +4 -4
  63. package/dist/integration/changeSet.js +3 -3
  64. package/dist/integration/integrationAttempt.js +3 -3
  65. package/dist/integration/integrationSourceApplication.js +1 -1
  66. package/dist/interaction/operatorPresentation.js +2 -1
  67. package/dist/job/durableJob.js +1 -1
  68. package/dist/job/jobRunner.js +2 -2
  69. package/dist/message/message.js +8 -6
  70. package/dist/message/messageContinuation.js +8 -9
  71. package/dist/milestone/milestone.js +3 -3
  72. package/dist/observability/executionAudit.js +1 -10
  73. package/dist/observability/runtimeIdentity.js +0 -23
  74. package/dist/output/agentConfigurationPresentation.js +4 -2
  75. package/dist/profile/agentProfile.js +3 -3
  76. package/dist/release/releaseHandover.js +2 -2
  77. package/dist/release/releaseIdempotencyStore.js +0 -23
  78. package/dist/release/releaseWorkflowPorts.js +8 -4
  79. package/dist/release/runtimeRelease.js +9 -7
  80. package/dist/repository/gitWorkspace.js +66 -19
  81. package/dist/repository/project.js +3 -3
  82. package/dist/repository/taskWorkspacePreparer.js +75 -68
  83. package/dist/resources/liveReferences.js +1 -1
  84. package/dist/resources/resourceRegistry.js +90 -44
  85. package/dist/resources/sqliteResourceRegistry.js +5 -7
  86. package/dist/review/reviewRound.js +5 -5
  87. package/dist/role/role.js +7 -8
  88. package/dist/runtime/acpProtocol.js +2 -3
  89. package/dist/runtime/acpSession.js +21 -5
  90. package/dist/runtime/agentDriverObservation.js +1 -1
  91. package/dist/runtime/agentEndpoint.js +4 -1
  92. package/dist/runtime/agentHost.js +25 -65
  93. package/dist/runtime/agentHostCleanup.js +85 -0
  94. package/dist/runtime/agentHostProtocol.js +3 -3
  95. package/dist/runtime/builtinAgentDrivers.js +46 -119
  96. package/dist/runtime/builtinTranscriptObserver.js +10 -6
  97. package/dist/runtime/codexAppServerRuntime.js +55 -89
  98. package/dist/runtime/codexInteractiveHost.js +1 -1
  99. package/dist/runtime/jsonLineChannel.js +35 -7
  100. package/dist/runtime/launchBroker.js +1 -1
  101. package/dist/runtime/processExitObservation.js +1 -1
  102. package/dist/runtime/providerContinuationReconciliationService.js +86 -38
  103. package/dist/runtime/providerRetry.js +15 -0
  104. package/dist/runtime/providerRuntimeIdentity.js +3 -3
  105. package/dist/runtime/providerRuntimeReconciler.js +12 -2
  106. package/dist/runtime/runtimeCoherence.js +7 -3
  107. package/dist/runtime/runtimeObservation.js +3 -3
  108. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  109. package/dist/runtime/structuredProviderHost.js +106 -81
  110. package/dist/runtime/taskRuntimeIsolation.js +3 -3
  111. package/dist/runtime/tmuxAdapters.js +3 -3
  112. package/dist/scheduler/activeRoleRunDelivery.js +5 -1
  113. package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
  114. package/dist/scheduler/taskWake.js +1 -1
  115. package/dist/storage/baselineSchema.js +606 -0
  116. package/dist/storage/homeLayout.js +5 -16
  117. package/dist/storage/recordValidation.js +16 -4
  118. package/dist/storage/sqliteSchema.js +106 -1741
  119. package/dist/storage/sqliteStore.js +16 -14
  120. package/dist/storage/storageSchema.js +8 -7
  121. package/dist/storage/storageVersions.js +23 -16
  122. package/dist/storage/taskStore.js +3 -29
  123. package/dist/storage/upgrade/upgradeOrchestrator.js +14 -112
  124. package/dist/task/nextAction.js +33 -23
  125. package/dist/task/task.js +17 -8
  126. package/dist/task/taskActivation.js +5 -4
  127. package/dist/telemetry/sqliteTelemetryStore.js +2 -2
  128. package/dist/verification/gateArtifact.js +5 -3
  129. package/dist/verification/verificationPlan.js +6 -7
  130. package/dist/web/assets/assetManifest.js +2 -0
  131. package/dist/web/assets/client/app.js +72 -3
  132. package/dist/web/assets/client/components.js +2 -1
  133. package/dist/web/assets/client/i18n.js +2 -0
  134. package/dist/web/assets/client/taskSummary.js +345 -0
  135. package/dist/web/assets/client/taskSurface.js +44 -26
  136. package/dist/web/assets/client/view.js +33 -0
  137. package/dist/web/assets/shell.js +1 -0
  138. package/dist/web/assets/styles/cards.js +21 -0
  139. package/dist/web/webServer.js +39 -5
  140. package/dist/web/webSessions.js +165 -0
  141. package/dist/web/webSnapshot.js +23 -0
  142. package/dist/web/webTaskSurface.js +23 -0
  143. package/dist/workItem/workItem.js +6 -6
  144. package/dist/workspace/cleanupInspection.js +1 -9
  145. package/dist/worktree/managedWorkspace.js +3 -3
  146. package/docs/managed-turn-and-session-runtime.md +50 -7
  147. package/docs/managed-turn-and-session-runtime.zh-CN.md +36 -5
  148. package/docs/observability/README.md +47 -0
  149. package/docs/observability/README.zh-CN.md +37 -0
  150. package/docs/project-refresh.md +9 -0
  151. package/docs/project-refresh.zh-CN.md +8 -0
  152. package/docs/provider-retry.md +35 -0
  153. package/docs/release-workflow.md +70 -214
  154. package/docs/release-workflow.zh-CN.md +53 -157
  155. package/docs/roles-and-configuration.md +30 -0
  156. package/docs/roles-and-configuration.zh-CN.md +20 -0
  157. package/docs/sqlite-control-plane-design.md +48 -289
  158. package/docs/sqlite-control-plane-design.zh-CN.md +37 -53
  159. package/docs/storage-baseline.md +132 -0
  160. package/docs/storage-baseline.zh-CN.md +106 -0
  161. package/docs/task-delivery.md +110 -2
  162. package/docs/task-delivery.zh-CN.md +82 -2
  163. package/docs/task-discovery.md +9 -0
  164. package/docs/task-discovery.zh-CN.md +6 -0
  165. package/docs/testing/verification-levels.md +42 -164
  166. package/docs/testing/verification-levels.zh-CN.md +29 -115
  167. package/i18n/README.zh-CN.md +7 -4
  168. package/package.json +1 -1
  169. package/skills/yui-leader/SKILL.md +11 -0
  170. package/skills/yui-leader/references/execution.md +22 -0
  171. package/skills/yui-leader/references/planning.md +6 -2
  172. package/skills/yui-operator/SKILL.md +45 -13
  173. package/skills/yui-operator/references/task-delivery.md +99 -0
  174. package/skills/yui-runtime/SKILL.md +5 -3
  175. package/skills/yui-runtime/references/publication.md +56 -5
  176. package/skills/yui-runtime/references/recovery.md +12 -2
  177. package/dist/runtime/agentHostCompatibility.js +0 -127
  178. package/dist/storage/migrations/agentFailureContext.js +0 -22
  179. package/dist/storage/migrations/agentRunContract.js +0 -159
  180. package/dist/storage/migrations/artifactsToGit.js +0 -338
  181. package/dist/storage/migrations/collapseWorktreeLayout.js +0 -963
  182. package/dist/storage/migrations/currentInputContract.js +0 -86
  183. package/dist/storage/migrations/currentRuntimeContract.js +0 -228
  184. package/dist/storage/migrations/historicalVerificationPlan.js +0 -35
  185. package/dist/storage/migrations/integrationContinuation.js +0 -105
  186. package/dist/storage/migrations/narrowAgentFailureContext.js +0 -65
  187. package/dist/storage/migrations/notificationOnlyWakes.js +0 -74
  188. package/dist/storage/migrations/removeRuntimeGeneration.js +0 -207
  189. package/dist/storage/migrations/submitIntent.js +0 -126
  190. package/dist/storage/migrations/unifyHomeLayout.js +0 -925
  191. package/dist/storage/migrations/verificationPlanV1.js +0 -162
  192. package/dist/storage/migrations/verificationPolicy.js +0 -74
  193. package/dist/storage/migrations/workItemHistory.js +0 -46
@@ -70,8 +70,12 @@ export class CodexAppServerRuntime {
70
70
  async readGoal(conversationId) {
71
71
  const id = text(conversationId, "Codex thread id");
72
72
  const result = await this.transport.request("thread/goal/get", { threadId: id });
73
+ if (result.goal === null)
74
+ return null;
73
75
  const goal = objectMember(result, "goal");
74
- return goal === null ? null : codexThreadGoal(goal, id);
76
+ if (goal === null)
77
+ throw new Error("Codex thread/goal/get returned no Goal state.");
78
+ return codexThreadGoal(goal, id);
75
79
  }
76
80
  async inspectConversation(conversationId) {
77
81
  const id = text(conversationId, "Codex thread id");
@@ -162,8 +166,7 @@ export class CodexAppServerRuntime {
162
166
  text_elements: []
163
167
  }]
164
168
  });
165
- const turnId = optionalId(result.turnId)
166
- ?? optionalId(objectMember(result, "turn")?.id);
169
+ const turnId = optionalId(objectMember(result, "turn")?.id);
167
170
  return turnId === undefined
168
171
  ? { status: "unknown", reason: "turn/start returned no durable turn id" }
169
172
  : { status: "accepted", turnId };
@@ -244,7 +247,7 @@ export class CodexAppServerRuntime {
244
247
  const result = await this.transport.request("thread/list", {
245
248
  ancestorThreadId: threadId
246
249
  });
247
- const candidates = arrayMember(result, "threads")
250
+ const candidates = arrayMember(result, "data")
248
251
  .flatMap((entry) => optionalId(object(entry)?.id) === undefined
249
252
  ? []
250
253
  : [optionalId(object(entry)?.id)]);
@@ -276,7 +279,7 @@ export class CodexAppServerRuntime {
276
279
  return {
277
280
  quality: "unavailable",
278
281
  continuations: [],
279
- detail: error instanceof Error ? error.message : String(error)
282
+ detail: serializeAgentErrorRaw(error)
280
283
  };
281
284
  }
282
285
  return { quality: "exact", continuations: Object.freeze(observed) };
@@ -284,10 +287,12 @@ export class CodexAppServerRuntime {
284
287
  }
285
288
  /** thread/closed unloads the client attachment; the durable thread remains resumable. */
286
289
  export function codexNotificationBoundary(input) {
287
- const conversationId = optionalId(input.params.threadId)
288
- ?? optionalId(objectMember(input.params, "thread")?.id);
289
- const turnId = optionalId(input.params.turnId)
290
- ?? optionalId(objectMember(input.params, "turn")?.id);
290
+ const conversationId = input.method === "thread/started"
291
+ ? optionalId(objectMember(input.params, "thread")?.id)
292
+ : optionalId(input.params.threadId);
293
+ const turnId = input.method === "turn/started" || input.method === "turn/completed"
294
+ ? optionalId(objectMember(input.params, "turn")?.id)
295
+ : optionalId(input.params.turnId);
291
296
  if (input.method === "thread/closed")
292
297
  return { kind: "attachment-closed", conversationId };
293
298
  if (input.method === "thread/goal/updated")
@@ -310,45 +315,53 @@ export function codexGoalNotification(input) {
310
315
  return undefined;
311
316
  const goal = objectMember(input.params, "goal");
312
317
  if (goal === null)
313
- return undefined;
318
+ throw new Error("Codex Goal notification has no Goal state.");
314
319
  return codexThreadGoal(goal, conversationId);
315
320
  }
316
321
  function parseThreadSnapshot(result, expectedThreadId, loaded) {
317
- const thread = objectMember(result, "thread") ?? result;
318
- const id = optionalId(thread.id) ?? expectedThreadId;
322
+ const thread = objectMember(result, "thread");
323
+ if (thread === null)
324
+ throw new Error("Codex App Server returned no thread.");
325
+ const id = text(optionalId(thread.id), "Codex response thread id");
319
326
  if (id !== expectedThreadId)
320
327
  throw new Error("Codex App Server returned a different thread.");
321
- const turns = arrayMember(thread, "turns").map(object).filter((entry) => (entry !== null));
322
- const active = [...turns].reverse().find((turn) => (["inProgress", "in_progress", "running", "active"].includes(String(turn.status))));
323
- const latestStatus = optionalTurnStatus(turns.at(-1)?.status);
324
- const turnSnapshots = turns.flatMap((turn) => {
325
- const turnId = optionalId(turn.id);
326
- if (turnId === undefined)
327
- return [];
328
+ const status = threadStatus(thread.status);
329
+ if (status === "unknown")
330
+ throw new Error("Codex thread status is missing or unsupported.");
331
+ if (!Array.isArray(thread.turns))
332
+ throw new Error("Codex thread turns are missing or invalid.");
333
+ const turnSnapshots = thread.turns.map((value) => {
334
+ const turn = object(value);
335
+ if (turn === null)
336
+ throw new Error("Codex thread contains an invalid Turn.");
337
+ const turnId = text(optionalId(turn.id), "Codex response Turn id");
328
338
  const status = optionalTurnStatus(turn.status);
339
+ if (status === undefined)
340
+ throw new Error("Codex Turn status is missing or unsupported.");
329
341
  const output = codexTurnOutput(turn);
330
342
  const error = providerError(turn.error);
331
- return [{
332
- turnId,
333
- ...(status === undefined ? {} : { status }),
334
- ...(output === undefined ? {} : { output }),
335
- ...(error === undefined ? {} : { error }),
336
- ...(turn.error === undefined || turn.error === null ? {} : {
337
- rawError: serializeAgentErrorRaw(turn.error)
338
- })
339
- }];
343
+ return {
344
+ turnId,
345
+ status,
346
+ ...(output === undefined ? {} : { output }),
347
+ ...(error === undefined ? {} : { error }),
348
+ ...(turn.error === undefined || turn.error === null ? {} : {
349
+ rawError: serializeAgentErrorRaw(turn.error)
350
+ })
351
+ };
340
352
  });
353
+ const active = [...turnSnapshots].reverse().find((turn) => turn.status === "inProgress");
354
+ const latestStatus = turnSnapshots.at(-1)?.status;
341
355
  return {
342
356
  threadId: id,
343
357
  loaded,
344
- status: threadStatus(thread.status),
345
- ...(optionalId(active?.id) === undefined ? {} : { activeTurnId: optionalId(active?.id) }),
358
+ status,
359
+ ...(active === undefined ? {} : { activeTurnId: active.turnId }),
346
360
  ...(latestStatus === undefined ? {} : { latestTurnStatus: latestStatus }),
347
361
  turns: Object.freeze(turnSnapshots),
348
362
  ...(optionalId(thread.parentThreadId) === undefined
349
363
  ? {}
350
364
  : { parentThreadId: optionalId(thread.parentThreadId) }),
351
- ancestorThreadIds: Object.freeze(arrayMember(thread, "ancestorThreadIds").flatMap((entry) => (optionalId(entry) === undefined ? [] : [optionalId(entry)]))),
352
365
  raw: result
353
366
  };
354
367
  }
@@ -356,36 +369,9 @@ function parseThreadSnapshot(result, expectedThreadId, loaded) {
356
369
  export function codexTurnOutput(turn) {
357
370
  for (const value of [...arrayMember(turn, "items")].reverse()) {
358
371
  const item = object(value);
359
- if (item === null)
360
- continue;
361
- const type = item.type;
362
- const message = object(item.message);
363
- const agentMessage = type === "agentMessage"
364
- || type === "agent_message"
365
- || (type === "message" && (item.role === "assistant" || message?.role === "assistant"));
366
- if (!agentMessage)
367
- continue;
368
- const text = messageOutput(item) ?? (message === null ? undefined : messageOutput(message));
369
- if (text !== undefined)
370
- return text;
371
- }
372
- return undefined;
373
- }
374
- function messageOutput(message) {
375
- for (const value of [message.text, message.content]) {
376
- if (typeof value === "string" && value.trim().length > 0)
377
- return value;
378
- if (!Array.isArray(value))
379
- continue;
380
- const parts = value.flatMap((part) => {
381
- if (typeof part === "string" && part.trim().length > 0)
382
- return [part];
383
- const record = object(part);
384
- const text = record?.text ?? record?.content;
385
- return typeof text === "string" && text.trim().length > 0 ? [text] : [];
386
- });
387
- if (parts.length > 0)
388
- return parts.join("\n");
372
+ if (item?.type === "agentMessage"
373
+ && typeof item.text === "string" && item.text.trim().length > 0)
374
+ return item.text;
389
375
  }
390
376
  return undefined;
391
377
  }
@@ -393,33 +379,12 @@ function messageOutput(message) {
393
379
  export function codexTurnInput(turn) {
394
380
  for (const value of arrayMember(turn, "items")) {
395
381
  const item = object(value);
396
- if (item === null)
397
- continue;
398
- const type = item.type;
399
- const message = object(item.message);
400
- const userMessage = type === "userMessage"
401
- || type === "user_message"
402
- || (type === "message" && (item.role === "user" || message?.role === "user"));
403
- if (!userMessage)
404
- continue;
405
- const text = messageText(item) ?? (message === null ? undefined : messageText(message));
406
- if (text !== undefined)
407
- return text;
408
- }
409
- return undefined;
410
- }
411
- function messageText(message) {
412
- for (const value of [message.text, message.content]) {
413
- if (typeof value === "string" && value.trim().length > 0)
414
- return value.trim();
415
- if (!Array.isArray(value))
382
+ if (item?.type !== "userMessage")
416
383
  continue;
417
- const parts = value.flatMap((part) => {
418
- if (typeof part === "string" && part.trim().length > 0)
419
- return [part];
384
+ const parts = arrayMember(item, "content").flatMap((part) => {
420
385
  const record = object(part);
421
- const text = record?.text ?? record?.content;
422
- return typeof text === "string" && text.trim().length > 0 ? [text] : [];
386
+ return record?.type === "text" && typeof record.text === "string"
387
+ && record.text.trim().length > 0 ? [record.text] : [];
423
388
  });
424
389
  if (parts.length > 0)
425
390
  return parts.join("\n").trim();
@@ -438,7 +403,7 @@ function providerError(value) {
438
403
  return JSON.stringify(record);
439
404
  }
440
405
  function codexThreadGoal(goal, expectedThreadId) {
441
- const conversationId = text(goal.threadId, "Codex Goal thread id");
406
+ const conversationId = text(optionalId(goal.threadId), "Codex Goal thread id");
442
407
  if (conversationId !== expectedThreadId) {
443
408
  throw new Error("Codex Goal belongs to a different thread.");
444
409
  }
@@ -554,7 +519,7 @@ export function codexAppServerErrorIsMissing(error) {
554
519
  || /\b(not found|missing|does not exist)\b.*\b(thread|conversation)\b/u.test(message);
555
520
  }
556
521
  function threadId(result) {
557
- return text(optionalId(result.threadId) ?? optionalId(objectMember(result, "thread")?.id), "Codex thread id");
522
+ return text(optionalId(objectMember(result, "thread")?.id), "Codex response thread id");
558
523
  }
559
524
  function object(value) {
560
525
  return value !== null && typeof value === "object" && !Array.isArray(value)
@@ -568,7 +533,8 @@ function arrayMember(value, key) {
568
533
  return Array.isArray(value[key]) ? value[key] : [];
569
534
  }
570
535
  function optionalId(value) {
571
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
536
+ return typeof value === "string" && !value.includes("\0") && value.length > 0
537
+ && value.trim() === value ? value : undefined;
572
538
  }
573
539
  function text(value, label) {
574
540
  if (typeof value !== "string" || value.includes("\0") || value.trim().length === 0) {
@@ -29,7 +29,7 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
29
29
  }
30
30
  const expectedId = environment.YUI_NATIVE_SESSION_ID;
31
31
  let snapshot = {
32
- schemaVersion: 2, state: "starting", adapterId: "codex", updatedAt: new Date().toISOString()
32
+ schemaVersion: 1, state: "starting", adapterId: "codex", updatedAt: new Date().toISOString()
33
33
  };
34
34
  const control = await openAgentHostControl(home, payload, () => snapshot, async (request) => {
35
35
  if (request.type !== "status") {
@@ -1,3 +1,4 @@
1
+ import { serializeAgentErrorRaw } from "./agentError.js";
1
2
  export const PROVIDER_MESSAGE_MAX_BYTES = 16 * 1024 * 1024;
2
3
  /**
3
4
  * Newline-delimited JSON over a child process's stdio. This is a plain shared
@@ -7,13 +8,15 @@ export const PROVIDER_MESSAGE_MAX_BYTES = 16 * 1024 * 1024;
7
8
  export class JsonLineChannel {
8
9
  child;
9
10
  mirror;
11
+ onFailure;
10
12
  #listeners = new Set();
11
13
  #closeListeners = new Set();
12
14
  #buffer = "";
13
15
  #closedError;
14
- constructor(child, mirror) {
16
+ constructor(child, mirror, onFailure) {
15
17
  this.child = child;
16
18
  this.mirror = mirror;
19
+ this.onFailure = onFailure;
17
20
  child.stdout.setEncoding("utf8");
18
21
  child.stdout.on("data", (chunk) => this.#receive(chunk));
19
22
  child.once("error", (error) => this.#close(error));
@@ -57,10 +60,12 @@ export class JsonLineChannel {
57
60
  });
58
61
  }
59
62
  #receive(chunk) {
63
+ if (this.#closedError !== undefined)
64
+ return;
60
65
  this.mirror("stdout", chunk);
61
66
  this.#buffer += chunk;
62
67
  if (Buffer.byteLength(this.#buffer, "utf8") > PROVIDER_MESSAGE_MAX_BYTES) {
63
- this.#close(new Error("Provider response line exceeds its message bound."));
68
+ this.#fail(new Error("Provider response line exceeds its message bound."));
64
69
  terminateProcessGroup(this.child, "SIGTERM");
65
70
  return;
66
71
  }
@@ -72,18 +77,41 @@ export class JsonLineChannel {
72
77
  this.#buffer = this.#buffer.slice(newline + 1);
73
78
  if (line.length === 0)
74
79
  continue;
80
+ let parsed;
81
+ try {
82
+ parsed = JSON.parse(line);
83
+ }
84
+ catch (cause) {
85
+ this.#fail(new Error("Provider response is not valid JSON.", { cause }));
86
+ return;
87
+ }
88
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
89
+ this.#fail(new Error("Provider response must be a JSON object."));
90
+ return;
91
+ }
92
+ // Unknown protocol objects belong to the protocol listener. A listener
93
+ // throwing is not an unrelated notification or a parse error.
75
94
  try {
76
- const parsed = JSON.parse(line);
77
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
78
- continue;
79
95
  for (const listener of this.#listeners)
80
96
  listener(parsed);
81
97
  }
82
- catch {
83
- continue;
98
+ catch (cause) {
99
+ this.#fail(new Error("Provider message listener failed.", { cause }));
100
+ return;
84
101
  }
85
102
  }
86
103
  }
104
+ #fail(error) {
105
+ // Failing this pipe neither proves a native terminal nor permits replay.
106
+ // Keep the original cause for pending callers and the Host's durable sink.
107
+ try {
108
+ this.mirror("stderr", `${serializeAgentErrorRaw(error)}\n`);
109
+ this.onFailure?.(error);
110
+ }
111
+ finally {
112
+ this.#close(error);
113
+ }
114
+ }
87
115
  #close(error) {
88
116
  if (this.#closedError !== undefined)
89
117
  return;
@@ -56,7 +56,7 @@ export function validateAgentHostLaunchPayload(value) {
56
56
  return validatePayload(value);
57
57
  }
58
58
  function validatePayload(payload) {
59
- if (payload.schemaVersion !== 2)
59
+ if (payload.schemaVersion !== 1)
60
60
  throw new Error("Agent Host launch payload version is invalid.");
61
61
  text(payload.command, "command");
62
62
  text(payload.cwd, "cwd");
@@ -1,4 +1,4 @@
1
- export const RUNTIME_PROCESS_EXIT_SCHEMA_VERSION = 2;
1
+ export const RUNTIME_PROCESS_EXIT_SCHEMA_VERSION = 1;
2
2
  export const RUNTIME_PROCESS_EXIT_TASK_EVENT = "runtime.process-exit-observed";
3
3
  export function validateRuntimeProcessExitObservation(observation) {
4
4
  if (observation.schemaVersion !== RUNTIME_PROCESS_EXIT_SCHEMA_VERSION) {
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { serializeAgentErrorRaw, standardAgentError } from "./agentError.js";
2
3
  import { providerContinuationKey } from "./providerContinuation.js";
3
4
  import { projectProviderContinuations } from "./runtimeContinuationProjection.js";
4
5
  import { reconcileKnownDetachedContinuations } from "./providerRuntimeReconciler.js";
@@ -12,54 +13,101 @@ export class ProviderContinuationReconciliationService {
12
13
  store;
13
14
  sink;
14
15
  metadata;
16
+ onError;
15
17
  #schedules = new Map();
16
- constructor(store, sink, metadata) {
18
+ constructor(store, sink, metadata, onError = error => { throw error; }) {
17
19
  this.store = store;
18
20
  this.sink = sink;
19
21
  this.metadata = metadata;
22
+ this.onError = onError;
20
23
  }
21
24
  async reconcile(now) {
22
25
  const changedTaskIds = new Set();
23
26
  for (const task of this.store.listTasks().filter((entry) => (entry.status === "active" && entry.executionGate.state === "enabled"))) {
24
- const events = this.store.listEvents(task.id);
25
- const groups = groupDetachedContinuations(projectProviderContinuations(events), events.map(runtimeObservationFromTaskEvent)
26
- .filter((entry) => entry !== null));
27
- for (const [groupKey, candidates] of groups) {
28
- const group = candidates.map(({ continuation }) => continuation);
29
- const previous = this.#schedules.get(groupKey);
30
- if (previous !== undefined && Date.parse(previous.nextReconcileAt) > now.getTime()) {
31
- continue;
32
- }
33
- let result;
34
- try {
35
- result = await reconcileKnownDetachedContinuations({
36
- port: this.metadata,
37
- continuations: group,
38
- ...(previous === undefined ? {} : { previous }),
39
- now
40
- });
41
- }
42
- catch {
43
- // One malformed identity group must not restart or block the
44
- // Controller. Keep it writer-owned and retry through the same
45
- // bounded, metadata-only schedule.
46
- this.#schedules.set(groupKey, failureSchedule(groupKey, previous, now));
47
- continue;
48
- }
49
- if (result.schedule === null)
50
- this.#schedules.delete(groupKey);
51
- else
52
- this.#schedules.set(groupKey, result.schedule);
53
- for (let index = 0; index < group.length; index += 1) {
54
- const before = group[index];
55
- const after = result.continuations[index];
56
- if (sameContinuationState(before, after))
27
+ try {
28
+ const events = this.store.listEvents(task.id);
29
+ const groups = groupDetachedContinuations(projectProviderContinuations(events), events.map(runtimeObservationFromTaskEvent)
30
+ .filter((entry) => entry !== null));
31
+ for (const [groupKey, candidates] of groups) {
32
+ const group = candidates.map(({ continuation }) => continuation);
33
+ const previous = this.#schedules.get(groupKey);
34
+ if (previous !== undefined && Date.parse(previous.nextReconcileAt) > now.getTime()) {
57
35
  continue;
58
- const disposition = this.sink.observeRuntimeObservation(reconciliationObservation(after, candidates[index].fence, now), now);
59
- if (disposition === "applied")
60
- changedTaskIds.add(after.taskId);
36
+ }
37
+ let result;
38
+ try {
39
+ result = await reconcileKnownDetachedContinuations({
40
+ port: this.metadata,
41
+ continuations: group,
42
+ ...(previous === undefined ? {} : { previous }),
43
+ now
44
+ });
45
+ }
46
+ catch (error) {
47
+ // One malformed identity group must not restart or block the
48
+ // Controller. Keep it writer-owned and retry through the same
49
+ // bounded, metadata-only schedule.
50
+ result = {
51
+ continuations: group, quality: "unavailable", changed: false,
52
+ schedule: failureSchedule(groupKey, previous, now),
53
+ failure: standardAgentError({
54
+ source: "yui", phase: "turn-reconcile",
55
+ message: error instanceof Error ? error.message : String(error),
56
+ raw: serializeAgentErrorRaw(error)
57
+ })
58
+ };
59
+ }
60
+ if (result.schedule === null)
61
+ this.#schedules.delete(groupKey);
62
+ else
63
+ this.#schedules.set(groupKey, result.schedule);
64
+ if (result.failure !== undefined) {
65
+ const failure = result.failure;
66
+ // One original failure and one circuit-limit notice, not one wake per
67
+ // poll. Existing observations deduplicate across Controller restarts.
68
+ const digest = createHash("sha256").update(JSON.stringify([
69
+ task.id, candidates.map(entry => entry.fence), failure.raw,
70
+ result.schedule?.circuitOpenUntil === undefined ? "unavailable" : "circuit-open"
71
+ ])).digest("hex");
72
+ const eventId = `continuation-reconcile-error:${digest}`;
73
+ if (!events.some(event => event.payload.eventId === eventId
74
+ || event.type === "runtime.agent-error" && event.payload.sourceEventId === eventId)) {
75
+ const observation = createRuntimeObservation({
76
+ schemaVersion: 1, eventId, semanticKey: eventId,
77
+ kind: "observer.health", authority: "controller",
78
+ receivedAt: now.toISOString(), observedAt: now.toISOString(),
79
+ fence: candidates[0].fence,
80
+ payload: {
81
+ sourceId: "continuation-metadata", observerStatus: "unavailable",
82
+ observerDetail: failure.message,
83
+ failure: { error: { ...failure, raw: serializeAgentErrorRaw({
84
+ failure, targets: group.map(entry => entry.identity),
85
+ schedule: result.schedule,
86
+ confirmed: "Metadata query did not prove quiescence; existing writer ownership is preserved.",
87
+ unknown: "Continuation execution, result and external effects."
88
+ }) } }
89
+ }
90
+ });
91
+ if (this.sink.observeRuntimeObservation(observation, now) === "applied")
92
+ changedTaskIds.add(task.id);
93
+ }
94
+ }
95
+ for (let index = 0; index < group.length; index += 1) {
96
+ const before = group[index];
97
+ const after = result.continuations[index];
98
+ if (sameContinuationState(before, after))
99
+ continue;
100
+ const disposition = this.sink.observeRuntimeObservation(reconciliationObservation(after, candidates[index].fence, now), now);
101
+ if (disposition === "applied")
102
+ changedTaskIds.add(after.taskId);
103
+ }
61
104
  }
62
105
  }
106
+ catch (error) {
107
+ this.onError(new Error(`Continuation reconciliation failed for ${task.id}: `
108
+ + `${error instanceof Error ? error.message : String(error)}. `
109
+ + "Unconfirmed continuation state and writer ownership are retained.", { cause: error }));
110
+ }
63
111
  }
64
112
  return Object.freeze([...changedTaskIds].sort());
65
113
  }
@@ -128,7 +176,7 @@ function reconciliationObservation(continuation, sourceFence, now) {
128
176
  const settled = continuation.execution === "quiescent"
129
177
  && continuation.observation === "exact";
130
178
  return createRuntimeObservation({
131
- schemaVersion: 4,
179
+ schemaVersion: 1,
132
180
  eventId: `continuation-reconcile:${digest}`,
133
181
  semanticKey: `continuation-reconcile:${digest}`,
134
182
  kind: settled ? "continuation.settled" : "continuation.started",
@@ -152,6 +152,21 @@ export function providerRetryProjection(binding) {
152
152
  category: retry.error.category, preservesWork: true, nativeSessionId: retry.nativeSessionId,
153
153
  ...(retry.reason === undefined ? {} : { reason: retry.reason }),
154
154
  failureRef: retry.failureRef,
155
+ error: retry.error,
156
+ inputRef: retry.input.kind === "text" ? { kind: "text" } : retry.input,
157
+ failedAttemptId: retry.failedAttemptId,
158
+ failureAttemptId: retry.failureAttemptId,
159
+ ...(retry.failedNativeTurnId === undefined ? {} : { failedNativeTurnId: retry.failedNativeTurnId }),
160
+ ...(retry.currentAttemptId === undefined ? {} : { currentAttemptId: retry.currentAttemptId }),
161
+ currentTurn: binding?.run == null ? null : {
162
+ attemptId: binding.run.attemptId, status: binding.run.status,
163
+ ...(binding.run.nativeTurnId === undefined ? {} : { nativeTurnId: binding.run.nativeTurnId }),
164
+ ...(binding.run.failure === undefined ? {} : { failure: binding.run.failure })
165
+ },
166
+ history: (binding?.retryHistory ?? []).map(entry => ({
167
+ chainId: entry.chainId, status: entry.status, attempts: entry.attempts,
168
+ failureRef: entry.failureRef, failedAttemptId: entry.failedAttemptId
169
+ })),
155
170
  ...(retry.previousRunId === undefined ? {} : { previousRunId: retry.previousRunId }),
156
171
  ...(retry.successorRunId === undefined ? {} : { successorRunId: retry.successorRunId })
157
172
  };
@@ -3,7 +3,7 @@ import { isStandardAgentError } from "./agentError.js";
3
3
  export function createProviderRuntimeBinding(input) {
4
4
  const startedAt = timestamp(input.startedAt, "Provider Conversation startedAt");
5
5
  return validateProviderRuntimeBinding({
6
- schemaVersion: 5,
6
+ schemaVersion: 1,
7
7
  providerNamespace: identity(input.providerNamespace, "Provider namespace"),
8
8
  accountScope: identity(input.accountScope, "Provider account scope"),
9
9
  currentConversationEpoch: 1,
@@ -308,8 +308,8 @@ export function supersedeProviderConversation(raw, input) {
308
308
  });
309
309
  }
310
310
  export function validateProviderRuntimeBinding(value) {
311
- if (value.schemaVersion !== 5)
312
- throw new Error("Provider Runtime Binding schemaVersion must be 5.");
311
+ if (value.schemaVersion !== 1)
312
+ throw new Error("Provider Runtime Binding schemaVersion must be 1.");
313
313
  identity(value.providerNamespace, "Provider namespace");
314
314
  identity(value.accountScope, "Provider account scope");
315
315
  integer(value.currentConversationEpoch, 1, "Current Provider Conversation epoch");
@@ -1,4 +1,5 @@
1
1
  import { continuationOwnsWriterUmbrella, observeProviderContinuation, providerContinuationKey } from "./providerContinuation.js";
2
+ import { serializeAgentErrorRaw, standardAgentError } from "./agentError.js";
2
3
  const BASE_RECONCILE_MS = 2_000;
3
4
  const MAX_RECONCILE_MS = 5 * 60_000;
4
5
  const CIRCUIT_ERROR_LIMIT = 5;
@@ -47,8 +48,11 @@ export async function reconcileKnownDetachedContinuations(input) {
47
48
  })))
48
49
  }), QUERY_TIMEOUT_MS);
49
50
  result = validateQueryResult(raw, new Set(candidates.map((entry) => (providerContinuationKey(entry.identity)))));
51
+ if (result.quality === "unavailable") {
52
+ throw new Error(result.detail ?? "Provider continuation metadata is unavailable.");
53
+ }
50
54
  }
51
- catch {
55
+ catch (error) {
52
56
  const errors = (input.previous?.consecutiveErrors ?? 0) + 1;
53
57
  const delay = backoff(input.previous?.attempts ?? 0);
54
58
  const circuitOpenUntil = errors >= CIRCUIT_ERROR_LIMIT
@@ -64,7 +68,13 @@ export async function reconcileKnownDetachedContinuations(input) {
64
68
  ...(circuitOpenUntil === undefined ? {} : { circuitOpenUntil })
65
69
  },
66
70
  quality: "unavailable",
67
- changed: false
71
+ changed: false,
72
+ failure: standardAgentError({
73
+ source: "driver", phase: "turn-reconcile",
74
+ message: error instanceof Error ? error.message : String(error),
75
+ raw: serializeAgentErrorRaw(error),
76
+ inputDisposition: "unknown", sessionDisposition: "unknown"
77
+ })
68
78
  };
69
79
  }
70
80
  const observations = new Map(result.continuations.map((entry) => [entry.key, entry]));
@@ -1,4 +1,5 @@
1
1
  import { resolve } from "node:path";
2
+ import { isStorageVersion, compareStorageVersions } from "../storage/storageVersions.js";
2
3
  import { callController as defaultCallController } from "../core/controllerClient.js";
3
4
  import { inspectStorageSchema } from "../storage/storageSchema.js";
4
5
  import { yuiVersionIdentity } from "../version.js";
@@ -41,9 +42,12 @@ function validateVersionIdentity(value) {
41
42
  throw new Error("Yui version identity is invalid.");
42
43
  const version = requireText(value.version, "Yui version");
43
44
  const controllerProtocolVersion = requireVersion(value.controllerProtocolVersion, "Controller protocol version");
44
- const storageVersion = requireVersion(value.storageVersion, "Storage version");
45
- const minimumStorageVersion = requireVersion(value.minimumStorageVersion, "Minimum storage migration version");
46
- if (minimumStorageVersion > storageVersion) {
45
+ const storageVersion = value.storageVersion;
46
+ const minimumStorageVersion = value.minimumStorageVersion;
47
+ if (!isStorageVersion(storageVersion) || !isStorageVersion(minimumStorageVersion)) {
48
+ throw new Error("Storage version must be a major.minor identity.");
49
+ }
50
+ if (compareStorageVersions(minimumStorageVersion, storageVersion) > 0) {
47
51
  throw new Error("Minimum storage migration version cannot exceed the current storage version.");
48
52
  }
49
53
  return {
@@ -84,8 +84,8 @@ const PROVIDER_STATE = new Set([
84
84
  "operation.failed"
85
85
  ]);
86
86
  export function createRuntimeObservation(input) {
87
- if (input.schemaVersion !== 4)
88
- throw new Error("Runtime observation schemaVersion must be 4.");
87
+ if (input.schemaVersion !== 1)
88
+ throw new Error("Runtime observation schemaVersion must be 1.");
89
89
  if (!KINDS.includes(input.kind))
90
90
  throw new Error("Runtime observation kind is invalid.");
91
91
  if (!AUTHORITIES.includes(input.authority))
@@ -126,7 +126,7 @@ export function createRuntimeObservation(input) {
126
126
  throw new Error("Runtime observation ordinal must be a non-negative safe integer.");
127
127
  }
128
128
  return Object.freeze({
129
- schemaVersion: 4,
129
+ schemaVersion: 1,
130
130
  eventId: requireIdentity(input.eventId, "Runtime observation event id"),
131
131
  semanticKey: requireIdentity(input.semanticKey, "Runtime observation semantic key"),
132
132
  kind: input.kind,
@@ -36,7 +36,7 @@ export function createSessionOwnerIdentity(input) {
36
36
  }
37
37
  const tmux = input.tmux;
38
38
  return Object.freeze({
39
- schemaVersion: 2,
39
+ schemaVersion: 1,
40
40
  kind: "yui-session-owner",
41
41
  owner: Object.freeze({
42
42
  scope: owner.scope,