@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.
- package/ARCHITECTURE.md +8 -4
- package/ARCHITECTURE.zh-CN.md +5 -2
- package/README.md +13 -5
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/cli/commandCatalog.js +38 -10
- package/dist/cli/interactionPolicy.js +4 -0
- package/dist/cli/managedDiagnostics.js +1 -1
- package/dist/cli.js +46 -15
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +27 -2
- package/dist/commands/projectCommands.js +44 -15
- package/dist/commands/taskCommands.js +95 -33
- package/dist/commands/taskIntegrationCommands.js +3 -1
- package/dist/commands/taskOverviewCommand.js +4 -3
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +11 -2
- package/dist/commands/taskPublicationVerifyCommand.js +23 -39
- package/dist/commands/taskRemoteDeliveryCommand.js +18 -7
- package/dist/context/runContextPack.js +3 -0
- package/dist/context/taskCatalog.js +187 -0
- package/dist/context/taskContext.js +17 -4
- package/dist/controller/controller.js +11 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +80 -19
- package/dist/controller/globalInputDelivery.js +13 -0
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/runtime.js +36 -1
- package/dist/integration/gitIntegrationService.js +19 -6
- package/dist/lifecycle/exactRunTerminalization.js +4 -1
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/repository/gitWorkspace.js +371 -105
- package/dist/repository/projectMaintenanceLock.js +75 -18
- package/dist/repository/taskWorkspaceCoordinator.js +71 -124
- package/dist/repository/taskWorkspacePreparer.js +86 -24
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentHost.js +28 -11
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +6 -2
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/scheduler/activeRoleRunDelivery.js +12 -0
- package/dist/scheduler/leaderWakeupProcessor.js +5 -0
- package/dist/scheduler/taskExecutionProjection.js +26 -5
- package/dist/scheduler/taskObservabilityProjection.js +6 -44
- package/dist/storage/sqliteSchema.js +31 -0
- package/dist/storage/sqliteStore.js +17 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +2 -0
- package/dist/task/archiveDiagnostics.js +1 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/web/assets/client/app.js +92 -17
- package/dist/web/assets/client/components.js +55 -13
- package/dist/web/assets/client/i18n.js +72 -4
- package/dist/web/assets/client/taskSurface.js +2 -1
- package/dist/web/assets/client/view.js +35 -7
- package/dist/web/assets/shell.js +6 -0
- package/dist/web/assets/styles/layout.js +7 -0
- package/dist/web/webServer.js +14 -3
- package/dist/web/webSnapshot.js +12 -3
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +110 -50
- package/docs/agent-result-consumption.md +4 -0
- package/docs/agent-result-consumption.zh-CN.md +3 -0
- package/docs/agent-runtime-drivers.md +7 -0
- package/docs/agent-runtime-drivers.zh-CN.md +5 -0
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/README.zh-CN.md +3 -1
- package/docs/architecture/capabilities-and-resources.md +30 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
- package/docs/managed-turn-and-session-runtime.md +47 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/project-refresh.md +77 -0
- package/docs/project-refresh.zh-CN.md +59 -0
- package/docs/provider-retry.md +70 -0
- package/docs/task-delivery.md +133 -13
- package/docs/task-delivery.zh-CN.md +99 -10
- package/docs/task-discovery.md +102 -0
- package/docs/task-discovery.zh-CN.md +86 -0
- package/docs/testing/verification-levels.md +16 -0
- package/docs/testing/verification-levels.zh-CN.md +13 -1
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +3 -2
- package/skills/yui-operator/SKILL.md +13 -2
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +17 -2
- package/skills/yui-runtime/references/publication.md +26 -4
- package/skills/yui-runtime/references/recovery.md +24 -0
|
@@ -19,11 +19,13 @@ import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
|
19
19
|
import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, workItemExecutionGroupById, recordWorkItemWorkspaceDisposition } from "../workItem/workItem.js";
|
|
20
20
|
import { createManagedWorkspace, isTaskOwnedWorkspace, managedWorkspaceKey, managedWorktreeName, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
21
21
|
import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
|
|
22
|
-
import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
|
|
22
|
+
import { acquireProjectMaintenanceLocks, ProjectMaintenanceLockedError, ProjectMaintenanceLockCancelledError } from "./projectMaintenanceLock.js";
|
|
23
23
|
import { generateTaskWorkspaceIdentity, taskWorkspaceRefSegment, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
|
|
24
24
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
25
25
|
import { captureTaskBaseProvenance, recordTaskBaseProvenanceEvents } from "./taskBaseFreshness.js";
|
|
26
26
|
import { managedTaskRoot } from "../storage/homeLayout.js";
|
|
27
|
+
import { inspectWorkspaceCleanup } from "./workspaceCleanupInspection.js";
|
|
28
|
+
import { CleanupInspectionError } from "../workspace/cleanupInspection.js";
|
|
27
29
|
const MAIN_WORKTREE = "main";
|
|
28
30
|
const LEADER_ROLE = "leader";
|
|
29
31
|
/**
|
|
@@ -62,11 +64,21 @@ export class FileTaskWorkspacePreparer {
|
|
|
62
64
|
store;
|
|
63
65
|
git;
|
|
64
66
|
now;
|
|
65
|
-
|
|
67
|
+
maintenanceLockOptions;
|
|
68
|
+
constructor(home, store, git = new NodeGitWorkspace(), now = () => new Date(), maintenanceLockOptions = {}) {
|
|
66
69
|
this.home = home;
|
|
67
70
|
this.store = store;
|
|
68
71
|
this.git = git;
|
|
69
72
|
this.now = now;
|
|
73
|
+
this.maintenanceLockOptions = maintenanceLockOptions;
|
|
74
|
+
}
|
|
75
|
+
inspectWorkspaceCleanup(workspace, disposition, forceArchive = false) {
|
|
76
|
+
return inspectWorkspaceCleanup(this.store, this.git, workspace, disposition, forceArchive);
|
|
77
|
+
}
|
|
78
|
+
async #assertWorkspaceCleanup(workspace, disposition) {
|
|
79
|
+
const checks = await this.inspectWorkspaceCleanup(workspace, disposition);
|
|
80
|
+
if (checks.length > 0)
|
|
81
|
+
throw new CleanupInspectionError(checks);
|
|
70
82
|
}
|
|
71
83
|
#resourceRegistrarValue;
|
|
72
84
|
#resourceRegistrar() {
|
|
@@ -75,7 +87,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
75
87
|
#registerWorkspace(workspace) {
|
|
76
88
|
this.#resourceRegistrar().registerManagedWorkspace(workspace);
|
|
77
89
|
}
|
|
78
|
-
async prepareTaskWorkspace(taskId) {
|
|
90
|
+
async prepareTaskWorkspace(taskId, signal) {
|
|
79
91
|
// The per-Project maintenance fence makes prepare mutually exclusive with
|
|
80
92
|
// Project maintenance and Task archive cleanup: a concurrent migration must not switch the
|
|
81
93
|
// Project catalog to the Home-managed repo while prepare is creating
|
|
@@ -87,7 +99,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
87
99
|
for (let attempt = 0;; attempt += 1) {
|
|
88
100
|
try {
|
|
89
101
|
const task = requireTask(this.store, taskId);
|
|
90
|
-
const { release, current } = this.#acquireTaskProjectMaintenanceLocks(task);
|
|
102
|
+
const { release, current } = await this.#acquireTaskProjectMaintenanceLocks(task, signal);
|
|
91
103
|
try {
|
|
92
104
|
return await this.#prepareTaskWorkspaceLocked(current.id, false);
|
|
93
105
|
}
|
|
@@ -116,11 +128,14 @@ export class FileTaskWorkspacePreparer {
|
|
|
116
128
|
* later launch failure can never present itself as "activation never
|
|
117
129
|
* happened".
|
|
118
130
|
*/
|
|
119
|
-
async activateTaskWorkspace(taskId, environment = {}) {
|
|
131
|
+
async activateTaskWorkspace(taskId, environment = {}, signal) {
|
|
120
132
|
for (let attempt = 0;; attempt += 1) {
|
|
121
133
|
let attemptedRequest;
|
|
122
134
|
try {
|
|
123
135
|
const task = requireTask(this.store, taskId);
|
|
136
|
+
if (signal?.aborted) {
|
|
137
|
+
throw new ProjectMaintenanceLockCancelledError(task.projectBindings[0]?.projectId ?? task.id, signal.reason);
|
|
138
|
+
}
|
|
124
139
|
const caller = taskLocalActor(this.store, environment, task.id);
|
|
125
140
|
const actor = task.activationRequest?.operation.actorId === `task:${task.id}/role:leader`
|
|
126
141
|
? "leader" : caller;
|
|
@@ -148,12 +163,14 @@ export class FileTaskWorkspacePreparer {
|
|
|
148
163
|
if (admission.disposition === "ready")
|
|
149
164
|
attemptedRequest = admission.request;
|
|
150
165
|
validateDraftTaskForActivation(this.store, task);
|
|
151
|
-
|
|
152
|
-
return adopted.workspaceFree
|
|
153
|
-
? this.#activateWorkspaceFreeTask(task.id, adopted, actor)
|
|
154
|
-
: await this.#prepareActivatedTaskWorkspace(task, adopted, actor);
|
|
166
|
+
return await this.#prepareActivatedTaskWorkspace(task, actor, environment, signal);
|
|
155
167
|
}
|
|
156
168
|
catch (error) {
|
|
169
|
+
// Waiting did not execute the workspace operation. Preserve the
|
|
170
|
+
// activation request instead of turning contention/stop into failure.
|
|
171
|
+
if (error instanceof ProjectMaintenanceLockedError
|
|
172
|
+
|| error instanceof ProjectMaintenanceLockCancelledError)
|
|
173
|
+
throw error;
|
|
157
174
|
if (error instanceof StorageConflictError
|
|
158
175
|
&& attempt < TASK_WORKSPACE_PREPARE_MAX_CONFLICT_RETRIES) {
|
|
159
176
|
continue;
|
|
@@ -170,10 +187,28 @@ export class FileTaskWorkspacePreparer {
|
|
|
170
187
|
}
|
|
171
188
|
}
|
|
172
189
|
/** Prepares the managed workspace of a Task being activated, under its fences. */
|
|
173
|
-
async #prepareActivatedTaskWorkspace(task,
|
|
174
|
-
const { release, current } = this.#acquireTaskProjectMaintenanceLocks(task);
|
|
190
|
+
async #prepareActivatedTaskWorkspace(task, actor, callerEnvironment, signal) {
|
|
191
|
+
const { release, current } = await this.#acquireTaskProjectMaintenanceLocks(task, signal);
|
|
175
192
|
try {
|
|
176
|
-
|
|
193
|
+
taskLocalActor(this.store, callerEnvironment, current.id);
|
|
194
|
+
if (current.executionGate.state !== "enabled") {
|
|
195
|
+
throw new Error(`Task execution is not enabled: ${current.id}.`);
|
|
196
|
+
}
|
|
197
|
+
if (!isDeepStrictEqual(current.activationRequest, task.activationRequest)) {
|
|
198
|
+
throw new Error(`Task activation request changed while waiting for Project maintenance: ${task.id}.`);
|
|
199
|
+
}
|
|
200
|
+
const provider = this.store.getTaskRoleSessionSet(current.id, LEADER_ROLE)?.providerBinding;
|
|
201
|
+
if (provider?.run != null
|
|
202
|
+
&& ["submitting", "accepted", "delivery-unknown"].includes(provider.run.status)) {
|
|
203
|
+
throw new Error("Planning native input is unsettled; preserve activation intent and retry after its exact terminal.");
|
|
204
|
+
}
|
|
205
|
+
validateDraftTaskForActivation(this.store, current);
|
|
206
|
+
// Acquire before resource adoption: a timed-out/cancelled waiter must
|
|
207
|
+
// not leave an adopted environment to be repeated on the next attempt.
|
|
208
|
+
const adopted = await this.#adoptActivationEnvironment(current);
|
|
209
|
+
return adopted.workspaceFree
|
|
210
|
+
? this.#activateWorkspaceFreeTask(current.id, adopted, actor)
|
|
211
|
+
: await this.#prepareTaskWorkspaceLocked(current.id, true, adopted, actor);
|
|
177
212
|
}
|
|
178
213
|
finally {
|
|
179
214
|
release();
|
|
@@ -330,12 +365,19 @@ export class FileTaskWorkspacePreparer {
|
|
|
330
365
|
* caller re-reads the Task and re-acquires the correct fence set. The
|
|
331
366
|
* caller owns `release` and MUST call it on every exit path.
|
|
332
367
|
*/
|
|
333
|
-
#acquireTaskProjectMaintenanceLocks(task) {
|
|
368
|
+
async #acquireTaskProjectMaintenanceLocks(task, signal) {
|
|
334
369
|
const projectIds = task.projectBindings.map(({ projectId }) => projectId);
|
|
370
|
+
const waitSignal = signal ?? this.maintenanceLockOptions.signal;
|
|
335
371
|
const release = projectIds.length === 0
|
|
336
372
|
? () => { }
|
|
337
|
-
: acquireProjectMaintenanceLocks(this.home, projectIds
|
|
373
|
+
: await acquireProjectMaintenanceLocks(this.home, projectIds, {
|
|
374
|
+
...this.maintenanceLockOptions,
|
|
375
|
+
signal: waitSignal
|
|
376
|
+
});
|
|
338
377
|
try {
|
|
378
|
+
if (waitSignal?.aborted) {
|
|
379
|
+
throw new ProjectMaintenanceLockCancelledError(projectIds[0] ?? task.id, waitSignal.reason);
|
|
380
|
+
}
|
|
339
381
|
const current = requireTask(this.store, task.id);
|
|
340
382
|
const currentIds = current.projectBindings.map(({ projectId }) => projectId).sort();
|
|
341
383
|
const lockedIds = [...projectIds].sort();
|
|
@@ -358,13 +400,16 @@ export class FileTaskWorkspacePreparer {
|
|
|
358
400
|
* catalog in that gap. The caller owns `release` and must call it on every
|
|
359
401
|
* exit path.
|
|
360
402
|
*/
|
|
361
|
-
acquireTaskProjectMaintenanceLocks(taskId) {
|
|
362
|
-
return this.#acquireTaskProjectMaintenanceLocks(requireTask(this.store, taskId));
|
|
403
|
+
acquireTaskProjectMaintenanceLocks(taskId, signal) {
|
|
404
|
+
return this.#acquireTaskProjectMaintenanceLocks(requireTask(this.store, taskId), signal);
|
|
363
405
|
}
|
|
364
406
|
async #prepareTaskWorkspaceLocked(taskId, activate,
|
|
365
407
|
/** Environment and request identity the activation adopted, when one exists. */
|
|
366
408
|
activationEnvironment = { workspaceFree: false }, actor = "user") {
|
|
367
409
|
const task = requireTask(this.store, taskId);
|
|
410
|
+
if (task.executionGate.state !== "enabled") {
|
|
411
|
+
throw new Error(`Task execution is not enabled: ${task.id}.`);
|
|
412
|
+
}
|
|
368
413
|
if (activate && task.status !== "draft") {
|
|
369
414
|
throw new Error(`Only a Draft Task can atomically adopt a workspace: ${task.id}/${task.status}.`);
|
|
370
415
|
}
|
|
@@ -910,15 +955,17 @@ export class FileTaskWorkspacePreparer {
|
|
|
910
955
|
return projects;
|
|
911
956
|
}
|
|
912
957
|
async prepareWorkItemWorkspace(taskId, workItemId) {
|
|
913
|
-
|
|
958
|
+
let item = requireWorkItem(this.store, taskId, workItemId);
|
|
914
959
|
const task = requireTask(this.store, item.taskId);
|
|
915
960
|
assertWorkItemWorkspaceEligible(this.store, task, item);
|
|
916
961
|
// Hold the per-Project maintenance fence across the ensure-main-prepared
|
|
917
962
|
// and the WorkItem worktree creation, so concurrent Project maintenance
|
|
918
963
|
// cannot switch the Project catalog between the two. The under-lock Task
|
|
919
964
|
// snapshot drives every Project read and Git effect below.
|
|
920
|
-
const { release, current: lockedTask } = this.#acquireTaskProjectMaintenanceLocks(task);
|
|
965
|
+
const { release, current: lockedTask } = await this.#acquireTaskProjectMaintenanceLocks(task);
|
|
921
966
|
try {
|
|
967
|
+
item = requireWorkItem(this.store, taskId, workItemId);
|
|
968
|
+
assertWorkItemWorkspaceEligible(this.store, lockedTask, item);
|
|
922
969
|
const lockedProjectIds = lockedTask.projectBindings.map(({ projectId }) => projectId);
|
|
923
970
|
await this.#prepareTaskWorkspaceWithRetries(lockedTask.id, lockedProjectIds);
|
|
924
971
|
// prepareTaskWorkspace may have just minted and persisted the workspace
|
|
@@ -1099,9 +1146,12 @@ export class FileTaskWorkspacePreparer {
|
|
|
1099
1146
|
// already holds (one locked boundary across preparation and adoption);
|
|
1100
1147
|
// otherwise the fence is acquired here.
|
|
1101
1148
|
const { release, current: lockedTask } = heldFence === undefined
|
|
1102
|
-
? this.#acquireTaskProjectMaintenanceLocks(task)
|
|
1149
|
+
? await this.#acquireTaskProjectMaintenanceLocks(task)
|
|
1103
1150
|
: { release: () => { }, current: heldFence.current };
|
|
1104
1151
|
try {
|
|
1152
|
+
if (lockedTask.status !== "active" || lockedTask.executionGate.state !== "enabled") {
|
|
1153
|
+
throw new Error(`Task is not open for Execution Lane preparation: ${lockedTask.id}.`);
|
|
1154
|
+
}
|
|
1105
1155
|
const lineage = executionLaneLineage(this.store, lockedTask, executionGroupId, executionLaneId, hint);
|
|
1106
1156
|
const item = lineage.purpose === "execution"
|
|
1107
1157
|
? this.store.getWorkItem(taskId, lineage.workItemId)
|
|
@@ -1269,6 +1319,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1269
1319
|
&& owner.executionLaneId === executionLaneId));
|
|
1270
1320
|
if (workspace === undefined)
|
|
1271
1321
|
return "missing";
|
|
1322
|
+
await this.#assertWorkspaceCleanup(workspace, "abandoned");
|
|
1272
1323
|
const state = await this.#inspectEntries(task.id, this.#taskSegment(task), managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
|
|
1273
1324
|
if (state === "dirty")
|
|
1274
1325
|
return "dirty";
|
|
@@ -1404,8 +1455,11 @@ export class FileTaskWorkspacePreparer {
|
|
|
1404
1455
|
* never used as a ChangeSet capture source. */
|
|
1405
1456
|
async prepareReviewRoundWorkspace(taskId, reviewRoundId) {
|
|
1406
1457
|
const taskSnapshot = requireTask(this.store, taskId);
|
|
1407
|
-
const { release, current: task } = this.#acquireTaskProjectMaintenanceLocks(taskSnapshot);
|
|
1458
|
+
const { release, current: task } = await this.#acquireTaskProjectMaintenanceLocks(taskSnapshot);
|
|
1408
1459
|
try {
|
|
1460
|
+
if (task.status !== "active" || task.executionGate.state !== "enabled") {
|
|
1461
|
+
throw new Error(`Task is not open for ReviewRound preparation: ${task.id}.`);
|
|
1462
|
+
}
|
|
1409
1463
|
const taskSegment = this.#taskSegment(task);
|
|
1410
1464
|
const round = this.store.getReviewRound(task.id, reviewRoundId);
|
|
1411
1465
|
if (round === null)
|
|
@@ -1878,6 +1932,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1878
1932
|
if (!isDeepStrictEqual(round.workspace, workspace)) {
|
|
1879
1933
|
throw new Error(`ReviewRound workspace record diverged: ${round.id}.`);
|
|
1880
1934
|
}
|
|
1935
|
+
await this.#assertWorkspaceCleanup(workspace, "abandoned");
|
|
1881
1936
|
assertWorkspaceSessionsRetirable(this.store, task.id, round.reviewerRoleName, this.now());
|
|
1882
1937
|
for (const lane of round.executionGroup?.lanes ?? []) {
|
|
1883
1938
|
assertWorkspaceSessionsRetirable(this.store, task.id, lane.roleName, this.now());
|
|
@@ -1945,6 +2000,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1945
2000
|
throw new Error(`WorkItem has no managed isolated worktree workspace: ${item.id}.`);
|
|
1946
2001
|
}
|
|
1947
2002
|
assertWorkItemOwnsWorkspace(item, workspace);
|
|
2003
|
+
await this.#assertWorkspaceCleanup(workspace, disposition);
|
|
1948
2004
|
if (item.assignee !== undefined) {
|
|
1949
2005
|
assertWorkspaceSessionsRetirable(this.store, task.id, item.assignee, this.now());
|
|
1950
2006
|
}
|
|
@@ -1998,7 +2054,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1998
2054
|
let found = false;
|
|
1999
2055
|
for (const entry of workspace.entries.filter(({ access }) => access === "write")) {
|
|
2000
2056
|
try {
|
|
2001
|
-
if (!await this.git.
|
|
2057
|
+
if (!await this.git.inspectClean(entry.path))
|
|
2002
2058
|
return "dirty";
|
|
2003
2059
|
found = true;
|
|
2004
2060
|
}
|
|
@@ -2010,7 +2066,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
2010
2066
|
}
|
|
2011
2067
|
return found ? "clean" : "missing";
|
|
2012
2068
|
}
|
|
2013
|
-
async cleanupIntegrationWorkspace(taskId, integrationId) {
|
|
2069
|
+
async cleanupIntegrationWorkspace(taskId, integrationId, options = {}) {
|
|
2014
2070
|
const task = requireTask(this.store, taskId);
|
|
2015
2071
|
const attempt = this.store.getIntegrationAttempt(task.id, integrationId);
|
|
2016
2072
|
if (attempt === null) {
|
|
@@ -2023,6 +2079,11 @@ export class FileTaskWorkspacePreparer {
|
|
|
2023
2079
|
throw new Error(`IntegrationAttempt must be terminal before cleanup: ${attempt.id}.`);
|
|
2024
2080
|
}
|
|
2025
2081
|
const workspace = this.store.getIntegrationWorkspace(task.id, attempt.id);
|
|
2082
|
+
// A failed Integration's explicitly disposable conflict tree retains its
|
|
2083
|
+
// existing cleanup contract outside archive. Archive never discards dirt.
|
|
2084
|
+
if (workspace !== null && (options.preserveChanges || task.status === "archived" || attempt.status !== "failed")) {
|
|
2085
|
+
await this.#assertWorkspaceCleanup(workspace, "abandoned");
|
|
2086
|
+
}
|
|
2026
2087
|
const project = requireProject(this.store, attempt.projectId);
|
|
2027
2088
|
// The recorded entry pins the exact on-disk worktree path; reconstruct the
|
|
2028
2089
|
// owner root from it so removal targets the real Git worktree regardless of
|
|
@@ -2042,7 +2103,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
2042
2103
|
directory,
|
|
2043
2104
|
taskSegment: this.#taskSegment(task),
|
|
2044
2105
|
integrationId: attempt.id,
|
|
2045
|
-
discardChanges: task.status !== "archived" && attempt.status === "failed"
|
|
2106
|
+
discardChanges: options.preserveChanges !== true && task.status !== "archived" && attempt.status === "failed"
|
|
2046
2107
|
});
|
|
2047
2108
|
if (result !== "dirty") {
|
|
2048
2109
|
for (const entry of workspace?.entries ?? []) {
|
|
@@ -2069,7 +2130,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
2069
2130
|
}
|
|
2070
2131
|
return this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries);
|
|
2071
2132
|
}
|
|
2072
|
-
async cleanupTaskForArchive(taskId) {
|
|
2133
|
+
async cleanupTaskForArchive(taskId, disposition = "abandoned") {
|
|
2073
2134
|
const task = requireTask(this.store, taskId);
|
|
2074
2135
|
assertTaskArchiveState(task, task);
|
|
2075
2136
|
const isolated = this.store.listManagedWorkspaces(task.id)
|
|
@@ -2084,6 +2145,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
2084
2145
|
}
|
|
2085
2146
|
const main = this.store.getTaskWorkspace(task.id);
|
|
2086
2147
|
if (main !== null) {
|
|
2148
|
+
await this.#assertWorkspaceCleanup(main, disposition);
|
|
2087
2149
|
assertTaskArchiveState(requireTask(this.store, task.id), task);
|
|
2088
2150
|
if (await this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries) === "dirty") {
|
|
2089
2151
|
return {
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { dirname, join, relative, isAbsolute } from "node:path";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { managedWorkspaceKey, managedWorktreeName } from "../worktree/managedWorkspace.js";
|
|
4
|
+
import { WorkItemChangeSetManager } from "../workspace/workItemChangeSetManager.js";
|
|
5
|
+
import { cleanupCheckFromError } from "../workspace/cleanupInspection.js";
|
|
6
|
+
import { projectTaskRemoteDeliveryFromStore } from "../commands/taskRemoteDeliveryCommand.js";
|
|
7
|
+
import { taskWorkspaceRefSegment } from "./taskWorkspaceIdentity.js";
|
|
8
|
+
import { integrationWorktreeIdentity, worktreeIdentity } from "./gitWorkspace.js";
|
|
9
|
+
/** Current owner facts + the same Git inspection used immediately before removal.
|
|
10
|
+
* No preparation, locks, Git refresh, runtime stop, DB writes or repair.
|
|
11
|
+
*/
|
|
12
|
+
export async function inspectWorkspaceCleanup(store, git, workspace, disposition, forceArchive = false) {
|
|
13
|
+
const owner = workspace.owner;
|
|
14
|
+
const task = store.getTask(owner.taskId);
|
|
15
|
+
if (task === null)
|
|
16
|
+
throw new Error(`Task not found: ${owner.taskId}.`);
|
|
17
|
+
const resource = managedWorkspaceKey(owner);
|
|
18
|
+
const sources = [resource];
|
|
19
|
+
const actions = workspaceCleanupActions(workspace);
|
|
20
|
+
const checks = [];
|
|
21
|
+
const add = (reason, detail, expected, observed) => checks.push({ resource, reason, status: "blocked", detail, expected, observed, sources, actions });
|
|
22
|
+
if (!isDeepStrictEqual(store.getManagedWorkspace(owner), workspace)) {
|
|
23
|
+
add("workspace-identity-mismatch", "Current workspace ownership differs from the inspected record.", "same managed workspace", "changed or missing");
|
|
24
|
+
return checks;
|
|
25
|
+
}
|
|
26
|
+
if (task.workspaceIdentity === undefined && workspace.entries.some(e => e.access === "write")) {
|
|
27
|
+
add("workspace-metadata-missing", "Task has no durable workspace identity; do not infer one from paths.", "recorded Task workspace identity", null);
|
|
28
|
+
return checks;
|
|
29
|
+
}
|
|
30
|
+
let roleName = managedWorktreeName(owner);
|
|
31
|
+
switch (owner.type) {
|
|
32
|
+
case "work-item": {
|
|
33
|
+
const item = store.getWorkItem(task.id, owner.workItemId);
|
|
34
|
+
if (item === null || !["accepted", "retired"].includes(item.status)) {
|
|
35
|
+
add("owner-unsettled", "WorkItem must be terminal before cleanup.", ["accepted", "retired"], item?.status ?? null);
|
|
36
|
+
}
|
|
37
|
+
if (item?.workspaceDisposition !== undefined && item.workspaceDisposition !== disposition) {
|
|
38
|
+
add("workspace-disposition-mismatch", "WorkItem cleanup disposition differs from the recorded decision.", item.workspaceDisposition, disposition);
|
|
39
|
+
}
|
|
40
|
+
if (item?.status === "accepted" && disposition === "integrated") {
|
|
41
|
+
const inspection = await new WorkItemChangeSetManager(store).inspectIntegrated(task.id, item.id);
|
|
42
|
+
checks.push(...inspection.checks);
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case "review-round": {
|
|
47
|
+
const round = store.getReviewRound(task.id, owner.reviewRoundId);
|
|
48
|
+
if (round === null || !["completed", "failed"].includes(round.status)) {
|
|
49
|
+
add("owner-unsettled", "ReviewRound must be terminal before cleanup.", ["completed", "failed"], round?.status ?? null);
|
|
50
|
+
}
|
|
51
|
+
if (round?.workspace === undefined || !isDeepStrictEqual(round.workspace, workspace)) {
|
|
52
|
+
add("workspace-metadata-mismatch", "ReviewRound and managed workspace records diverge.", "same workspace in ReviewRound and registry", "different or missing mirror");
|
|
53
|
+
}
|
|
54
|
+
if (round?.scope === "task")
|
|
55
|
+
roleName = `reviewer-${round.reviewerRoleName}`;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "execution-lane": {
|
|
59
|
+
const group = owner.purpose === "execution"
|
|
60
|
+
? store.getWorkItem(task.id, owner.workItemId)?.executionGroups.find(g => g.id === owner.executionGroupId)
|
|
61
|
+
: store.getReviewRound(task.id, owner.reviewRoundId)?.executionGroup;
|
|
62
|
+
const lane = group?.id === owner.executionGroupId
|
|
63
|
+
? group.lanes.find(l => l.id === owner.executionLaneId) : undefined;
|
|
64
|
+
if (lane === undefined || lane.disposition === "open") {
|
|
65
|
+
add("owner-unsettled", "Execution Lane is not terminally resolved.", "settled lane", lane?.disposition ?? null);
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "integration-attempt": {
|
|
70
|
+
const attempt = store.getIntegrationAttempt(task.id, owner.integrationAttemptId);
|
|
71
|
+
if (attempt === null || ["running", "blocked", "conflicted", "validating"].includes(attempt.status)) {
|
|
72
|
+
add("owner-unsettled", "IntegrationAttempt must be terminal before cleanup.", "terminal attempt", attempt?.status ?? null);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case "task":
|
|
77
|
+
if (!["completed", "cancelled", "archived"].includes(task.status)) {
|
|
78
|
+
add("task-not-terminal", "Task must be completed or retired before archive cleanup.", ["completed", "cancelled", "archived"], task.status);
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
const delivery = owner.type === "task" && (disposition === "integrated" || task.status === "archived" || forceArchive)
|
|
83
|
+
? await readArchiveDelivery(store, git, task, forceArchive) : undefined;
|
|
84
|
+
if (delivery !== undefined && (!delivery.allMerged || !delivery.allVerified)) {
|
|
85
|
+
add("delivery-coverage", "Task main has commits without verified remote coverage; cleanup does not establish delivery.", { allMerged: true, allVerified: true }, { allMerged: delivery.allMerged, allVerified: delivery.allVerified });
|
|
86
|
+
}
|
|
87
|
+
for (const entry of workspace.entries.filter(e => e.access === "write")) {
|
|
88
|
+
const entryResource = `${resource}/${entry.projectId}`;
|
|
89
|
+
const entryChecks = [];
|
|
90
|
+
const check = (reason, detail, expected, observed) => entryChecks.push({ resource: entryResource, reason, status: "blocked", detail, expected, observed, sources, actions });
|
|
91
|
+
// Integration's durable root is its single Project worktree (not the
|
|
92
|
+
// multi-Project owner container used by the other workspace owners).
|
|
93
|
+
const container = owner.type === "integration-attempt" ? dirname(workspace.root) : workspace.root;
|
|
94
|
+
const expectedPath = join(container, entry.directory);
|
|
95
|
+
if (entry.path !== expectedPath) {
|
|
96
|
+
check("workspace-path-mismatch", "Recorded path is not the current owner-root/Project path; relocation is not proven.", { ownerRelativePath: entry.directory }, { ownerRelativePath: safeRelativePath(container, entry.path) });
|
|
97
|
+
checks.push(...entryChecks);
|
|
98
|
+
continue; // Never inspect a foreign/mismatched path.
|
|
99
|
+
}
|
|
100
|
+
const taskSegment = taskWorkspaceRefSegment(task);
|
|
101
|
+
const expectedBranch = owner.type === "integration-attempt"
|
|
102
|
+
? integrationWorktreeIdentity(taskSegment, owner.integrationAttemptId).branch
|
|
103
|
+
: worktreeIdentity(taskSegment, roleName).branch;
|
|
104
|
+
if (entry.branch !== expectedBranch) {
|
|
105
|
+
check("workspace-identity-mismatch", "Recorded branch does not match the exact owner.", expectedBranch, entry.branch);
|
|
106
|
+
checks.push(...entryChecks);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const main = store.getTaskWorkspace(task.id)?.entries.find(e => e.projectId === entry.projectId);
|
|
111
|
+
if (main === undefined && owner.type !== "task") {
|
|
112
|
+
check("workspace-metadata-missing", "Task main Git repository reference is unavailable.", "Task main Project entry", null);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const state = owner.type === "task"
|
|
116
|
+
? await git.inspectTaskClone({ path: entry.path, container: workspace.root, directory: entry.directory,
|
|
117
|
+
taskSegment, branch: entry.branch })
|
|
118
|
+
: await git.inspectWorktree({ repositoryPath: main.path, container, directory: entry.directory,
|
|
119
|
+
taskSegment, roleName, expectedBranch });
|
|
120
|
+
if (state === "dirty")
|
|
121
|
+
check("dirty-worktree", "Uncommitted or untracked changes must be preserved.", "clean", "dirty");
|
|
122
|
+
if (owner.type === "task" && state !== "missing" && delivery !== undefined) {
|
|
123
|
+
const covered = delivery.projects.find(p => p.projectId === entry.projectId);
|
|
124
|
+
const expected = covered?.expectedLocalCommit;
|
|
125
|
+
const observed = (await git.inspect(entry.path, "HEAD")).baseCommit;
|
|
126
|
+
if (expected != null && expected !== observed && covered?.deliveryLocalCommit !== observed) {
|
|
127
|
+
check("head-mismatch", "Task main HEAD differs from the accepted head and its covered publication candidate.", { acceptedCommit: expected, deliveryLocalCommit: covered?.deliveryLocalCommit ?? null }, observed);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
entryChecks.push(...cleanupCheckFromError(error, entryResource, sources, actions));
|
|
134
|
+
}
|
|
135
|
+
checks.push(...entryChecks);
|
|
136
|
+
}
|
|
137
|
+
return checks;
|
|
138
|
+
}
|
|
139
|
+
/** Reuse Publication's projection, supplying live heads only where its existing
|
|
140
|
+
* contract asks for them (active/cancelled). Completed/archived heads stay frozen.
|
|
141
|
+
* Never prepare a workspace or substitute an old binding's commit for a failed read.
|
|
142
|
+
*/
|
|
143
|
+
export async function readArchiveDelivery(store, git, task, forceArchive = false) {
|
|
144
|
+
const projects = [];
|
|
145
|
+
// Force admission preserves its existing store-only evidence; it never
|
|
146
|
+
// freezes new delivery heads merely because an inspection observed them.
|
|
147
|
+
if (!forceArchive && (task.status === "active" || task.status === "cancelled") && task.workspaceIdentity !== undefined) {
|
|
148
|
+
const main = store.getTaskWorkspace(task.id);
|
|
149
|
+
if (main?.owner.type === "task" && main.owner.taskId === task.id) {
|
|
150
|
+
const branch = worktreeIdentity(taskWorkspaceRefSegment(task), "main").branch;
|
|
151
|
+
for (const entry of main.entries.filter(e => e.access === "write")) {
|
|
152
|
+
if (entry.path !== join(main.root, entry.directory) || entry.branch !== branch)
|
|
153
|
+
continue;
|
|
154
|
+
try {
|
|
155
|
+
const head = await git.inspect(entry.path, "HEAD");
|
|
156
|
+
if (head.root === entry.path && await git.headRef(entry.path) === branch) {
|
|
157
|
+
projects.push({ projectId: entry.projectId, commit: head.baseCommit });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch { /* Failed reads remain unavailable in the existing projection. */ }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return projectTaskRemoteDeliveryFromStore(store, task, { projects });
|
|
165
|
+
}
|
|
166
|
+
export function workspaceCleanupActions(workspace) {
|
|
167
|
+
const owner = workspace.owner;
|
|
168
|
+
switch (owner.type) {
|
|
169
|
+
case "task": return [`yui task remote-delivery ${owner.taskId}`, `yui task show ${owner.taskId}`];
|
|
170
|
+
case "work-item": return [`yui task work show ${owner.taskId}/${owner.workItemId}`,
|
|
171
|
+
`yui task work cleanup ${owner.taskId}/${owner.workItemId} --integrated|--abandon`];
|
|
172
|
+
case "review-round": return [`yui task work review show ${owner.taskId}/${owner.reviewRoundId}`,
|
|
173
|
+
`yui task work review cleanup ${owner.taskId}/${owner.reviewRoundId}`];
|
|
174
|
+
case "integration-attempt": return [`yui task integration show ${owner.taskId}/${owner.integrationAttemptId}`,
|
|
175
|
+
`yui task integration cleanup ${owner.taskId}/${owner.integrationAttemptId}`];
|
|
176
|
+
case "execution-lane": return owner.purpose === "execution"
|
|
177
|
+
? [`yui task work show ${owner.taskId}/${owner.workItemId}`,
|
|
178
|
+
`yui task work cleanup ${owner.taskId}/${owner.workItemId} --integrated|--abandon`]
|
|
179
|
+
: [`yui task work review show ${owner.taskId}/${owner.reviewRoundId}`,
|
|
180
|
+
`yui task work review cleanup ${owner.taskId}/${owner.reviewRoundId}`];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Only expose paths inside this already-authorized owner, never foreign paths. */
|
|
184
|
+
export function safeRelativePath(root, path) {
|
|
185
|
+
const value = relative(root, path);
|
|
186
|
+
return value === ".." || value.startsWith("../") || isAbsolute(value) ? "[outside owner root]" : value || ".";
|
|
187
|
+
}
|
|
@@ -21,7 +21,7 @@ export const AGENT_ERROR_CATEGORIES = Object.freeze([
|
|
|
21
21
|
]);
|
|
22
22
|
export function standardAgentError(input) {
|
|
23
23
|
const classification = input.classification ?? UNKNOWN_AGENT_ERROR_CLASSIFICATION;
|
|
24
|
-
const retryAfterMs = input.retryAfterMs;
|
|
24
|
+
const retryAfterMs = input.retryAfterMs ?? classification.retryAfterMs;
|
|
25
25
|
if (retryAfterMs !== undefined
|
|
26
26
|
&& (!Number.isSafeInteger(retryAfterMs) || retryAfterMs < 0)) {
|
|
27
27
|
throw new Error("Agent error retryAfterMs must be a non-negative safe integer.");
|
|
@@ -42,7 +42,8 @@ export function standardAgentError(input) {
|
|
|
42
42
|
sessionDisposition: input.sessionDisposition
|
|
43
43
|
?? classification.sessionDisposition
|
|
44
44
|
?? "unknown",
|
|
45
|
-
...(retryAfterMs === undefined ? {} : { retryAfterMs })
|
|
45
|
+
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
46
|
+
...(classification.retryable === undefined ? {} : { retryable: classification.retryable })
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
49
|
export const UNKNOWN_AGENT_ERROR_CLASSIFICATION = Object.freeze({
|
|
@@ -177,7 +178,8 @@ export function isStandardAgentError(value) {
|
|
|
177
178
|
&& ["accepted", "not-accepted", "unknown"].includes(error.inputDisposition ?? "")
|
|
178
179
|
&& ["recoverable", "unrecoverable", "unknown"].includes(error.sessionDisposition ?? "")
|
|
179
180
|
&& (error.retryAfterMs === undefined
|
|
180
|
-
|| (Number.isSafeInteger(error.retryAfterMs) && error.retryAfterMs >= 0))
|
|
181
|
+
|| (Number.isSafeInteger(error.retryAfterMs) && error.retryAfterMs >= 0))
|
|
182
|
+
&& (error.retryable === undefined || typeof error.retryable === "boolean");
|
|
181
183
|
}
|
|
182
184
|
/**
|
|
183
185
|
* Upper bound for one persisted raw payload. A Provider stack trace or a
|
|
@@ -18,6 +18,7 @@ import { persistRuntimeProcessExitObservation, replayRuntimeProcessExitOutbox }
|
|
|
18
18
|
import { AGENT_HOST_CLIENT_EXIT_GRACE_MS, AGENT_HOST_CONTROL_TIMEOUT_MS, AGENT_HOST_READY_TIMEOUT_MS, ENDPOINT_DRAIN_TIMEOUT_MS, PROVIDER_ACCEPT_TIMEOUT_MS } from "./runtimeDeadlines.js";
|
|
19
19
|
import { providerDeliveryFailure, providerDeliveryFailureFrom, redactAgentErrorText, serializeAgentErrorRaw } from "./agentError.js";
|
|
20
20
|
import { runCodexInteractiveHost } from "./codexInteractiveHost.js";
|
|
21
|
+
import { CodexPreSubmissionError } from "./codexAppServerRuntime.js";
|
|
21
22
|
import { readAgentRunConfigurationObservation, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
|
|
22
23
|
import { createSessionOwnerIdentity, readLinuxProcessIdentity } from "./sessionOwnerIdentity.js";
|
|
23
24
|
import { yuiTmuxServerName, yuiTmuxSessionName } from "../tmux/tmuxManager.js";
|
|
@@ -767,13 +768,25 @@ export async function runAgentHost(input) {
|
|
|
767
768
|
attemptId: request.run.attemptId,
|
|
768
769
|
...authorityFields()
|
|
769
770
|
}));
|
|
770
|
-
const durableRun = hostRunControlParams(sessionPayload, session.nativeSessionId, request.authority, request.run.attemptId, request.runId)
|
|
771
|
+
const durableRun = { ...hostRunControlParams(sessionPayload, session.nativeSessionId, request.authority, request.run.attemptId, request.runId), boundedText: request.run.boundedText,
|
|
772
|
+
retrySupport: session.adapterId === "codex" ? "codex-failed-turn-v1" : "unsupported" };
|
|
773
|
+
let recoveryInput;
|
|
771
774
|
// Registration precedes the Provider write, so its failure modes are not
|
|
772
775
|
// the Provider's. A definite failure means the Provider never saw this
|
|
773
776
|
// input and the occupancy must be released; anything unconfirmed leaves
|
|
774
777
|
// durable state ambiguous and keeps it held.
|
|
775
778
|
try {
|
|
776
|
-
await beginDurableProviderTurn(input.home, durableRun);
|
|
779
|
+
const admitted = await beginDurableProviderTurn(input.home, durableRun);
|
|
780
|
+
if (admitted !== null && typeof admitted === "object" && !Array.isArray(admitted)) {
|
|
781
|
+
const retry = admitted.retryInput;
|
|
782
|
+
if (retry !== null && typeof retry === "object" && !Array.isArray(retry)
|
|
783
|
+
&& typeof retry.text === "string")
|
|
784
|
+
recoveryInput = {
|
|
785
|
+
text: retry.text,
|
|
786
|
+
...(typeof retry.expectedFailedNativeTurnId === "string"
|
|
787
|
+
? { expectedFailedNativeTurnId: retry.expectedFailedNativeTurnId } : {})
|
|
788
|
+
};
|
|
789
|
+
}
|
|
777
790
|
}
|
|
778
791
|
catch (error) {
|
|
779
792
|
// Registration ends in one of three states and they are not
|
|
@@ -831,7 +844,11 @@ export async function runAgentHost(input) {
|
|
|
831
844
|
// authority and Session identity before this native write.
|
|
832
845
|
const receipt = endpointReceipt(await session.submit({
|
|
833
846
|
...request.run,
|
|
834
|
-
boundedText: withSessionContextPointer(request.run.boundedText, activeRunPayload.environment),
|
|
847
|
+
boundedText: withSessionContextPointer(recoveryInput?.text ?? request.run.boundedText, activeRunPayload.environment),
|
|
848
|
+
...(recoveryInput === undefined ? {} : { requireQuiescent: true }),
|
|
849
|
+
...(recoveryInput?.expectedFailedNativeTurnId === undefined ? {} : {
|
|
850
|
+
expectedFailedNativeTurnId: recoveryInput.expectedFailedNativeTurnId
|
|
851
|
+
}),
|
|
835
852
|
inputRef: request.runId ?? request.run.attemptId
|
|
836
853
|
}), request.run.attemptId);
|
|
837
854
|
transportAccepted = true;
|
|
@@ -1852,7 +1869,7 @@ function hostRunControlParams(payload, nativeSessionId, authority, attemptId, ru
|
|
|
1852
1869
|
}
|
|
1853
1870
|
async function beginDurableProviderTurn(home, durableRun) {
|
|
1854
1871
|
try {
|
|
1855
|
-
await callControllerIdempotently(home, "runtime.provider-turn-begin", durableRun);
|
|
1872
|
+
return await callControllerIdempotently(home, "runtime.provider-turn-begin", durableRun);
|
|
1856
1873
|
}
|
|
1857
1874
|
catch (error) {
|
|
1858
1875
|
if (error instanceof ControllerClientError && error.code === "SESSION_BUSY") {
|
|
@@ -1860,7 +1877,7 @@ async function beginDurableProviderTurn(home, durableRun) {
|
|
|
1860
1877
|
}
|
|
1861
1878
|
if (!(error instanceof ControllerAcknowledgementUnknownError))
|
|
1862
1879
|
throw error;
|
|
1863
|
-
await resolveProviderTurnSubmission(home, durableRun, new Error(`Provider Turn intent acknowledgement failed before Provider write: ${errorText(error)}`, { cause: error }));
|
|
1880
|
+
await resolveProviderTurnSubmission(home, durableRun, new Error(`Provider Turn intent acknowledgement failed before Provider write: ${errorText(error)}`, { cause: error }), true);
|
|
1864
1881
|
// The resolve is fenced on this exact attempt id, so its success proves
|
|
1865
1882
|
// the registration did commit and is now settled with nothing written to
|
|
1866
1883
|
// the Provider. That is a known outcome; reporting it as unknown would
|
|
@@ -1871,7 +1888,7 @@ async function beginDurableProviderTurn(home, durableRun) {
|
|
|
1871
1888
|
}
|
|
1872
1889
|
async function callControllerIdempotently(home, method, request) {
|
|
1873
1890
|
try {
|
|
1874
|
-
await callAgentController(home, method, request);
|
|
1891
|
+
return await callAgentController(home, method, request);
|
|
1875
1892
|
}
|
|
1876
1893
|
catch (error) {
|
|
1877
1894
|
if (!(error instanceof ControllerClientError)
|
|
@@ -1882,7 +1899,7 @@ async function callControllerIdempotently(home, method, request) {
|
|
|
1882
1899
|
// These methods carry exact attempt, launch, and authority fences. A
|
|
1883
1900
|
// bounded replay confirms a commit whose acknowledgement may have been lost.
|
|
1884
1901
|
try {
|
|
1885
|
-
await callAgentController(home, method, request);
|
|
1902
|
+
return await callAgentController(home, method, request);
|
|
1886
1903
|
}
|
|
1887
1904
|
catch (replayError) {
|
|
1888
1905
|
if (firstCallMayHaveApplied || controllerCallMayHaveApplied(replayError)) {
|
|
@@ -1903,7 +1920,7 @@ class ControllerAcknowledgementUnknownError extends Error {
|
|
|
1903
1920
|
class ProviderTurnRegistrationSettledError extends Error {
|
|
1904
1921
|
name = "ProviderTurnRegistrationSettledError";
|
|
1905
1922
|
}
|
|
1906
|
-
async function resolveProviderTurnSubmission(home, durableRun, error) {
|
|
1923
|
+
async function resolveProviderTurnSubmission(home, durableRun, error, noProviderWrite = false) {
|
|
1907
1924
|
const attemptId = durableRun.attemptId;
|
|
1908
1925
|
if (typeof attemptId !== "string") {
|
|
1909
1926
|
throw new Error("Provider Turn resolution has no attempt id.");
|
|
@@ -1915,6 +1932,7 @@ async function resolveProviderTurnSubmission(home, durableRun, error) {
|
|
|
1915
1932
|
: error instanceof ProviderTurnBusyError ? "deferred" : "rejected",
|
|
1916
1933
|
reason: errorText(error),
|
|
1917
1934
|
raw: serializeAgentErrorRaw(error),
|
|
1935
|
+
...(noProviderWrite || error instanceof CodexPreSubmissionError ? { providerWrite: "not-issued" } : {}),
|
|
1918
1936
|
observedAt: new Date().toISOString()
|
|
1919
1937
|
};
|
|
1920
1938
|
try {
|
|
@@ -1935,8 +1953,7 @@ async function resolveProviderTurnSubmission(home, durableRun, error) {
|
|
|
1935
1953
|
*/
|
|
1936
1954
|
async function callAgentController(home, method, params) {
|
|
1937
1955
|
try {
|
|
1938
|
-
await callController(home, method, params);
|
|
1939
|
-
return;
|
|
1956
|
+
return await callController(home, method, params);
|
|
1940
1957
|
}
|
|
1941
1958
|
catch (error) {
|
|
1942
1959
|
if (!isControllerUnavailable(error) || !isForeignHandoverLockHeld(home)) {
|
|
@@ -1948,7 +1965,7 @@ async function callAgentController(home, method, params) {
|
|
|
1948
1965
|
await delay(50);
|
|
1949
1966
|
// The pinned Host can rediscover a replacement, but must never start a
|
|
1950
1967
|
// Controller using its own old implementation or open the Home's database.
|
|
1951
|
-
await callController(home, method, params);
|
|
1968
|
+
return await callController(home, method, params);
|
|
1952
1969
|
}
|
|
1953
1970
|
function isControllerUnavailable(error) {
|
|
1954
1971
|
return error instanceof ControllerClientError
|