@zq-silk/yui 0.16.0 → 0.99.0

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.

Potentially problematic release.


This version of @zq-silk/yui might be problematic. Click here for more details.

Files changed (114) hide show
  1. package/ARCHITECTURE.md +12 -0
  2. package/ARCHITECTURE.zh-CN.md +8 -0
  3. package/README.md +3 -0
  4. package/dist/agent/managedRuntimeEnvironment.js +0 -4
  5. package/dist/agentRun/agentRun.js +7 -17
  6. package/dist/cli/agentConfigurationPicker.js +24 -21
  7. package/dist/cli/commandCatalog.js +18 -10
  8. package/dist/cli/roleWizard.js +41 -30
  9. package/dist/cli/updateCommand.js +35 -13
  10. package/dist/cli/updateOrchestrator.js +73 -33
  11. package/dist/cli/updatePorts.js +51 -20
  12. package/dist/cli.js +35 -26
  13. package/dist/commands/controllerCommands.js +1 -1
  14. package/dist/commands/executionAuditCommands.js +2 -2
  15. package/dist/commands/projectCommands.js +1 -1
  16. package/dist/commands/releaseCommands.js +9 -38
  17. package/dist/commands/taskCommands.js +84 -81
  18. package/dist/commands/taskCompletionGate.js +0 -117
  19. package/dist/commands/taskRoleRuntimeStatus.js +1 -1
  20. package/dist/commands/taskUpstreamCommands.js +93 -54
  21. package/dist/context/runContextPack.js +30 -54
  22. package/dist/context/runInputContract.js +9 -0
  23. package/dist/context/sessionBootstrapManifest.js +11 -19
  24. package/dist/controller/agentHostObservation.js +4 -2
  25. package/dist/controller/controller.js +8 -6
  26. package/dist/controller/fileSchedulerStoreAdapter.js +85 -18
  27. package/dist/controller/globalInputDelivery.js +28 -7
  28. package/dist/controller/globalRuntimeAttention.js +34 -0
  29. package/dist/controller/operatorNotification.js +31 -0
  30. package/dist/controller/providerRetryAdmission.js +3 -1
  31. package/dist/controller/providerRetryDelivery.js +10 -3
  32. package/dist/controller/runtime.js +1 -27
  33. package/dist/controller/sessionOwnerReconciliation.js +42 -24
  34. package/dist/controller/structuredProviderObservation.js +27 -0
  35. package/dist/controller/updateReconciliation.js +72 -20
  36. package/dist/doctor/doctor.js +2 -1
  37. package/dist/executor/agentAdapter.js +28 -96
  38. package/dist/executor/agentConfigurationCatalog.js +13 -68
  39. package/dist/executor/agentConfigurationFields.js +120 -0
  40. package/dist/executor/agentConfigurationProbe.js +55 -80
  41. package/dist/executor/agentExecutor.js +5 -2
  42. package/dist/executor/executorRegistry.js +0 -9
  43. package/dist/executor/fileRoleLaunchPlanner.js +12 -11
  44. package/dist/interaction/operatorPresentation.js +2 -1
  45. package/dist/message/messageContinuation.js +8 -9
  46. package/dist/observability/executionAudit.js +1 -10
  47. package/dist/observability/runtimeIdentity.js +0 -23
  48. package/dist/output/agentConfigurationPresentation.js +4 -2
  49. package/dist/release/releaseHandover.js +2 -2
  50. package/dist/release/releaseIdempotencyStore.js +0 -23
  51. package/dist/release/releaseWorkflowPorts.js +8 -4
  52. package/dist/repository/gitWorkspace.js +66 -19
  53. package/dist/repository/taskWorkspacePreparer.js +75 -68
  54. package/dist/resources/resourceRegistry.js +90 -44
  55. package/dist/resources/sqliteResourceRegistry.js +3 -5
  56. package/dist/runtime/acpSession.js +21 -5
  57. package/dist/runtime/agentEndpoint.js +4 -1
  58. package/dist/runtime/agentHost.js +22 -62
  59. package/dist/runtime/agentHostCleanup.js +85 -0
  60. package/dist/runtime/agentHostProtocol.js +1 -1
  61. package/dist/runtime/builtinAgentDrivers.js +46 -119
  62. package/dist/runtime/builtinTranscriptObserver.js +10 -6
  63. package/dist/runtime/codexAppServerRuntime.js +55 -89
  64. package/dist/runtime/jsonLineChannel.js +35 -7
  65. package/dist/runtime/providerContinuationReconciliationService.js +42 -3
  66. package/dist/runtime/providerRetry.js +15 -0
  67. package/dist/runtime/providerRuntimeReconciler.js +12 -2
  68. package/dist/runtime/structuredProviderHost.js +106 -81
  69. package/dist/scheduler/activeRoleRunDelivery.js +5 -1
  70. package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
  71. package/dist/storage/upgrade/upgradeOrchestrator.js +0 -10
  72. package/dist/task/nextAction.js +33 -23
  73. package/dist/task/task.js +2 -1
  74. package/dist/web/assets/assetManifest.js +2 -0
  75. package/dist/web/assets/client/app.js +72 -3
  76. package/dist/web/assets/client/components.js +2 -1
  77. package/dist/web/assets/client/i18n.js +2 -0
  78. package/dist/web/assets/client/taskSummary.js +345 -0
  79. package/dist/web/assets/client/taskSurface.js +44 -26
  80. package/dist/web/assets/client/view.js +33 -0
  81. package/dist/web/assets/shell.js +1 -0
  82. package/dist/web/assets/styles/cards.js +21 -0
  83. package/dist/web/webServer.js +39 -5
  84. package/dist/web/webSessions.js +165 -0
  85. package/dist/web/webSnapshot.js +23 -0
  86. package/dist/web/webTaskSurface.js +23 -0
  87. package/docs/managed-turn-and-session-runtime.md +31 -0
  88. package/docs/managed-turn-and-session-runtime.zh-CN.md +23 -0
  89. package/docs/observability/README.md +47 -0
  90. package/docs/observability/README.zh-CN.md +37 -0
  91. package/docs/project-refresh.md +9 -0
  92. package/docs/project-refresh.zh-CN.md +8 -0
  93. package/docs/provider-retry.md +35 -0
  94. package/docs/release-workflow.md +128 -20
  95. package/docs/release-workflow.zh-CN.md +107 -14
  96. package/docs/roles-and-configuration.md +30 -0
  97. package/docs/roles-and-configuration.zh-CN.md +20 -0
  98. package/docs/task-delivery.md +109 -0
  99. package/docs/task-delivery.zh-CN.md +80 -0
  100. package/docs/task-discovery.md +9 -0
  101. package/docs/task-discovery.zh-CN.md +6 -0
  102. package/docs/testing/verification-levels.md +33 -12
  103. package/docs/testing/verification-levels.zh-CN.md +28 -8
  104. package/i18n/README.zh-CN.md +2 -0
  105. package/package.json +1 -1
  106. package/skills/yui-leader/SKILL.md +11 -0
  107. package/skills/yui-leader/references/execution.md +22 -0
  108. package/skills/yui-leader/references/planning.md +6 -2
  109. package/skills/yui-operator/SKILL.md +45 -13
  110. package/skills/yui-operator/references/task-delivery.md +99 -0
  111. package/skills/yui-runtime/SKILL.md +5 -3
  112. package/skills/yui-runtime/references/publication.md +56 -5
  113. package/skills/yui-runtime/references/recovery.md +12 -2
  114. package/dist/runtime/agentHostCompatibility.js +0 -127
