@zq-silk/yui 0.15.11 → 0.15.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/ARCHITECTURE.md +8 -4
  2. package/ARCHITECTURE.zh-CN.md +5 -2
  3. package/README.md +13 -5
  4. package/dist/agentRun/agentRun.js +3 -0
  5. package/dist/cli/commandCatalog.js +38 -10
  6. package/dist/cli/interactionPolicy.js +4 -0
  7. package/dist/cli/managedDiagnostics.js +1 -1
  8. package/dist/cli.js +46 -15
  9. package/dist/commands/executionAuditCommands.js +10 -0
  10. package/dist/commands/globalRoleCommands.js +27 -2
  11. package/dist/commands/projectCommands.js +44 -15
  12. package/dist/commands/taskCommands.js +95 -33
  13. package/dist/commands/taskIntegrationCommands.js +3 -1
  14. package/dist/commands/taskOverviewCommand.js +4 -3
  15. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  16. package/dist/commands/taskPublicationCommands.js +11 -2
  17. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  18. package/dist/commands/taskRemoteDeliveryCommand.js +18 -7
  19. package/dist/context/runContextPack.js +3 -0
  20. package/dist/context/taskCatalog.js +187 -0
  21. package/dist/context/taskContext.js +17 -4
  22. package/dist/controller/controller.js +11 -2
  23. package/dist/controller/fileSchedulerStoreAdapter.js +80 -19
  24. package/dist/controller/globalInputDelivery.js +13 -0
  25. package/dist/controller/providerRetryAdmission.js +100 -0
  26. package/dist/controller/providerRetryDelivery.js +218 -0
  27. package/dist/controller/runtime.js +36 -1
  28. package/dist/integration/gitIntegrationService.js +19 -6
  29. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  30. package/dist/message/globalProviderRetry.js +15 -0
  31. package/dist/observability/executionAudit.js +19 -0
  32. package/dist/repository/gitWorkspace.js +371 -105
  33. package/dist/repository/projectMaintenanceLock.js +75 -18
  34. package/dist/repository/taskWorkspaceCoordinator.js +71 -124
  35. package/dist/repository/taskWorkspacePreparer.js +86 -24
  36. package/dist/repository/workspaceCleanupInspection.js +187 -0
  37. package/dist/runtime/agentError.js +5 -3
  38. package/dist/runtime/agentHost.js +28 -11
  39. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  40. package/dist/runtime/codexAppServerRuntime.js +34 -3
  41. package/dist/runtime/providerControl.js +5 -1
  42. package/dist/runtime/providerRetry.js +198 -0
  43. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  44. package/dist/runtime/sessionTokenMetrics.js +15 -5
  45. package/dist/runtime/structuredProviderHost.js +6 -2
  46. package/dist/runtime/taskUsageMetrics.js +275 -0
  47. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  48. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  49. package/dist/scheduler/taskExecutionProjection.js +26 -5
  50. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  51. package/dist/storage/sqliteSchema.js +31 -0
  52. package/dist/storage/sqliteStore.js +17 -0
  53. package/dist/storage/storageVersions.js +1 -1
  54. package/dist/storage/storeRpc.js +1 -0
  55. package/dist/storage/taskCatalog.js +123 -0
  56. package/dist/storage/taskStore.js +2 -0
  57. package/dist/task/archiveDiagnostics.js +1 -0
  58. package/dist/task/archivePreflight.js +124 -0
  59. package/dist/task/publicationAdoption.js +56 -0
  60. package/dist/task/publicationReference.js +10 -0
  61. package/dist/task/remoteDelivery.js +31 -16
  62. package/dist/web/assets/client/app.js +92 -17
  63. package/dist/web/assets/client/components.js +55 -13
  64. package/dist/web/assets/client/i18n.js +72 -4
  65. package/dist/web/assets/client/taskSurface.js +2 -1
  66. package/dist/web/assets/client/view.js +35 -7
  67. package/dist/web/assets/shell.js +6 -0
  68. package/dist/web/assets/styles/layout.js +7 -0
  69. package/dist/web/webServer.js +14 -3
  70. package/dist/web/webSnapshot.js +12 -3
  71. package/dist/workspace/cleanupInspection.js +63 -0
  72. package/dist/workspace/workItemChangeSetManager.js +110 -50
  73. package/docs/agent-result-consumption.md +4 -0
  74. package/docs/agent-result-consumption.zh-CN.md +3 -0
  75. package/docs/agent-runtime-drivers.md +7 -0
  76. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  77. package/docs/architecture/README.md +2 -0
  78. package/docs/architecture/README.zh-CN.md +3 -1
  79. package/docs/architecture/capabilities-and-resources.md +30 -5
  80. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  81. package/docs/managed-turn-and-session-runtime.md +47 -0
  82. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  83. package/docs/observability/README.md +62 -0
  84. package/docs/observability/README.zh-CN.md +47 -0
  85. package/docs/project-refresh.md +77 -0
  86. package/docs/project-refresh.zh-CN.md +59 -0
  87. package/docs/provider-retry.md +70 -0
  88. package/docs/task-delivery.md +133 -13
  89. package/docs/task-delivery.zh-CN.md +99 -10
  90. package/docs/task-discovery.md +102 -0
  91. package/docs/task-discovery.zh-CN.md +86 -0
  92. package/docs/testing/verification-levels.md +16 -0
  93. package/docs/testing/verification-levels.zh-CN.md +13 -1
  94. package/i18n/README.zh-CN.md +13 -7
  95. package/package.json +1 -1
  96. package/skills/yui-leader/references/execution.md +3 -2
  97. package/skills/yui-operator/SKILL.md +13 -2
  98. package/skills/yui-reviewer/SKILL.md +4 -0
  99. package/skills/yui-runtime/SKILL.md +17 -2
  100. package/skills/yui-runtime/references/publication.md +26 -4
  101. package/skills/yui-runtime/references/recovery.md +24 -0
