@zq-silk/yui 2.5.0 → 2.7.1

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 (104) hide show
  1. package/README.md +23 -0
  2. package/dist/cli/commandCatalog.js +31 -4
  3. package/dist/commands/globalRoleCommands.js +22 -2
  4. package/dist/commands/inboxCommands.js +352 -0
  5. package/dist/commands/roleSkillValidation.js +2 -0
  6. package/dist/commands/taskCommands.js +40 -7
  7. package/dist/commands/taskContextCommand.js +3 -2
  8. package/dist/context/currentSkills.js +48 -0
  9. package/dist/context/roleSessionContext.js +13 -16
  10. package/dist/context/runContextPack.js +2 -0
  11. package/dist/context/runInputContract.js +1 -1
  12. package/dist/context/sessionBootstrapManifest.js +27 -4
  13. package/dist/context/skillCatalog.js +2 -3
  14. package/dist/context/taskContext.js +9 -4
  15. package/dist/controlPlaneCli.js +18 -1
  16. package/dist/controller/controller.js +3 -2
  17. package/dist/controller/fileSchedulerStoreAdapter.js +19 -4
  18. package/dist/controller/globalInputDelivery.js +73 -15
  19. package/dist/controller/globalRuntimeAttention.js +1 -1
  20. package/dist/controller/operatorNotification.js +6 -1
  21. package/dist/controller/providerRetryDelivery.js +3 -5
  22. package/dist/controller/runtime.js +2 -2
  23. package/dist/doctor/doctor.js +25 -2
  24. package/dist/executor/effectiveLaunch.js +2 -2
  25. package/dist/executor/fileRoleLaunchPlanner.js +4 -6
  26. package/dist/interaction/notificationPresentation.js +2 -2
  27. package/dist/interaction/operatorPresentation.js +1 -1
  28. package/dist/kernel/builtinCapabilities.js +2 -1
  29. package/dist/message/globalInputBacklog.js +69 -0
  30. package/dist/message/globalMessageQuery.js +1 -0
  31. package/dist/message/inboxReceipt.js +32 -0
  32. package/dist/message/message.js +6 -1
  33. package/dist/message/messageContinuation.js +34 -13
  34. package/dist/message/taskInboxOwnership.js +21 -0
  35. package/dist/nativeAgent/session/backends.js +1 -1
  36. package/dist/observability/runtimeIdentity.js +1 -1
  37. package/dist/resources/sqliteResourceRegistry.js +1 -1
  38. package/dist/runtime/agentHost.js +105 -16
  39. package/dist/runtime/builtinAgentErrorMappers.js +9 -5
  40. package/dist/runtime/providerRetry.js +38 -0
  41. package/dist/runtime/taskSessions.js +240 -0
  42. package/dist/scheduler/operatorEvent.js +49 -0
  43. package/dist/scheduler/operatorInputNotificationProcessor.js +5 -4
  44. package/dist/scheduler/taskExecutionProjection.js +78 -6
  45. package/dist/scheduler/taskWake.js +5 -0
  46. package/dist/sqlite/driver.js +108 -0
  47. package/dist/storage/sqliteSchema.js +11 -1
  48. package/dist/storage/sqliteStore.js +60 -1
  49. package/dist/storage/storageSchema.js +1 -1
  50. package/dist/storage/storageVersions.js +1 -1
  51. package/dist/storage/storeRpc.js +1 -0
  52. package/dist/storage/upgrade/upgradeOrchestrator.js +1 -1
  53. package/dist/task/completionReadiness.js +11 -4
  54. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  55. package/dist/web/assets/assetManifest.js +2 -0
  56. package/dist/web/assets/client/app/taskView.js +1 -0
  57. package/dist/web/assets/client/domain/records.js +70 -2
  58. package/dist/web/assets/client/domain/runs.js +3 -2
  59. package/dist/web/assets/client/domain/vocab.js +1 -1
  60. package/dist/web/assets/client/i18n/en.js +17 -1
  61. package/dist/web/assets/client/i18n/zh-CN.js +17 -1
  62. package/dist/web/assets/client/lib/api.js +4 -1
  63. package/dist/web/assets/client/lib/theme.js +31 -10
  64. package/dist/web/assets/client/views/task/actions.js +38 -21
  65. package/dist/web/assets/client/views/task/observation.js +12 -3
  66. package/dist/web/assets/client/views/task/overview.js +7 -40
  67. package/dist/web/assets/client/views/task/runtime.js +71 -34
  68. package/dist/web/assets/shared/icons.js +2 -0
  69. package/dist/web/assets/shell/index.js +1 -0
  70. package/dist/web/assets/shell/settings.js +1 -0
  71. package/dist/web/assets/styles/views/task.js +14 -0
  72. package/dist/web/webServer.js +12 -0
  73. package/dist/web/webSessions.js +2 -165
  74. package/dist/web/webSnapshot.js +9 -5
  75. package/dist/web/webTaskActivity.js +54 -0
  76. package/dist/web/webTaskSurface.js +1 -0
  77. package/docs/managed-turn-and-session-runtime.md +81 -5
  78. package/docs/managed-turn-and-session-runtime.zh-CN.md +51 -3
  79. package/docs/observability/README.md +25 -1
  80. package/docs/observability/README.zh-CN.md +18 -1
  81. package/docs/provider-retry.md +11 -0
  82. package/docs/release-workflow.md +74 -2
  83. package/docs/release-workflow.zh-CN.md +56 -1
  84. package/docs/storage-baseline.md +17 -7
  85. package/docs/storage-baseline.zh-CN.md +11 -5
  86. package/docs/task-delivery.md +17 -8
  87. package/docs/task-delivery.zh-CN.md +11 -5
  88. package/docs/testing/verification-levels.md +22 -4
  89. package/docs/testing/verification-levels.zh-CN.md +8 -1
  90. package/i18n/README.zh-CN.md +19 -0
  91. package/package.json +5 -2
  92. package/skills/yui-leader/SKILL.md +28 -11
  93. package/skills/yui-leader/VERSION +1 -0
  94. package/skills/yui-operator/SKILL.md +10 -2
  95. package/skills/yui-operator/VERSION +1 -0
  96. package/skills/yui-operator/references/task-delivery.md +2 -0
  97. package/skills/yui-reviewer/SKILL.md +8 -1
  98. package/skills/yui-reviewer/VERSION +1 -0
  99. package/skills/yui-runtime/SKILL.md +94 -11
  100. package/skills/yui-runtime/VERSION +1 -0
  101. package/skills/yui-runtime/references/publication.md +16 -5
  102. package/skills/yui-runtime/references/recovery.md +9 -0
  103. package/skills/yui-worker/SKILL.md +8 -1
  104. package/skills/yui-worker/VERSION +1 -0