@@ -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) {
@@ -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;
@@ -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";
@@ -39,17 +40,55 @@ export class ProviderContinuationReconciliationService {
39
40
  now
40
41
  });
41
42
  }
42
- catch {
43
+ catch (error) {
43
44
  // One malformed identity group must not restart or block the
44
45
  // Controller. Keep it writer-owned and retry through the same
45
46
  // bounded, metadata-only schedule.
46
- this.#schedules.set(groupKey, failureSchedule(groupKey, previous, now));
47
- continue;
47
+ result = {
48
+ continuations: group, quality: "unavailable", changed: false,
49
+ schedule: failureSchedule(groupKey, previous, now),
50
+ failure: standardAgentError({
51
+ source: "yui", phase: "turn-reconcile",
52
+ message: error instanceof Error ? error.message : String(error),
53
+ raw: serializeAgentErrorRaw(error)
54
+ })
55
+ };
48
56
  }
49
57
  if (result.schedule === null)
50
58
  this.#schedules.delete(groupKey);
51
59
  else
52
60
  this.#schedules.set(groupKey, result.schedule);
61
+ if (result.failure !== undefined) {
62
+ const failure = result.failure;
63
+ // One original failure and one circuit-limit notice, not one wake per
64
+ // poll. Existing observations deduplicate across Controller restarts.
65
+ const digest = createHash("sha256").update(JSON.stringify([
66
+ task.id, candidates.map(entry => entry.fence), failure.raw,
67
+ result.schedule?.circuitOpenUntil === undefined ? "unavailable" : "circuit-open"
68
+ ])).digest("hex");
69
+ const eventId = `continuation-reconcile-error:${digest}`;
70
+ if (!events.some(event => event.payload.eventId === eventId
71
+ || event.type === "runtime.agent-error" && event.payload.sourceEventId === eventId)) {
72
+ const observation = createRuntimeObservation({
73
+ schemaVersion: 4, eventId, semanticKey: eventId,
74
+ kind: "observer.health", authority: "controller",
75
+ receivedAt: now.toISOString(), observedAt: now.toISOString(),
76
+ fence: candidates[0].fence,
77
+ payload: {
78
+ sourceId: "continuation-metadata", observerStatus: "unavailable",
79
+ observerDetail: failure.message,
80
+ failure: { error: { ...failure, raw: serializeAgentErrorRaw({
81
+ failure, targets: group.map(entry => entry.identity),
82
+ schedule: result.schedule,
83
+ confirmed: "Metadata query did not prove quiescence; existing writer ownership is preserved.",
84
+ unknown: "Continuation execution, result and external effects."
85
+ }) } }
86
+ }
87
+ });
88
+ if (this.sink.observeRuntimeObservation(observation, now) === "applied")
89
+ changedTaskIds.add(task.id);
90
+ }
91
+ }
53
92
  for (let index = 0; index < group.length; index += 1) {
54
93
  const before = group[index];
55
94
  const after = result.continuations[index];
@@ -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
  };
@@ -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]));