@@ -2,6 +2,8 @@ import { sendAgentHostRunControl, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime
2
2
  import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
3
3
  import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
4
4
  import { markGlobalRoleMessageNotDelivered } from "../message/message.js";
5
+ import { providerRetryPending } from "../runtime/providerRetry.js";
6
+ import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
5
7
  /** Messages are the only queue. Host's atomic begin arbitrates native writers. */
6
8
  export async function deliverGlobalInputs(home, store, ensure, onError) {
7
9
  for (const role of store.listGlobalRoles()) {
@@ -11,6 +13,15 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
11
13
  if (pending.length === 0 || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
12
14
  continue;
13
15
  let sessions = store.getGlobalRoleSessionSet(role.name);
16
+ if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
17
+ store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
18
+ pending = store.listGlobalRoleMessages(role.name).filter(message => message.delivery === undefined && message.notDelivered === undefined
19
+ && (message.inputControl?.action === "queue" || message.interruptThen !== undefined));
20
+ if (pending.length === 0)
21
+ continue;
22
+ }
23
+ if (providerRetryPending(sessions?.providerBinding))
24
+ continue;
14
25
  const current = sessions?.providerBinding?.run;
15
26
  const obsolete = pending.filter(message => {
16
27
  const claim = message.interruptThen;
@@ -99,6 +110,8 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
99
110
  if (latest === undefined || latest.delivery !== undefined || latest.notDelivered !== undefined)
100
111
  return;
101
112
  const turn = tx.getGlobalRoleSessionSet(role.name)?.providerBinding?.run;
113
+ if (providerRetryPending(tx.getGlobalRoleSessionSet(role.name)?.providerBinding))
114
+ return;
102
115
  // A later canonical acceptance wins over transport diagnostics.
103
116
  if (turn?.attemptId === attemptId && ["accepted", "completed", "failed", "cancelled"].includes(turn.status))
104
117
  return;
@@ -0,0 +1,100 @@
1
+ import { admitProviderRetry, cancelProviderRetry, providerRetryAttemptId, providerRetryPending } from "../runtime/providerRetry.js";
2
+ import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
3
+ import { validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ export function ownedProviderInput(input) {
6
+ if (input.runId !== undefined && input.taskId !== undefined) {
7
+ return { kind: "run", taskId: input.taskId, runId: input.runId };
8
+ }
9
+ const wake = /^notification:([^/]+)\/([^/]+)\//u.exec(input.attemptId);
10
+ if (wake !== null && wake[1] === input.taskId) {
11
+ return { kind: "wake", taskId: wake[1], wakeId: wake[2] };
12
+ }
13
+ const message = /^global-input:([^/]+)\/([^/]+)$/u.exec(input.attemptId);
14
+ if (message !== null && message[1] === input.roleName && input.taskId === undefined) {
15
+ return { kind: "message", roleName: input.roleName, messageId: message[2] };
16
+ }
17
+ return input.boundedText === undefined ? undefined : { kind: "text", text: input.boundedText };
18
+ }
19
+ export function retryIntentBlocker(store, input, retry) {
20
+ if (input.taskId === undefined) {
21
+ const role = store.getGlobalRole(input.roleName);
22
+ const sessions = store.getGlobalRoleSessionSet(input.roleName);
23
+ if (role === null || sessions?.activeAgentId !== role.activeAgentId)
24
+ return "role-changed";
25
+ if (sessions.sessions[sessions.activeAgentId]?.effective.sourceDesiredRevision !== role.launchRevision)
26
+ return "role-configuration-changed";
27
+ if (store.listGlobalRoleMessages(input.roleName).some(message => Date.parse(message.createdAt) > Date.parse(retry.intentAt)
28
+ && message.delivery === undefined && message.notDelivered === undefined))
29
+ return "new-user-input";
30
+ return undefined;
31
+ }
32
+ const task = store.getTask(input.taskId);
33
+ if (task === null || !["active", "draft"].includes(task.status)
34
+ || task.executionGate.state !== "enabled")
35
+ return "task-execution-stopped";
36
+ const role = store.getRole(input.taskId, input.roleName);
37
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
38
+ if (role === null || sessions?.activeAgentId !== role.activeAgentId)
39
+ return "role-changed";
40
+ if (sessions.sessions[sessions.activeAgentId]?.effective.sourceDesiredRevision !== role.launchRevision)
41
+ return "role-configuration-changed";
42
+ if (store.listMessages(input.taskId).some(message => (message.kind === "operator" || message.kind === "user")
43
+ && Date.parse(message.createdAt) > Date.parse(retry.intentAt)))
44
+ return "new-user-input";
45
+ if (blockingProviderContinuations(store.listEvents(input.taskId)).some(c => c.roleName === input.roleName && c.identity.conversationId === retry.nativeSessionId)) {
46
+ return "background-work-unsettled";
47
+ }
48
+ return undefined;
49
+ }
50
+ /** Shared by automatic recovery and every ordinary Host submit. The native
51
+ * writer CAS remains the only grant; a scheduled successor is not a grant. */
52
+ export function admitOwnedProviderInput(store, binding, input) {
53
+ const retry = binding.retry;
54
+ const sameReviewSuccessor = retry?.successorReviewRoundId !== undefined && input.taskId !== undefined && input.runId !== undefined
55
+ && store.getRun(input.taskId, input.runId)?.reviewRoundId === retry.successorReviewRoundId;
56
+ if (retry !== undefined && !providerRetryPending(binding)
57
+ && (input.attemptId.startsWith("provider-retry:") || input.runId !== undefined && input.runId === retry.successorRunId
58
+ || sameReviewSuccessor)) {
59
+ throw new Error(`Provider retry is ${retry.status}; this scheduled successor cannot regain admission.`);
60
+ }
61
+ if (!providerRetryPending(binding) || retry === undefined)
62
+ return binding;
63
+ const automatic = input.attemptId === providerRetryAttemptId(retry)
64
+ || input.runId !== undefined && input.runId === retry.successorRunId
65
+ || input.runId !== undefined && input.taskId !== undefined && retry.successorReviewRoundId !== undefined
66
+ && store.getRun(input.taskId, input.runId)?.reviewRoundId === retry.successorReviewRoundId
67
+ || retry.status === "in-flight" && input.attemptId === retry.currentAttemptId;
68
+ if (!automatic)
69
+ return cancelProviderRetry(binding, "superseded-by-explicit-input", input.now.getTime());
70
+ const blocker = retryIntentBlocker(store, input, retry);
71
+ if (blocker !== undefined)
72
+ throw new Error(`Provider retry admission stopped: ${blocker}.`);
73
+ if (input.retrySupported !== true)
74
+ throw new Error("This Host cannot prove safe same-Session recovery; use explicit recovery.");
75
+ if (binding.goal?.status === "active")
76
+ throw new Error("Provider still owns an autonomous Goal; inspect its exact continuation.");
77
+ if (input.taskId !== undefined && input.runId !== undefined) {
78
+ const run = store.getRun(input.taskId, input.runId);
79
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
80
+ if (run === null || run.status !== "active"
81
+ || !isDeepStrictEqual(run.effective, sessions?.sessions[input.agentId]?.effective)) {
82
+ throw new Error("Retry execution no longer matches its fixed native Session.");
83
+ }
84
+ if (run.workspace !== undefined && !isDeepStrictEqual(store.getManagedWorkspace(run.workspace.owner), run.workspace)) {
85
+ throw new Error("Retry workspace authority changed.");
86
+ }
87
+ if (run.workItemId !== undefined) {
88
+ const item = store.getWorkItem(input.taskId, run.workItemId);
89
+ if (item?.status !== "open" || run.purpose !== "review" && run.executionLaneId === undefined
90
+ && item.assignee !== run.roleName)
91
+ throw new Error("Retry WorkItem responsibility changed.");
92
+ }
93
+ if (validateExactRunReviewRound(store, run).disposition !== "applied") {
94
+ throw new Error("Retry Review no longer matches its exact Round and frozen candidate.");
95
+ }
96
+ }
97
+ const admitted = admitProviderRetry(binding, { attemptId: input.attemptId, at: input.now.getTime() });
98
+ return input.runId === undefined ? admitted
99
+ : { ...admitted, retry: { ...admitted.retry, successorRunId: input.runId } };
100
+ }
@@ -0,0 +1,218 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { runTaskCommand } from "../commands/taskCommands.js";
3
+ import { retryIntentBlocker } from "./providerRetryAdmission.js";
4
+ import { cancelProviderRetry, providerRetryAttemptId, providerRetryPending } from "../runtime/providerRetry.js";
5
+ import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
6
+ import { runtimeLifecycleTarget, hasRuntimeCleanupObligation } from "../runtime/lifecycleReservation.js";
7
+ import { sendAgentHostRunControl, inspectAgentHostSocket, agentHostControlSocketPath, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
8
+ import { redactAgentErrorText } from "../runtime/agentError.js";
9
+ import { createTaskEvent } from "../event/taskEvent.js";
10
+ import { enqueueWork } from "../coordination/workMailboxQueue.js";
11
+ import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
12
+ /** Hooks in the existing Controller pass and nearest-deadline timer. SQLite's
13
+ * current Provider input is the only pending intent; nothing is queued here. */
14
+ export function createProviderRetryHooks(home, store, ports = {}) {
15
+ const anchors = new Map();
16
+ const inspectedUnknown = new Set();
17
+ const inspect = ports.inspect ?? ((target) => inspectAgentHostSocket(agentHostControlSocketPath(target), 250));
18
+ const now = (retry) => {
19
+ const wall = (ports.now ?? (() => new Date()))().getTime();
20
+ const monotonic = (ports.monotonicNow ?? (() => performance.now()))();
21
+ let anchor = anchors.get(retry.chainId);
22
+ if (anchor === undefined) {
23
+ anchor = { wall: Math.max(wall, Date.parse(retry.updatedAt)), monotonic };
24
+ anchors.set(retry.chainId, anchor);
25
+ }
26
+ return Math.max(wall, anchor.wall + Math.max(0, monotonic - anchor.monotonic));
27
+ };
28
+ const load = (owner) => owner.scope === "task"
29
+ ? store.getTaskRoleSessionSet(owner.taskId, owner.roleName) : store.getGlobalRoleSessionSet(owner.roleName);
30
+ const save = (set) => {
31
+ if (set.owner.scope === "task")
32
+ store.saveTaskRoleSessionSet(set);
33
+ else
34
+ store.saveGlobalRoleSessionSet(set);
35
+ };
36
+ const stop = (set, reason, at, exhausted = false) => {
37
+ store.transaction(() => {
38
+ const current = load(set.owner);
39
+ if (current === null || current.providerBinding?.retry?.chainId !== set.providerBinding?.retry?.chainId)
40
+ return;
41
+ const binding = current.providerBinding;
42
+ if (!providerRetryPending(binding))
43
+ return;
44
+ const cancelled = cancelProviderRetry(binding, reason, at);
45
+ save({ ...current, providerBinding: exhausted
46
+ ? { ...cancelled, retry: { ...cancelled.retry, status: "exhausted" } } : cancelled });
47
+ const owner = set.owner;
48
+ if (owner.scope === "global")
49
+ settleGlobalRetryInput(store, owner.roleName, load(owner)?.providerBinding, new Date(at));
50
+ if (owner.scope === "task") {
51
+ const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "provider.retry-stopped", {
52
+ roleName: owner.roleName, chainId: binding.retry.chainId, reason
53
+ }, new Date(at));
54
+ store.saveEvent(owner.taskId, event);
55
+ enqueueWork(store, owner.roleName === "leader" ? { kind: "operator" }
56
+ : { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
57
+ }
58
+ });
59
+ };
60
+ const key = (set) => set.owner.scope === "task"
61
+ ? `role:${encodeURIComponent(set.owner.taskId)}/${encodeURIComponent(set.owner.roleName)}`
62
+ : `global-role:${encodeURIComponent(set.owner.roleName)}`;
63
+ return {
64
+ deadlines() {
65
+ return store.listProviderRetrySessions().flatMap(set => {
66
+ const retry = set.providerBinding.retry;
67
+ if (retry.status !== "waiting")
68
+ return [];
69
+ // Busy or already-queued work awaits the next ordinary pass without a
70
+ // zero-delay timer loop. Waiting never consumes an execution attempt.
71
+ const wall = (ports.now ?? (() => new Date()))().getTime();
72
+ const effective = now(retry);
73
+ return [{ key: key(set), at: wall + Math.max(250, (retry.successorAutomatic === false ? Date.parse(retry.nextEligibleAt)
74
+ : Math.min(Date.parse(retry.nextEligibleAt), Date.parse(retry.deadline))) - effective) }];
75
+ });
76
+ },
77
+ async reconcile() {
78
+ const current = store.listProviderRetrySessions();
79
+ const activeChains = new Set(current.map(set => set.providerBinding.retry.chainId));
80
+ for (const id of anchors.keys())
81
+ if (!activeChains.has(id))
82
+ anchors.delete(id);
83
+ const pendingAttempts = new Set(current.map(set => set.providerBinding.retry.currentAttemptId));
84
+ for (const id of inspectedUnknown)
85
+ if (!pendingAttempts.has(id))
86
+ inspectedUnknown.delete(id);
87
+ for (const set of current) {
88
+ const owner = set.owner;
89
+ const binding = set.providerBinding;
90
+ const retry = binding.retry;
91
+ const at = now(retry);
92
+ const scoped = { ...(owner.scope === "task" ? { taskId: owner.taskId } : {}), roleName: owner.roleName };
93
+ const session = set.sessions[set.activeAgentId];
94
+ const blocker = retryIntentBlocker(store, scoped, retry);
95
+ if (blocker !== undefined && blocker !== "background-work-unsettled") {
96
+ stop(set, blocker, at);
97
+ continue;
98
+ }
99
+ if (binding.retryDisabled && retry.successorAutomatic !== false || binding.authority.owner !== "controller"
100
+ || binding.authority.epoch !== retry.authorityEpoch
101
+ || currentProviderConversation(binding).conversationId !== retry.nativeSessionId
102
+ || session?.status !== "active" || session.nativeSessionId !== retry.nativeSessionId
103
+ || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget(owner)))) {
104
+ stop(set, "session-or-authority-changed", at);
105
+ continue;
106
+ }
107
+ // Never replay an admitted attempt. Existing exact runtime queries and
108
+ // terminal ingress resolve it, even across Controller restart.
109
+ if (retry.status === "in-flight") {
110
+ if (binding.run?.status === "delivery-unknown" && retry.currentAttemptId !== undefined
111
+ && !inspectedUnknown.has(retry.currentAttemptId)) {
112
+ inspectedUnknown.add(retry.currentAttemptId);
113
+ let detail;
114
+ try {
115
+ const host = await inspect({ home, ...scoped, scope: owner.scope });
116
+ detail = host.nativeSessionId === retry.nativeSessionId && host.attemptId === retry.currentAttemptId
117
+ ? `Exact Host query: ${host.state}; awaiting original canonical Provider evidence.`
118
+ : "Host query did not prove the original Session/input identity; no replay.";
119
+ }
120
+ catch (error) {
121
+ detail = `Original Host query unavailable; no replay: ${redactAgentErrorText(error instanceof Error ? error.message : String(error))}`;
122
+ }
123
+ store.transaction(() => {
124
+ const latest = load(owner);
125
+ if (latest === null || latest.providerBinding == null || latest.providerBinding.run === null
126
+ || latest.providerBinding.run.attemptId !== retry.currentAttemptId
127
+ || latest.providerBinding.run.status !== "delivery-unknown")
128
+ return;
129
+ save({ ...latest, providerBinding: { ...latest.providerBinding,
130
+ retry: { ...latest.providerBinding.retry, reason: detail } } });
131
+ if (owner.scope === "task") {
132
+ const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "provider.retry-unconfirmed", {
133
+ roleName: owner.roleName, attemptId: retry.currentAttemptId, nativeSessionId: retry.nativeSessionId, detail
134
+ }, new Date(at));
135
+ store.saveEvent(owner.taskId, event);
136
+ enqueueWork(store, owner.roleName === "leader" ? { kind: "operator" }
137
+ : { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
138
+ }
139
+ });
140
+ }
141
+ continue;
142
+ }
143
+ if (retry.successorAutomatic !== false && at >= Date.parse(retry.deadline)) {
144
+ stop(set, "deadline-exhausted", at, true);
145
+ continue;
146
+ }
147
+ if (at < Date.parse(retry.nextEligibleAt) || blocker !== undefined || binding.goal?.status === "active")
148
+ continue;
149
+ if (binding.run?.attemptId !== retry.failedAttemptId) {
150
+ stop(set, "superseded-input", at);
151
+ continue;
152
+ }
153
+ try {
154
+ if (owner.scope === "task" && retry.previousRunId !== undefined) {
155
+ if (retry.successorRunId !== undefined || retry.successorReviewRoundId !== undefined)
156
+ continue;
157
+ const previous = store.getRun(owner.taskId, retry.previousRunId);
158
+ if (previous?.status === "active")
159
+ continue; // ordinary rejected-delivery terminal fold
160
+ if (previous?.status !== "failed") {
161
+ stop(set, "run-no-longer-failed", at);
162
+ continue;
163
+ }
164
+ const actualTaskReviewCandidate = await ports.snapshotTaskCandidate?.(previous);
165
+ store.transaction(() => {
166
+ const latest = load(owner)?.providerBinding?.retry;
167
+ if (latest?.chainId !== retry.chainId || latest.failedAttemptId !== retry.failedAttemptId
168
+ || latest.status !== "waiting")
169
+ return;
170
+ const reason = retryIntentBlocker(store, scoped, latest);
171
+ if (reason !== undefined)
172
+ throw new Error(reason);
173
+ runTaskCommand(["run", "retry", `${owner.taskId}/${previous.id}`], store, {
174
+ now: () => new Date(at), environment: {}, providerRetryChainId: retry.chainId,
175
+ ...(actualTaskReviewCandidate === undefined ? {} : { actualTaskReviewCandidate })
176
+ });
177
+ });
178
+ }
179
+ else {
180
+ const original = retry.input.kind === "text" ? retry.input.text
181
+ : retry.input.kind === "wake"
182
+ ? `Yui Task notification: task=${retry.input.taskId} wake=${retry.input.wakeId}.\nRead current Task context and that fixed wake window, including original Messages in full. This is not an execution assignment.`
183
+ : retry.input.kind === "message"
184
+ ? `Yui Global Message: role=${retry.input.roleName} message=${retry.input.messageId}.\nRead your Session Context and referenced original Message in full.`
185
+ : undefined;
186
+ if (original === undefined) {
187
+ stop(set, "original-input-unavailable", at);
188
+ continue;
189
+ }
190
+ const result = await (ports.submit ?? sendAgentHostRunControl)({
191
+ home, ...scoped, scope: owner.scope,
192
+ control: {
193
+ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "submit-turn", nativeSessionId: retry.nativeSessionId,
194
+ authority: { epoch: binding.authority.epoch, owner: "controller", holderId: binding.authority.holderId },
195
+ run: { attemptId: providerRetryAttemptId(retry), boundedText: original }
196
+ }
197
+ });
198
+ // Durable settlement belongs to Host/Controller ingress. In
199
+ // particular a lost response is never a reason to replay.
200
+ if (result.outcome === "rejected" && result.failure?.registrationDisposition === "not-committed"
201
+ && result.failure.errorName !== "ProviderTurnBusyError") {
202
+ stop(set, result.failure.detail, at);
203
+ }
204
+ }
205
+ }
206
+ catch (error) {
207
+ // A send failure may have applied. A durable submitting/unknown
208
+ // identity fences it; do not manufacture a successor to uncertainty.
209
+ const latest = load(owner);
210
+ if (latest?.providerBinding?.run?.attemptId === retry.failedAttemptId) {
211
+ stop(set, error instanceof Error ? error.message : String(error), at);
212
+ }
213
+ ports.onError?.(error);
214
+ }
215
+ }
216
+ }
217
+ };
218
+ }
@@ -46,6 +46,8 @@ import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } fro
46
46
  import { replayRuntimeProcessExitOutbox } from "../runtime/processExitOutbox.js";
47
47
  import { appendGlobalProcessExitObservation } from "../runtime/globalProcessExitStore.js";
48
48
  import { deliverGlobalInputs } from "./globalInputDelivery.js";
49
+ import { providerRetryPrompt } from "../runtime/providerRetry.js";
50
+ import { createProviderRetryHooks } from "./providerRetryDelivery.js";
49
51
  import { createGlobalRoleMessage } from "../message/message.js";
50
52
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
51
53
  import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
@@ -470,6 +472,26 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
470
472
  now: options.now,
471
473
  onError: options.onError,
472
474
  lifecycleHost,
475
+ providerRetry: createProviderRetryHooks(home, store, {
476
+ now: options.now,
477
+ onError: options.onError,
478
+ snapshotTaskCandidate: async (run) => {
479
+ if (run.purpose !== "review" || run.reviewRoundId === undefined)
480
+ return undefined;
481
+ const round = store.getReviewRound(run.taskId, run.reviewRoundId);
482
+ if (round?.scope !== "task")
483
+ return undefined;
484
+ const task = store.getTask(run.taskId);
485
+ const workspace = store.getTaskWorkspace(run.taskId);
486
+ if (task === null || workspace === null || !(workspacePreparer instanceof FileTaskWorkspacePreparer)) {
487
+ throw new Error("Task-final retry requires an observable authoritative Task workspace.");
488
+ }
489
+ const snapshot = await workspacePreparer.snapshotDirectTaskMain(workspace, task.projectBindings.map(p => p.projectId));
490
+ return { schemaVersion: 1, projects: snapshot.projects.map(p => ({
491
+ projectId: p.projectId, commit: p.headCommit
492
+ })) };
493
+ }
494
+ }),
473
495
  jobSupervisor,
474
496
  globalInputDelivery: () => deliverGlobalInputs(home, store, async (roleName) => {
475
497
  await lifecycleDispatcher("runtime.ensure-role-session", { scope: "global", roleName });
@@ -617,6 +639,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
617
639
  authorityEpoch: value.authorityEpoch,
618
640
  authorityOwner: value.authorityOwner,
619
641
  holderId: value.holderId,
642
+ boundedText: value.boundedText,
643
+ retrySupported: value.retrySupported,
620
644
  now: value.now
621
645
  });
622
646
  }
@@ -628,7 +652,15 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
628
652
  }
629
653
  throw error;
630
654
  }
631
- return { recorded: true };
655
+ const binding = (value.taskId === undefined
656
+ ? store.getGlobalRoleSessionSet(value.roleName)
657
+ : store.getTaskRoleSessionSet(value.taskId, value.roleName))?.providerBinding;
658
+ const retry = binding?.retry;
659
+ return { recorded: true, ...(retry?.currentAttemptId === value.attemptId && value.boundedText !== undefined
660
+ ? { retryInput: {
661
+ text: providerRetryPrompt(retry, value.boundedText),
662
+ ...(retry.failedNativeTurnId === undefined ? {} : { expectedFailedNativeTurnId: retry.failedNativeTurnId })
663
+ } } : {}) };
632
664
  }
633
665
  if (method === "runtime.provider-turn-submission-resolve") {
634
666
  const value = providerTurnControlParams(params);
@@ -648,6 +680,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
648
680
  status,
649
681
  reason,
650
682
  raw,
683
+ noProviderWrite: params.providerWrite === "not-issued",
651
684
  now: value.now
652
685
  });
653
686
  return { recorded: true };
@@ -1207,6 +1240,8 @@ function providerTurnControlParams(params) {
1207
1240
  authorityEpoch: authorityEpoch,
1208
1241
  authorityOwner,
1209
1242
  holderId: requiredParam(value.holderId),
1243
+ ...(value.boundedText === undefined ? {} : { boundedText: requiredParam(value.boundedText) }),
1244
+ ...(value.retrySupport === undefined ? {} : { retrySupported: value.retrySupport === "codex-failed-turn-v1" }),
1210
1245
  now: new Date(observedAt)
1211
1246
  };
1212
1247
  }