@@ -1,4 +1,4 @@
1
- import Database from 'better-sqlite3';
1
+ import Database from "../../sqlite/driver.js";
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { isAbsolute, resolve } from 'node:path';
4
4
  import { existsSync, lstatSync } from 'node:fs';
@@ -12,7 +12,7 @@ import { createHash } from "node:crypto";
12
12
  import { spawnSync } from "node:child_process";
13
13
  import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
14
14
  import { join } from "node:path";
15
- import Database from "better-sqlite3";
15
+ import Database from "../sqlite/driver.js";
16
16
  import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "../storage/storageVersions.js";
17
17
  import { inspectStorageSchema } from "../storage/storageSchema.js";
18
18
  import { resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
@@ -13,7 +13,7 @@
13
13
  import { existsSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { isDeepStrictEqual } from "node:util";
16
- import Database from "better-sqlite3";
16
+ import Database from "../sqlite/driver.js";
17
17
  import { validateSqliteSchema } from "../storage/sqliteSchema.js";
18
18
  import { emptyResourceRegistry, parseResourceRegistryState } from "./resourceRegistry.js";
19
19
  import { RESOURCE_REGISTRY_SCHEMA_VERSION } from "./resourceTypes.js";
@@ -93,6 +93,9 @@ export async function runAgentHost(input) {
93
93
  let activeRunPayload;
94
94
  let lastTerminalPayload;
95
95
  let lastTerminal;
96
+ // One exact direct Turn witnessed by this Endpoint under its original fence.
97
+ // This is recovery evidence, never ownership of the Desktop client's input.
98
+ let directObservation;
96
99
  let activeRunAttemptId;
97
100
  let activeNativeTurnId;
98
101
  let codexClientAttachedAt;
@@ -166,6 +169,7 @@ export async function runAgentHost(input) {
166
169
  const handleStarted = (started, observedPayload = sessionPayload) => {
167
170
  if (started.clientOwned || observedPayload === undefined)
168
171
  return;
172
+ const observedAuthority = authority === undefined ? undefined : { ...authority };
169
173
  updateSnapshot(hostSnapshot("busy", {
170
174
  adapterId: observedPayload.providerControl?.adapterId,
171
175
  processInstanceId: session?.processInstanceId,
@@ -175,6 +179,22 @@ export async function runAgentHost(input) {
175
179
  ...authorityFields()
176
180
  }));
177
181
  void enqueueSerialized(async () => {
182
+ // Desktop can start another Turn before the Controller's periodic pass.
183
+ // Persist the previous exact terminal before replacing its bounded cache
184
+ // or publishing the new start. Never clear the newer native writer.
185
+ const previous = directObservation;
186
+ if (previous?.terminal !== undefined && authority !== undefined
187
+ && sameProviderAuthorityFence(previous.authority, authority)
188
+ && previous.started.nativeSessionId === session?.nativeSessionId) {
189
+ await publishStructuredProviderTerminal({
190
+ home: input.home, environment: previous.payload.environment, terminal: previous.terminal
191
+ });
192
+ }
193
+ if (session !== undefined && started.nativeSessionId === session.nativeSessionId
194
+ && started.conversationId === session.conversationId && observedAuthority !== undefined
195
+ && authority !== undefined && sameProviderAuthorityFence(observedAuthority, authority)) {
196
+ directObservation = { started, payload: observedPayload, authority: observedAuthority };
197
+ }
178
198
  await publishStructuredProviderStarted({
179
199
  home: input.home,
180
200
  environment: observedPayload.environment,
@@ -194,6 +214,14 @@ export async function runAgentHost(input) {
194
214
  // AgentRun audit record. It has no WorkItem ownership and no managed input
195
215
  // fence; the runtime observer records only visible input/output.
196
216
  void enqueueSerialized(async () => {
217
+ const direct = directObservation;
218
+ if (direct !== undefined && authority !== undefined
219
+ && sameProviderAuthorityFence(direct.authority, authority)
220
+ && direct.started.nativeSessionId === terminal.nativeSessionId
221
+ && direct.started.conversationId === terminal.conversationId
222
+ && direct.started.nativeTurnId === terminal.nativeTurnId) {
223
+ direct.terminal = terminal;
224
+ }
197
225
  await publishStructuredProviderTerminal({
198
226
  home: input.home,
199
227
  environment: busyPayload.environment,
@@ -274,6 +302,7 @@ export async function runAgentHost(input) {
274
302
  conversationId: terminal.conversationId,
275
303
  ...authorityFields()
276
304
  }));
305
+ signalRoleMailbox(input.home, currentPayload);
277
306
  promptHuman();
278
307
  }
279
308
  catch (error) {
@@ -621,6 +650,7 @@ export async function runAgentHost(input) {
621
650
  await recordProviderConnection(input.home, next, started.session);
622
651
  lastTerminalPayload = undefined;
623
652
  lastTerminal = undefined;
653
+ directObservation = undefined;
624
654
  // Recoverable means a later process can rebind this Conversation by
625
655
  // its native id. The Endpoint answers that from the Driver capability
626
656
  // or, where the protocol settles it per connection, from what this
@@ -1157,6 +1187,51 @@ export async function runAgentHost(input) {
1157
1187
  }
1158
1188
  return controlResult("accepted", { ...snapshot, nativeTui: !!nativeTui });
1159
1189
  }
1190
+ if (request.type === "reconcile-turn") {
1191
+ if (session === undefined || session.nativeSessionId !== request.nativeSessionId
1192
+ || authority === undefined || !sameProviderAuthorityFence(authority, request.authority)) {
1193
+ throw new Error("Terminal reconciliation targets a different Session or authority.");
1194
+ }
1195
+ const direct = directObservation;
1196
+ const directMatch = direct?.terminal !== undefined
1197
+ && request.nativeTurnId !== undefined
1198
+ && request.attemptId === `direct:${request.nativeTurnId}`
1199
+ && direct.started.nativeTurnId === request.nativeTurnId
1200
+ && direct.started.nativeSessionId === request.nativeSessionId
1201
+ && sameProviderAuthorityFence(direct.authority, request.authority);
1202
+ const terminal = directMatch ? direct.terminal : lastTerminal;
1203
+ const terminalPayload = directMatch ? direct.payload : lastTerminalPayload;
1204
+ if (terminal === undefined || terminalPayload === undefined
1205
+ || terminal.nativeSessionId !== request.nativeSessionId
1206
+ || !directMatch && (terminal.clientOwned !== true || terminal.attemptId !== request.attemptId)
1207
+ || request.nativeTurnId !== undefined && terminal.nativeTurnId !== request.nativeTurnId) {
1208
+ return { ...controlResult("status", snapshot), reconciliation: "unobserved" };
1209
+ }
1210
+ // Serialize with submission/terminal persistence. The original Host and
1211
+ // original launch envelope remain the observer, including the full result.
1212
+ await enqueueSerialized(async () => {
1213
+ if (session?.nativeSessionId !== request.nativeSessionId || authority === undefined
1214
+ || !sameProviderAuthorityFence(authority, request.authority)
1215
+ || directMatch && directObservation !== direct) {
1216
+ throw new Error("Session changed before terminal reconciliation.");
1217
+ }
1218
+ await publishStructuredProviderTerminal({
1219
+ home: input.home, environment: terminalPayload.environment, terminal
1220
+ });
1221
+ if (!directMatch && activeRunAttemptId === terminal.attemptId) {
1222
+ activeRunPayload = undefined;
1223
+ activeRunAttemptId = undefined;
1224
+ activeNativeTurnId = undefined;
1225
+ updateSnapshot(hostSnapshot(session.inspect().activeNativeTurnId === undefined ? "idle" : "busy", {
1226
+ adapterId: session.adapterId, processInstanceId: session.processInstanceId,
1227
+ nativeSessionId: session.nativeSessionId, conversationId: session.conversationId,
1228
+ ...authorityFields()
1229
+ }));
1230
+ }
1231
+ signalRoleMailbox(input.home, terminalPayload);
1232
+ });
1233
+ return { ...controlResult("status", snapshot), reconciliation: "terminal-republished" };
1234
+ }
1160
1235
  if (request.type === "status") {
1161
1236
  // Read the Agent's configuration at answer time, not from the stored
1162
1237
  // snapshot. The Agent may have changed it since the last state
@@ -1461,6 +1536,11 @@ export async function inspectAgentHost(input) {
1461
1536
  });
1462
1537
  return result.snapshot;
1463
1538
  }
1539
+ export async function reconcileAgentHostTurn(input) {
1540
+ // Periodic observation must not hold the scheduler behind an unavailable Host.
1541
+ // A timeout is only an observation failure, never permission to submit again.
1542
+ return exchangeAgentHostControl(agentHostControlSocketPath(input), input.control, 1_000);
1543
+ }
1464
1544
  export async function waitForAgentHostLaunchAck(input) {
1465
1545
  const deadline = Date.now() + (input.timeoutMs ?? AGENT_HOST_READY_TIMEOUT_MS);
1466
1546
  let lastError;
@@ -1616,7 +1696,7 @@ export async function openAgentHostControl(home, payload, snapshot, dispatch) {
1616
1696
  ...result,
1617
1697
  snapshot: {
1618
1698
  ...result.snapshot,
1619
- ...(payload.environment.YUI_SESSION_SCOPE !== "task" ? {} : {
1699
+ ...(!["task", "global"].includes(payload.environment.YUI_SESSION_SCOPE ?? "") ? {} : {
1620
1700
  eventDelivery: structuredProviderEventDelivery(home, payload.environment, result.snapshot.nativeSessionId)
1621
1701
  })
1622
1702
  }
@@ -1691,7 +1771,8 @@ function boundControlResponse(result) {
1691
1771
  // visible through an otherwise successful status request.
1692
1772
  result = {
1693
1773
  ...controlResult(result.outcome, validateSnapshot(result.snapshot), result.failure === undefined ? undefined : providerDeliveryFailure(result.failure)),
1694
- ...(result.cancellation === undefined ? {} : { cancellation: result.cancellation })
1774
+ ...(result.cancellation === undefined ? {} : { cancellation: result.cancellation }),
1775
+ ...(result.reconciliation === undefined ? {} : { reconciliation: result.reconciliation })
1695
1776
  };
1696
1777
  if (withinControlBound(result))
1697
1778
  return result;
@@ -1709,29 +1790,29 @@ function boundControlResponse(result) {
1709
1790
  };
1710
1791
  // Never make an omitted option list look like a complete empty enumeration.
1711
1792
  if (result.snapshot.runConfiguration?.status === "observed") {
1712
- result = controlResult(result.outcome, validateSnapshot({
1713
- ...result.snapshot,
1714
- runConfiguration: unknownAgentRunConfiguration("The Agent's reported configuration is too large for the Agent Host "
1715
- + "control response, so it could not be carried in full and none of it "
1716
- + "is shown rather than part of it.")
1717
- }), result.failure);
1793
+ result = { ...result, ...controlResult(result.outcome, validateSnapshot({
1794
+ ...result.snapshot,
1795
+ runConfiguration: unknownAgentRunConfiguration("The Agent's reported configuration is too large for the Agent Host "
1796
+ + "control response, so it could not be carried in full and none of it "
1797
+ + "is shown rather than part of it.")
1798
+ }), result.failure) };
1718
1799
  if (withinControlBound(result))
1719
1800
  return result;
1720
1801
  }
1721
- let bounded = controlResult(result.outcome, {
1722
- ...result.snapshot,
1723
- ...(result.snapshot.detail === undefined ? {} : { detail: clip(result.snapshot.detail, 1200) })
1724
- }, result.failure === undefined ? undefined : { ...result.failure, detail: clip(result.failure.detail, 1200) });
1802
+ let bounded = { ...result, ...controlResult(result.outcome, {
1803
+ ...result.snapshot,
1804
+ ...(result.snapshot.detail === undefined ? {} : { detail: clip(result.snapshot.detail, 1200) })
1805
+ }, result.failure === undefined ? undefined : { ...result.failure, detail: clip(result.failure.detail, 1200) }) };
1725
1806
  const raw = bounded.failure?.raw;
1726
1807
  if (withinControlBound(bounded) || raw === undefined)
1727
1808
  return bounded;
1728
1809
  let chars = raw.length;
1729
1810
  while (!withinControlBound(bounded) && chars > 0) {
1730
1811
  chars = Math.floor(chars / 2);
1731
- bounded = controlResult(bounded.outcome, bounded.snapshot, {
1732
- ...bounded.failure,
1733
- raw: clip(raw, chars)
1734
- });
1812
+ bounded = { ...bounded, ...controlResult(bounded.outcome, bounded.snapshot, {
1813
+ ...bounded.failure,
1814
+ raw: clip(raw, chars)
1815
+ }) };
1735
1816
  }
1736
1817
  return bounded;
1737
1818
  }
@@ -1764,6 +1845,14 @@ function validateControl(control) {
1764
1845
  }
1765
1846
  if (control.type === "status")
1766
1847
  return Object.freeze({ ...control });
1848
+ if (control.type === "reconcile-turn") {
1849
+ validateIdentity(control.nativeSessionId, "native Session id");
1850
+ validateIdentity(control.attemptId, "Provider input attempt id");
1851
+ if (control.nativeTurnId !== undefined)
1852
+ validateIdentity(control.nativeTurnId, "native Turn id");
1853
+ validateProviderAuthorityFence(control.authority);
1854
+ return Object.freeze({ ...control });
1855
+ }
1767
1856
  if (control.type === "native-open" || control.type === "native-respond" || control.type === "native-model") {
1768
1857
  validateIdentity(control.nativeSessionId, "native Session id");
1769
1858
  if (control.type === "native-model" && (typeof control.model !== "string"
@@ -26,7 +26,7 @@ function mapClaudeDisplayError(input) {
26
26
  }
27
27
  /** Provider-specific Codex failure recognition. */
28
28
  export function mapCodexAgentError(input) {
29
- return withTransientEvidence(mapCodexDisplayError(input), input);
29
+ return withTransientEvidence(mapCodexDisplayError(input), input, true);
30
30
  }
31
31
  function mapCodexDisplayError(input) {
32
32
  const text = `${input.message}\n${input.raw}`;
@@ -208,7 +208,7 @@ function sessionUnavailable(code) {
208
208
  }
209
209
  /** Replay qualification is stricter than the readable, best-effort category.
210
210
  * Never promote a number occurring in provider prose to submission safety. */
211
- function structuredTransientError(input) {
211
+ function structuredTransientError(input, codex) {
212
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
213
  return { category: "access", code: "provider.quota-exhausted", retryable: false };
214
214
  const nodes = [];
@@ -228,7 +228,8 @@ function structuredTransientError(input) {
228
228
  catch { /* Display classification may still use prose. */ }
229
229
  const codes = nodes.flatMap(n => [n.code, n.type, n.error, n.codexErrorInfo]).filter(v => typeof v === "string");
230
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))) {
231
+ "model_not_found", "usage_limit_exceeded", "insufficient_quota"].includes(c))
232
+ || codex && nodes.some(n => ["usageLimitExceeded", "unauthorized", "badRequest"].includes(String(n.codexErrorInfo)))) {
232
233
  return { category: "access", code: "provider.access-denied", retryable: false };
233
234
  }
234
235
  const statuses = nodes.flatMap(n => [n.httpStatusCode, n.statusCode, n.status]).filter(v => typeof v === "number");
@@ -240,6 +241,9 @@ function structuredTransientError(input) {
240
241
  if (statuses.includes(429) || codes.includes("rate_limit_error") || nativeCode === "rate_limit_error") {
241
242
  result = recoverable("rate-limit", "provider.rate-limit");
242
243
  }
244
+ else if (codex && nodes.some(n => n.codexErrorInfo === "serverOverloaded")) {
245
+ result = recoverable("availability", "provider.model-capacity");
246
+ }
243
247
  else if (statuses.some(s => [500, 502, 503, 504].includes(s))
244
248
  || codes.some(c => ["server_error", "overloaded_error"].includes(c))
245
249
  || ["server_error", "overloaded_error"].includes(nativeCode)) {
@@ -277,8 +281,8 @@ function structuredTransientError(input) {
277
281
  }
278
282
  /** Add positive evidence without renaming established diagnostics or letting
279
283
  * a transient nested cause override a known fatal Session/request failure. */
280
- function withTransientEvidence(base, input) {
281
- const evidence = structuredTransientError(input);
284
+ function withTransientEvidence(base, input, codex = false) {
285
+ const evidence = structuredTransientError(input, codex);
282
286
  if (evidence === undefined)
283
287
  return base;
284
288
  const possiblyTransient = ["rate-limit", "availability", "transport", "unknown"].includes(base.category);
@@ -3,6 +3,44 @@ export const PROVIDER_RETRY_BUDGET_MS = 600_000;
3
3
  export function providerRetryPending(binding) {
4
4
  return binding?.retry?.status === "waiting" || binding?.retry?.status === "in-flight";
5
5
  }
6
+ /** An explicit Run retry can carry exact failed-input proof even when the
7
+ * error did not qualify for automation. This does not grant automatic replay:
8
+ * the shared admission and native latest-Turn/quiescence checks still apply. */
9
+ export function providerRunRetry(binding, runId, at) {
10
+ const turn = binding.run;
11
+ const nativeSessionId = binding.conversations.find(c => c.epoch === binding.currentConversationEpoch)?.conversationId;
12
+ if (turn?.runId !== runId || !["failed", "rejected", "deferred"].includes(turn.status)
13
+ || binding.authority.owner !== "controller" || turn.authorityEpoch !== binding.authority.epoch
14
+ || nativeSessionId === undefined)
15
+ return undefined;
16
+ const previous = binding.retry;
17
+ if (previous?.previousRunId === runId && previous.status !== "recovered"
18
+ && previous.failedAttemptId === turn.attemptId
19
+ && previous.authorityEpoch === binding.authority.epoch
20
+ && previous.nativeSessionId === nativeSessionId)
21
+ return previous;
22
+ const failure = turn.failure;
23
+ if (turn.status !== "failed" || turn.nativeTurnId === undefined || turn.input === undefined
24
+ || turn.retrySupported !== true || failure?.error.inputDisposition !== "accepted"
25
+ || failure.error.sessionDisposition !== "recoverable")
26
+ return undefined;
27
+ // A manual attempt can cancel automation when its error changes character.
28
+ // Refresh its failed-Turn proof without granting the same input a new budget.
29
+ const continuing = previous?.status !== "recovered" && previous?.currentAttemptId === turn.attemptId
30
+ && previous.authorityEpoch === binding.authority.epoch && previous.nativeSessionId === nativeSessionId;
31
+ return {
32
+ policyVersion: 1, chainId: continuing ? previous.chainId : turn.attemptId, status: "waiting",
33
+ input: continuing ? previous.input : turn.input, nativeSessionId, authorityEpoch: binding.authority.epoch,
34
+ attempts: continuing ? previous.attempts : 0,
35
+ transportSequence: continuing ? previous.transportSequence : 0,
36
+ startedAt: continuing ? previous.startedAt : iso(at), intentAt: iso(at),
37
+ deadline: continuing ? previous.deadline : iso(at + PROVIDER_RETRY_BUDGET_MS),
38
+ nextEligibleAt: iso(at), updatedAt: iso(at),
39
+ failedAttemptId: turn.attemptId, failureAttemptId: turn.attemptId,
40
+ failedNativeTurnId: turn.nativeTurnId, previousRunId: runId,
41
+ failureRef: failure.ref, error: failure.error, successorAutomatic: false
42
+ };
43
+ }
6
44
  /** Called only in the transaction recording the exact failed/rejected input. */
7
45
  export function recordProviderFailure(binding, input) {
8
46
  let turn = binding.run;
@@ -0,0 +1,240 @@
1
+ import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
2
+ import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
3
+ import { projectRuntimeTaskEvents, classifyRuntimeHealth } from "../runtime/runtimeProjection.js";
4
+ import { DEFAULT_RUNTIME_HEALTH_POLICY } from "../runtime/runtimeHealthPolicy.js";
5
+ import { providerRetryProjection } from "../runtime/providerRetry.js";
6
+ /** Read-only presentation of the selected native connections, never an AgentRun
7
+ * count. Foreground facts are fenced to the exact input; retained continuations
8
+ * stay attached to their original input and are not counted as extra Sessions.
9
+ * No live probe, timers, persistence or recovery actions are introduced here. */
10
+ export function projectTaskSessions(input) {
11
+ const policy = input.policy ?? DEFAULT_RUNTIME_HEALTH_POLICY;
12
+ const sessions = [];
13
+ const seen = new Map();
14
+ const selectedIdentities = new Set();
15
+ const observationIdentity = (fence) => JSON.stringify([fence.roleName, fence.agentId, fence.driverId, fence.nativeSessionId]);
16
+ const observations = input.events.map(event => ({ event, observation: runtimeObservationFromTaskEvent(event) }))
17
+ .filter(entry => entry.event.taskId === input.taskId && entry.observation?.fence.taskId === input.taskId);
18
+ for (const set of input.sessionSets) {
19
+ if (set.owner.scope !== "task" || set.owner.taskId !== input.taskId)
20
+ continue;
21
+ const session = set.sessions[set.activeAgentId];
22
+ const binding = set.providerBinding;
23
+ const conversation = binding?.conversations.find(c => c.epoch === binding.currentConversationEpoch && c.status === "current");
24
+ // Replacement can leave an empty set. That is absence, not uncertain
25
+ // execution; retained child facts are still discovered below.
26
+ if (!session && !conversation && !binding?.run)
27
+ continue;
28
+ const nativeSessionId = session?.nativeSessionId ?? conversation?.conversationId ?? null;
29
+ const identity = JSON.stringify([binding?.providerNamespace ?? session?.adapterId,
30
+ binding?.accountScope ?? set.activeAgentId, nativeSessionId ?? set.owner.roleName]);
31
+ const prior = seen.get(identity);
32
+ if (prior !== undefined) {
33
+ if (sessions[prior].roleName !== set.owner.roleName)
34
+ sessions[prior] = {
35
+ ...sessions[prior], group: "unknown", reason: "Multiple Roles name this native Session; inspect ownership."
36
+ };
37
+ continue;
38
+ }
39
+ seen.set(identity, sessions.length);
40
+ const turn = binding?.run;
41
+ let driverId = null;
42
+ if (session) {
43
+ try {
44
+ driverId = builtinDriverIdForAdapter(session.adapterId);
45
+ }
46
+ catch { /* Unsupported native observation remains explicitly unknown. */ }
47
+ }
48
+ const row = {
49
+ taskId: input.taskId, roleName: set.owner.roleName, agentId: set.activeAgentId,
50
+ driverId,
51
+ nativeSessionId, attemptId: turn?.attemptId ?? null, nativeTurnId: turn?.nativeTurnId ?? null,
52
+ runId: turn?.runId ?? null, group: "unknown", reason: "Current native input is not observable.",
53
+ layer: null, lastActivityAt: null, semanticProgressAt: input.semanticProgressAt ?? null,
54
+ sourceUpdatedAt: turn?.updatedAt ?? session?.updatedAt ?? set.updatedAt,
55
+ waitingReason: null, operations: [], background: []
56
+ };
57
+ if (!session || driverId === null) {
58
+ sessions.push(row);
59
+ continue;
60
+ }
61
+ const fence = { taskId: input.taskId, roleName: set.owner.roleName,
62
+ agentId: session.agentId, driverId, nativeSessionId: session.nativeSessionId };
63
+ selectedIdentities.add(observationIdentity(fence));
64
+ const sessionFacts = observations.filter(({ observation }) => Object.entries(fence).every(([key, value]) => observation.fence[key] === value));
65
+ // Each Run's continuation fold has its own authority. Foreground operations
66
+ // from older inputs must not make the current input look active.
67
+ const groups = new Map();
68
+ for (const { event, observation } of sessionFacts) {
69
+ const runId = observation.fence.runId;
70
+ const group = groups.get(runId) ?? [];
71
+ group.push(event);
72
+ groups.set(runId, group);
73
+ }
74
+ const background = backgroundForRuns(fence, session.createdAt, groups);
75
+ if (!binding || !conversation || conversation.conversationId !== session.nativeSessionId) {
76
+ sessions.push({ ...row, background });
77
+ continue;
78
+ }
79
+ if (!turn) {
80
+ const sessionEvents = sessionFacts.filter(({ observation }) => {
81
+ const f = observation.fence;
82
+ return f.runId === undefined && f.nativeTurnId === undefined
83
+ && f.receiptId === undefined && f.continuationId === undefined;
84
+ }).map(entry => entry.event);
85
+ const projection = projectRuntimeTaskEvents(fence, session.createdAt, sessionEvents);
86
+ const ready = session.status !== "ended" && projection.session === "ready"
87
+ && projection.host !== "exited" && projection.run === "none"
88
+ && projection.observer.status !== "unavailable" && projection.observer.status !== "degraded"
89
+ && projection.goal !== "active" && binding.goal?.status !== "active"
90
+ && Object.keys(projection.operations).length === 0;
91
+ sessions.push({ ...row, background, ...(background.length ? {
92
+ group: "background", reason: "Retained background execution is not settled."
93
+ } : session.status === "ended" || projection.session === "ended" ? {
94
+ group: "stopped", reason: "The selected Session ended with no submitted native input."
95
+ } : ready ? {
96
+ group: "idle", reason: "The selected Session is ready with no submitted native input."
97
+ } : {}) });
98
+ continue;
99
+ }
100
+ const exactEvents = sessionFacts.filter(({ observation }) => {
101
+ const observed = observation;
102
+ const f = observed.fence;
103
+ if (f.continuationId !== undefined || f.runId !== turn.runId)
104
+ return false;
105
+ if (f.receiptId === undefined && f.nativeTurnId === undefined
106
+ && ["host.observed", "session.ended", "session.failed", "observer.health"].includes(observed.kind)) {
107
+ return observed.receivedAt >= turn.submittedAt;
108
+ }
109
+ if (f.receiptId !== undefined && f.receiptId !== turn.attemptId)
110
+ return false;
111
+ if (f.nativeTurnId !== undefined && f.nativeTurnId !== turn.nativeTurnId)
112
+ return false;
113
+ return f.receiptId === turn.attemptId
114
+ || (turn.nativeTurnId !== undefined && f.nativeTurnId === turn.nativeTurnId);
115
+ }).map(entry => entry.event);
116
+ const projection = projectRuntimeTaskEvents({
117
+ ...fence, ...(turn.runId ? { runId: turn.runId } : {}),
118
+ receiptId: turn.attemptId, ...(turn.nativeTurnId ? { nativeTurnId: turn.nativeTurnId } : {})
119
+ }, turn.submittedAt, exactEvents);
120
+ const health = classifyRuntimeHealth({ projection, now: input.now, policy,
121
+ semanticProgressAt: input.semanticProgressAt ?? turn.submittedAt });
122
+ const retry = providerRetryProjection(binding);
123
+ let group;
124
+ let reason = health.reason;
125
+ if (turn.status === "delivery-unknown") {
126
+ group = "unknown";
127
+ reason = "Original input delivery is unknown; do not replay it.";
128
+ }
129
+ else if (session.status === "ended") {
130
+ group = background.length ? "background" : "stopped";
131
+ reason = "The selected Session ended; Task acceptance and retained background execution are separate.";
132
+ }
133
+ else if (retry?.status === "waiting") {
134
+ group = "waiting";
135
+ reason = `Controller-owned Provider retry waiting (${retry.attempts}/${retry.limit}); next eligible ${retry.nextEligibleAt}. Existing work is preserved.`;
136
+ }
137
+ else if (["failed", "cancelled", "rejected"].includes(turn.status)) {
138
+ group = background.length ? "background" : "stopped";
139
+ reason = turn.terminalReason ?? "The selected native execution stopped; Task acceptance is separate.";
140
+ }
141
+ else if (turn.status === "completed") {
142
+ group = background.length ? "background" : "idle";
143
+ reason = background.length ? "Main Turn ended; background execution remains unsettled."
144
+ : "The native Turn ended; this is not Task completion.";
145
+ }
146
+ else if (turn.status !== "accepted") {
147
+ group = "waiting";
148
+ reason = "Waiting for native input acceptance.";
149
+ }
150
+ else if (projection.run === "waiting") {
151
+ group = "waiting";
152
+ }
153
+ else if (["broken", "stopped"].includes(health.layer)) {
154
+ group = "stopped";
155
+ }
156
+ else if (["completed", "failed", "cancelled"].includes(projection.run)) {
157
+ group = "unknown";
158
+ reason = "Native terminal and current input binding disagree; inspect the exact original input.";
159
+ }
160
+ else if (projection.observer.status === "unavailable") {
161
+ group = "unknown";
162
+ }
163
+ else if (health.layer === "diagnostic-needed") {
164
+ group = "diagnostic";
165
+ }
166
+ else if (health.runtimeIdleMs < policy.quietAfterMs && projection.activity.kind !== "provider") {
167
+ group = "active";
168
+ }
169
+ else {
170
+ group = "quiet";
171
+ reason = "Accepted input has no recent structured activity; silence is not a proven deadlock.";
172
+ }
173
+ sessions.push({ ...row, group, reason, layer: health.layer, background,
174
+ lastActivityAt: projection.lastRuntimeActivityAt ?? null,
175
+ waitingReason: projection.waitingReason ?? null, operations: health.activeOperations });
176
+ }
177
+ // Retained, exactly fenced child observations can outlive a selected Session.
178
+ // Do not revive old foreground Turns or enumerate historical Sessions: only
179
+ // identities with still-unsettled child facts enter this current projection.
180
+ const retained = new Map();
181
+ for (const entry of observations) {
182
+ const fence = entry.observation.fence;
183
+ const key = observationIdentity(fence);
184
+ if (!fence.nativeSessionId || selectedIdentities.has(key))
185
+ continue;
186
+ const group = retained.get(key) ?? [];
187
+ group.push(entry);
188
+ retained.set(key, group);
189
+ }
190
+ for (const group of retained.values()) {
191
+ const first = group[0].observation;
192
+ const fence = first.fence;
193
+ const runs = new Map();
194
+ for (const entry of group) {
195
+ const runId = entry.observation.fence.runId;
196
+ const events = runs.get(runId) ?? [];
197
+ events.push(entry.event);
198
+ runs.set(runId, events);
199
+ }
200
+ const background = backgroundForRuns({
201
+ taskId: input.taskId, roleName: fence.roleName, agentId: fence.agentId,
202
+ driverId: fence.driverId, nativeSessionId: fence.nativeSessionId
203
+ }, first.receivedAt, runs);
204
+ if (!background.length)
205
+ continue;
206
+ sessions.push({
207
+ taskId: input.taskId, roleName: fence.roleName, agentId: fence.agentId,
208
+ driverId: fence.driverId,
209
+ nativeSessionId: fence.nativeSessionId, attemptId: null, nativeTurnId: null, runId: null,
210
+ group: "background", reason: "Retained child observations from a non-selected native Session; foreground execution is not inferred.",
211
+ layer: null, lastActivityAt: group.at(-1).observation.receivedAt,
212
+ semanticProgressAt: null, sourceUpdatedAt: group.at(-1).observation.receivedAt,
213
+ waitingReason: null, operations: [], background
214
+ });
215
+ }
216
+ const counts = {
217
+ active: 0, waiting: 0, quiet: 0, diagnostic: 0, unknown: 0, stopped: 0, idle: 0, background: 0
218
+ };
219
+ for (const session of sessions)
220
+ counts[session.group]++;
221
+ return { scope: "task-recorded-native-activity", taskId: input.taskId,
222
+ readAt: input.now.toISOString(), counts, sessions,
223
+ coverage: "Selected Sessions and retained, exactly fenced child observations only; not a live process or complete historical Session inventory." };
224
+ }
225
+ function backgroundForRuns(fence, since, groups) {
226
+ return [...groups].flatMap(([runId, events]) => {
227
+ const projection = projectRuntimeTaskEvents({ ...fence, ...(runId === undefined ? {} : { runId }) }, since, events);
228
+ return [
229
+ ...Object.entries(projection.continuations)
230
+ .filter(([, child]) => child.execution !== "quiescent" || child.identityConflict)
231
+ .map(([id, child]) => ({ id: `${runId ?? "native"}/${id}`, runId: runId ?? null,
232
+ execution: child.identityConflict ? "unknown" : child.execution })),
233
+ // Drivers may emit both forms for one child. This fold is already
234
+ // fenced to Task/Role/Agent/driver/Session/Run; continuation is richer.
235
+ ...Object.entries(projection.operations).filter(([id, operation]) => operation.kind === "subagent" && !Object.hasOwn(projection.continuations, id))
236
+ .map(([id]) => ({ id: `${runId ?? "native"}/operation:${id}`, runId: runId ?? null,
237
+ execution: "unsettled" }))
238
+ ];
239
+ });
240
+ }
@@ -1,6 +1,55 @@
1
1
  import { enqueueWork } from "../coordination/workMailboxQueue.js";
2
2
  import { createTaskEvent } from "../event/taskEvent.js";
3
+ import { claimPending } from "../coordination/workMailbox.js";
3
4
  export const LEADER_ATTENTION_REQUIRED_EVENT = "leader.attention-required";
5
+ /** Bound each Operator pass, retaining the exact unclaimed sequence suffix. */
6
+ export function claimOperatorAttention(mailbox, options) {
7
+ const pending = mailbox.pending;
8
+ if (pending === null || pending.requestCount <= 64)
9
+ return claimPending(mailbox, options);
10
+ // Operator producers append one unique source ref per signal. Mailbox merge
11
+ // preserves insertion order for refs and keys, including the supported
12
+ // default sequence keys used by Provider retry attention. Preserve those
13
+ // keys verbatim; they are valid current data, not a migration or repair.
14
+ if (pending.requestCount !== pending.dedupeKeys.length
15
+ || pending.refs.length !== pending.requestCount
16
+ || pending.toSequence - pending.fromSequence + 1 !== pending.requestCount
17
+ || pending.dedupeKeys.some((key, index) => {
18
+ const ref = pending.refs[index];
19
+ return (ref.type !== "event" && ref.type !== "input")
20
+ || key !== `operator-${ref.type}:${ref.taskId}:${ref.id}`
21
+ && key !== `sequence:${pending.fromSequence + index}`;
22
+ })) {
23
+ throw new Error("Operator attention batch cannot be safely partitioned; inspect its source refs and sequence before retrying.");
24
+ }
25
+ const prefixKeys = pending.dedupeKeys.slice(0, 64);
26
+ const suffixKeys = pending.dedupeKeys.slice(64);
27
+ const through = pending.fromSequence + 63;
28
+ const prefix = { ...pending, toSequence: through, requestCount: 64,
29
+ dedupeKeys: prefixKeys, refs: pending.refs.slice(0, 64) };
30
+ const claimed = claimPending({ ...mailbox, pending: prefix }, {
31
+ ...options, batchId: `operator:${prefix.fromSequence}-${prefix.toSequence}`
32
+ });
33
+ return { ...claimed, pending: { ...pending, fromSequence: through + 1,
34
+ requestCount: pending.requestCount - 64, dedupeKeys: suffixKeys,
35
+ refs: pending.refs.slice(64) } };
36
+ }
37
+ /**
38
+ * Fixed source admission at the durable event write, in the same transaction.
39
+ * The existing mailbox is the incremental restart cursor: periodic scheduling
40
+ * reads only outstanding refs, never re-scans or replays historical events.
41
+ * Context, idle observations and ordinary progress are deliberately absent.
42
+ */
43
+ export function enqueueDurableOperatorFact(store, event) {
44
+ const now = new Date(event.createdAt);
45
+ if (event.type === "input.requested" && typeof event.payload.requestId === "string") {
46
+ enqueueWork(store, { kind: "operator" }, "input-requested", now, [{ type: "input", taskId: event.taskId, id: event.payload.requestId }], { source: "input-request", dedupeKey: `operator-input:${event.taskId}:${event.payload.requestId}` });
47
+ }
48
+ else if (["task.completed", "task.cancelled", "task.retired", "publication.recorded",
49
+ "publication.candidate-adopted", LEADER_ATTENTION_REQUIRED_EVENT].includes(event.type)) {
50
+ enqueueOperatorEvent(store, event, event.type.startsWith("task.") ? "task-terminal" : event.type, now);
51
+ }
52
+ }
4
53
  /** One event, one supervisor signal; the Leader wake is this mailbox's projection. */
5
54
  export function enqueueSupervisorEvent(store, event, recipient, reason, now) {
6
55
  if (store.getTask(event.taskId)?.status === "archived")