@@ -76,7 +76,7 @@ export class GitIntegrationService {
76
76
  #resourceRegistrar() {
77
77
  return this.#resourceRegistrarValue ??= new ResourceRegistrar(this.home, this.now);
78
78
  }
79
- async integrate(taskId, integrationId) {
79
+ async integrate(taskId, integrationId, signal) {
80
80
  let initial = requireIntegration(this.store, taskId, integrationId);
81
81
  if (!["running", "conflicted", "blocked", "validating"].includes(initial.status)) {
82
82
  throw new Error(`Integration cannot continue from ${initial.status}.`);
@@ -86,7 +86,7 @@ export class GitIntegrationService {
86
86
  // CAS. A concurrent `project migrate` must not switch the catalog path or
87
87
  // remove the old checkout mid-Integration, so the per-Project maintenance
88
88
  // fence is held across every Git effect and released only on exit.
89
- const release = acquireProjectMaintenanceLocks(this.home, [initial.projectId]);
89
+ const release = await acquireProjectMaintenanceLocks(this.home, [initial.projectId], { signal });
90
90
  try {
91
91
  // A concurrent abort may have settled the attempt while this caller was
92
92
  // acquiring the same Project fence. Never execute a pre-fence snapshot.
@@ -260,9 +260,9 @@ export class GitIntegrationService {
260
260
  /** Abandon under the same fence as Git/CAS, not just a status whitelist.
261
261
  * A delivered validating candidate is recorded as committed; a provably
262
262
  * unadvanced one may be abandoned without requiring obsolete check settings. */
263
- async abort(taskId, integrationId, reason, authorize) {
263
+ async abort(taskId, integrationId, reason, authorize, signal) {
264
264
  const initial = requireIntegration(this.store, taskId, integrationId);
265
- const release = acquireProjectMaintenanceLocks(this.home, [initial.projectId]);
265
+ const release = await acquireProjectMaintenanceLocks(this.home, [initial.projectId], { signal });
266
266
  try {
267
267
  const attempt = requireIntegration(this.store, taskId, integrationId);
268
268
  authorize(this.store, taskId);
@@ -338,12 +338,25 @@ export class GitIntegrationService {
338
338
  release();
339
339
  }
340
340
  }
341
- async cleanup(integration) {
341
+ async cleanup(integration, options = {}) {
342
342
  // The whole cleanup is one Git transaction against the Project's
343
343
  // repository, mirroring integrate(): a concurrent `project migrate` must
344
344
  // not switch the catalog path or remove the old checkout mid-cleanup.
345
- const release = acquireProjectMaintenanceLocks(this.home, [integration.projectId]);
345
+ const release = await acquireProjectMaintenanceLocks(this.home, [integration.projectId], { signal: options.signal });
346
346
  try {
347
+ options.authorize?.();
348
+ const current = requireIntegration(this.store, integration.taskId, integration.id);
349
+ if (current.projectId !== integration.projectId
350
+ || !["committed", "superseded", "failed"].includes(current.status)) {
351
+ throw new Error("Integration changed before cleanup; read its current record.");
352
+ }
353
+ integration = current;
354
+ const activeJob = this.store.listDurableJobs(integration.taskId).find(job => job.owner.kind === "integration-attempt" && job.owner.integrationAttemptId === integration.id
355
+ && (job.status === "queued" || job.status === "running"
356
+ || (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined)));
357
+ if (activeJob !== undefined) {
358
+ throw new Error(`Integration ${integration.id} has an active DurableJob: ${activeJob.id}/${activeJob.status}.`);
359
+ }
347
360
  const task = this.store.getTask(integration.taskId);
348
361
  if (task === null || !task.projectBindings.some(({ projectId }) => projectId === integration.projectId)) {
349
362
  throw new Error(`Integration Task Project is unavailable: ${integration.id}.`);
@@ -1,5 +1,6 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { createTaskMessage } from "../message/message.js";
3
+ import { providerRetryPending } from "../runtime/providerRetry.js";
3
4
  import { captureRoleRunDispatch, enqueueWork, settleRoleRunDispatch } from "../coordination/workMailboxQueue.js";
4
5
  import { finishReviewRound, updateReviewExecutionGroup } from "../review/reviewRound.js";
5
6
  import { completeRun, failRun, runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
@@ -394,7 +395,9 @@ export function terminalizeExactTaskRun(store, input, now) {
394
395
  const message = createTaskMessage(store.nextMessageId(terminal.taskId), terminal.taskId, `Execution ${terminal.id} ${terminal.status}.`, "role-result", { type: "role", roleName: terminal.roleName }, now, { resultRef: { type: "agent-run-result", runId: terminal.id }, ...(terminal.workItemId === undefined
395
396
  ? {} : { workItemId: terminal.workItemId }) });
396
397
  store.saveMessage(terminal.taskId, message);
397
- enqueueWork(store, { kind: "role", taskId: terminal.taskId, roleName: "leader" }, "role-result", now, [{ type: "message", taskId: terminal.taskId, id: message.id }], { source: "task-event", dedupeKey: `result:${terminal.taskId}/${terminal.id}` });
398
+ if (!providerRetryPending(store.getTaskRoleSessionSet(terminal.taskId, terminal.roleName)?.providerBinding)) {
399
+ enqueueWork(store, { kind: "role", taskId: terminal.taskId, roleName: "leader" }, "role-result", now, [{ type: "message", taskId: terminal.taskId, id: message.id }], { source: "task-event", dedupeKey: `result:${terminal.taskId}/${terminal.id}` });
400
+ }
398
401
  }
399
402
  const dispatchIdentity = {
400
403
  taskId: terminal.taskId,
@@ -0,0 +1,15 @@
1
+ import { markGlobalRoleMessageNotDelivered } from "./message.js";
2
+ /** Settle the original queue entry when recovery ends with exact nondelivery.
3
+ * Cancellation of a submitted/unknown request is not negative Provider proof. */
4
+ export function settleGlobalRetryInput(store, roleName, binding, now) {
5
+ const retry = binding?.retry;
6
+ if (retry === undefined || retry.input.kind !== "message" || retry.input.roleName !== roleName
7
+ || !["cancelled", "exhausted", "needs-attention"].includes(retry.status)
8
+ || binding?.run == null || ["submitting", "accepted", "delivery-unknown"].includes(binding.run.status))
9
+ return;
10
+ const messageId = retry.input.messageId;
11
+ const message = store.listGlobalRoleMessages(roleName).find(m => m.id === messageId);
12
+ if (message === undefined || message.delivery !== undefined || message.notDelivered !== undefined)
13
+ return;
14
+ store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(message, `provider-retry-${retry.status}: ${retry.reason ?? retry.failureRef}`, now));
15
+ }
@@ -15,6 +15,7 @@ import { join } from "node:path";
15
15
  import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
16
16
  import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
17
17
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
18
+ import { projectTaskUsageMetrics } from "../runtime/taskUsageMetrics.js";
18
19
  import { classifyRunFailure, classifyIntegrationAttempt, classifyReviewRound, countFaultClasses } from "./faultClassification.js";
19
20
  import { RUNTIME_LAUNCH_KINDS, RUNTIME_LAUNCH_PHASES } from "../runtime/launchDiagnostics.js";
20
21
  import { UNSUPPORTED } from "./runtimeIdentity.js";
@@ -192,6 +193,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
192
193
  agentErrors: section,
193
194
  workItems: section,
194
195
  orchestration: section,
196
+ usage: section,
195
197
  storage: section,
196
198
  runtimeProtocol: section,
197
199
  topLongRunning: section
@@ -658,6 +660,22 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
658
660
  return failed(error);
659
661
  }
660
662
  })();
663
+ const usage = (() => {
664
+ try {
665
+ return ok(taskIds.map((taskId) => {
666
+ const task = store.getTask(taskId);
667
+ if (task === null)
668
+ throw new Error(`Task not found: ${taskId}.`);
669
+ return projectTaskUsageMetrics({
670
+ task, events: store.listEvents(taskId), runs: store.listRuns(taskId),
671
+ now: new Date(generatedAt)
672
+ });
673
+ }));
674
+ }
675
+ catch (error) {
676
+ return failed(error);
677
+ }
678
+ })();
661
679
  const storage = (() => {
662
680
  try {
663
681
  let stateJsonBytes = UNSUPPORTED;
@@ -796,6 +814,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
796
814
  agentErrors,
797
815
  workItems,
798
816
  orchestration,
817
+ usage,
799
818
  storage,
800
819
  runtimeProtocol,
801
820
  topLongRunning