@zq-silk/yui 0.16.0 → 0.16.2
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 +12 -0
- package/ARCHITECTURE.zh-CN.md +8 -0
- package/README.md +3 -0
- package/dist/agent/managedRuntimeEnvironment.js +0 -4
- package/dist/agentRun/agentRun.js +7 -17
- package/dist/cli/agentConfigurationPicker.js +24 -21
- package/dist/cli/commandCatalog.js +18 -10
- package/dist/cli/roleWizard.js +41 -30
- package/dist/cli/updateCommand.js +35 -13
- package/dist/cli/updateOrchestrator.js +73 -33
- package/dist/cli/updatePorts.js +51 -20
- package/dist/cli.js +35 -26
- package/dist/commands/controllerCommands.js +1 -1
- package/dist/commands/executionAuditCommands.js +2 -2
- package/dist/commands/projectCommands.js +1 -1
- package/dist/commands/releaseCommands.js +9 -38
- package/dist/commands/taskCommands.js +84 -81
- package/dist/commands/taskCompletionGate.js +0 -117
- package/dist/commands/taskRoleRuntimeStatus.js +1 -1
- package/dist/commands/taskUpstreamCommands.js +93 -54
- package/dist/context/runContextPack.js +30 -54
- package/dist/context/runInputContract.js +9 -0
- package/dist/context/sessionBootstrapManifest.js +11 -19
- package/dist/controller/agentHostObservation.js +4 -2
- package/dist/controller/controller.js +8 -6
- package/dist/controller/fileSchedulerStoreAdapter.js +85 -18
- package/dist/controller/globalInputDelivery.js +28 -7
- package/dist/controller/globalRuntimeAttention.js +34 -0
- package/dist/controller/operatorNotification.js +31 -0
- package/dist/controller/providerRetryAdmission.js +3 -1
- package/dist/controller/providerRetryDelivery.js +10 -3
- package/dist/controller/runtime.js +1 -27
- package/dist/controller/sessionOwnerReconciliation.js +42 -24
- package/dist/controller/structuredProviderObservation.js +27 -0
- package/dist/controller/updateReconciliation.js +72 -20
- package/dist/doctor/doctor.js +2 -1
- package/dist/executor/agentAdapter.js +28 -96
- package/dist/executor/agentConfigurationCatalog.js +13 -68
- package/dist/executor/agentConfigurationFields.js +120 -0
- package/dist/executor/agentConfigurationProbe.js +55 -80
- package/dist/executor/agentExecutor.js +5 -2
- package/dist/executor/executorRegistry.js +0 -9
- package/dist/executor/fileRoleLaunchPlanner.js +12 -11
- package/dist/interaction/operatorPresentation.js +2 -1
- package/dist/message/messageContinuation.js +8 -9
- package/dist/observability/executionAudit.js +1 -10
- package/dist/observability/runtimeIdentity.js +0 -23
- package/dist/output/agentConfigurationPresentation.js +4 -2
- package/dist/release/releaseHandover.js +2 -2
- package/dist/release/releaseIdempotencyStore.js +0 -23
- package/dist/release/releaseWorkflowPorts.js +8 -4
- package/dist/repository/gitWorkspace.js +66 -19
- package/dist/repository/taskWorkspacePreparer.js +75 -68
- package/dist/resources/resourceRegistry.js +90 -44
- package/dist/resources/sqliteResourceRegistry.js +3 -5
- package/dist/runtime/acpSession.js +21 -5
- package/dist/runtime/agentEndpoint.js +4 -1
- package/dist/runtime/agentHost.js +22 -62
- package/dist/runtime/agentHostCleanup.js +85 -0
- package/dist/runtime/agentHostProtocol.js +1 -1
- package/dist/runtime/builtinAgentDrivers.js +46 -119
- package/dist/runtime/builtinTranscriptObserver.js +10 -6
- package/dist/runtime/codexAppServerRuntime.js +55 -89
- package/dist/runtime/jsonLineChannel.js +35 -7
- package/dist/runtime/providerContinuationReconciliationService.js +42 -3
- package/dist/runtime/providerRetry.js +15 -0
- package/dist/runtime/providerRuntimeReconciler.js +12 -2
- package/dist/runtime/structuredProviderHost.js +106 -81
- package/dist/scheduler/activeRoleRunDelivery.js +5 -1
- package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
- package/dist/storage/upgrade/upgradeOrchestrator.js +0 -10
- package/dist/task/nextAction.js +33 -23
- package/dist/task/task.js +2 -1
- package/dist/web/assets/assetManifest.js +2 -0
- package/dist/web/assets/client/app.js +72 -3
- package/dist/web/assets/client/components.js +2 -1
- package/dist/web/assets/client/i18n.js +2 -0
- package/dist/web/assets/client/taskSummary.js +345 -0
- package/dist/web/assets/client/taskSurface.js +44 -26
- package/dist/web/assets/client/view.js +33 -0
- package/dist/web/assets/shell.js +1 -0
- package/dist/web/assets/styles/cards.js +21 -0
- package/dist/web/webServer.js +39 -5
- package/dist/web/webSessions.js +165 -0
- package/dist/web/webSnapshot.js +23 -0
- package/dist/web/webTaskSurface.js +23 -0
- package/docs/managed-turn-and-session-runtime.md +31 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +23 -0
- package/docs/observability/README.md +47 -0
- package/docs/observability/README.zh-CN.md +37 -0
- package/docs/project-refresh.md +9 -0
- package/docs/project-refresh.zh-CN.md +8 -0
- package/docs/provider-retry.md +35 -0
- package/docs/release-workflow.md +139 -20
- package/docs/release-workflow.zh-CN.md +115 -14
- package/docs/roles-and-configuration.md +30 -0
- package/docs/roles-and-configuration.zh-CN.md +20 -0
- package/docs/task-delivery.md +109 -0
- package/docs/task-delivery.zh-CN.md +80 -0
- package/docs/task-discovery.md +9 -0
- package/docs/task-discovery.zh-CN.md +6 -0
- package/docs/testing/verification-levels.md +33 -12
- package/docs/testing/verification-levels.zh-CN.md +28 -8
- package/i18n/README.zh-CN.md +2 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +11 -0
- package/skills/yui-leader/references/execution.md +22 -0
- package/skills/yui-leader/references/planning.md +6 -2
- package/skills/yui-operator/SKILL.md +45 -13
- package/skills/yui-operator/references/task-delivery.md +99 -0
- package/skills/yui-runtime/SKILL.md +5 -3
- package/skills/yui-runtime/references/publication.md +56 -5
- package/skills/yui-runtime/references/recovery.md +12 -2
- package/dist/runtime/agentHostCompatibility.js +0 -127
|
@@ -348,6 +348,11 @@ export class NodeGitWorkspace {
|
|
|
348
348
|
}
|
|
349
349
|
await canonicalContainer(dirname(destination), true);
|
|
350
350
|
const branch = input.branch === undefined ? undefined : safeRef(input.branch);
|
|
351
|
+
// Reserve exactly this operation's directory before invoking Git. A clone
|
|
352
|
+
// rejection because somebody else created the destination cannot authorize
|
|
353
|
+
// removal of that other directory.
|
|
354
|
+
await mkdir(destination, { mode: 0o700 });
|
|
355
|
+
const createdDirectory = await lstat(destination);
|
|
351
356
|
try {
|
|
352
357
|
await git([
|
|
353
358
|
"clone",
|
|
@@ -363,12 +368,31 @@ export class NodeGitWorkspace {
|
|
|
363
368
|
}
|
|
364
369
|
await git(["-C", destination, "branch", "-M", "--", localBranch]);
|
|
365
370
|
}
|
|
371
|
+
return await this.inspect(destination, "HEAD");
|
|
366
372
|
}
|
|
367
373
|
catch (error) {
|
|
368
|
-
|
|
374
|
+
try {
|
|
375
|
+
const current = await lstat(destination).catch(failure => {
|
|
376
|
+
if (isErrno(failure, "ENOENT"))
|
|
377
|
+
return undefined;
|
|
378
|
+
throw failure;
|
|
379
|
+
});
|
|
380
|
+
if (current !== undefined) {
|
|
381
|
+
if (!current.isDirectory() || current.dev !== createdDirectory.dev || current.ino !== createdDirectory.ino) {
|
|
382
|
+
throw new Error("Clone destination ownership changed; no direct deletion attempted.");
|
|
383
|
+
}
|
|
384
|
+
await rm(destination, { recursive: true });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch (cleanupError) {
|
|
388
|
+
throw new AggregateError([error, cleanupError], `Clone failed at ${destination}: ${error instanceof Error ? error.message : String(error)}. `
|
|
389
|
+
+ `Temporary compensation failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}. `
|
|
390
|
+
+ "Cleanup effects are unconfirmed; inspect this exact destination before retrying.");
|
|
391
|
+
}
|
|
392
|
+
if (error instanceof Error)
|
|
393
|
+
error.message += ` Exact temporary clone directory removed or already absent: ${destination}.`;
|
|
369
394
|
throw error;
|
|
370
395
|
}
|
|
371
|
-
return this.inspect(destination, "HEAD");
|
|
372
396
|
}
|
|
373
397
|
async ensureLocalBranch(repositoryPath, branch) {
|
|
374
398
|
try {
|
|
@@ -822,31 +846,54 @@ export class NodeGitWorkspace {
|
|
|
822
846
|
* worktree's own Git identity instead. Only call for unadopted worktrees
|
|
823
847
|
* that are safe to discard (e.g. Lane preparation compensation).
|
|
824
848
|
*/
|
|
825
|
-
async removeStrandedWorktree(path) {
|
|
826
|
-
|
|
827
|
-
if (kind === undefined)
|
|
828
|
-
return "missing";
|
|
829
|
-
if (kind === "symlink")
|
|
830
|
-
throw new Error("Stranded worktree path must not be a symbolic link.");
|
|
831
|
-
// Inspect dirty state through the worktree's own Git, bypassing the
|
|
832
|
-
// Project ownership check that fails after a catalog switch.
|
|
833
|
-
const porcelain = await git(["-C", path, "status", "--porcelain=v1", "--untracked-files=all"]);
|
|
834
|
-
if (porcelain.length > 0)
|
|
835
|
-
return "dirty";
|
|
849
|
+
async removeStrandedWorktree(path, identity) {
|
|
850
|
+
let worktreeRemoved = false;
|
|
836
851
|
try {
|
|
837
|
-
|
|
852
|
+
const expected = managedPath(resolve(identity.container), safeIdentity(identity.directory, "Project directory"));
|
|
853
|
+
if (path !== expected)
|
|
854
|
+
throw new Error("Unadopted worktree does not match its exact managed path.");
|
|
855
|
+
const kind = await pathKind(path);
|
|
856
|
+
if (kind === undefined)
|
|
857
|
+
return "missing";
|
|
858
|
+
if (kind === "symlink")
|
|
859
|
+
throw new Error("Stranded worktree path must not be a symbolic link.");
|
|
860
|
+
await canonicalContainer(identity.container, false);
|
|
861
|
+
if (!(await lstat(join(path, ".git"))).isFile()) {
|
|
862
|
+
throw new Error("Expected a linked worktree, not a standalone clone.");
|
|
863
|
+
}
|
|
864
|
+
const project = await this.inspect(path, "HEAD");
|
|
865
|
+
await assertOwnedWorktree(project, resolve(identity.container), path);
|
|
866
|
+
const branch = worktreeIdentity(identity.taskSegment, identity.roleName).branch;
|
|
867
|
+
await assertExpectedBranch(path, branch);
|
|
868
|
+
if (project.baseCommit !== identity.expectedCommit) {
|
|
869
|
+
throw new Error("Unadopted worktree HEAD changed since preparation; retained.");
|
|
870
|
+
}
|
|
871
|
+
if (!await this.inspectClean(path))
|
|
872
|
+
return "dirty";
|
|
873
|
+
// Use the worktree's proven common directory, never the changed catalog.
|
|
874
|
+
// Git still enforces locks/submodule restrictions. A Git error is not
|
|
875
|
+
// authority for direct filesystem deletion, even for unadopted work.
|
|
876
|
+
await git(["--git-dir", project.gitDirectory, "worktree", "remove", "--", path]);
|
|
877
|
+
worktreeRemoved = true;
|
|
878
|
+
if (identity.deleteBranch === true) {
|
|
879
|
+
await git(["--git-dir", project.gitDirectory, "update-ref", "-d", `refs/heads/${branch}`, identity.expectedCommit]);
|
|
880
|
+
}
|
|
881
|
+
return "removed";
|
|
838
882
|
}
|
|
839
|
-
catch {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
883
|
+
catch (error) {
|
|
884
|
+
throw new Error(`Unadopted worktree cleanup failed at ${path}: ${error instanceof Error ? error.message : String(error)} `
|
|
885
|
+
+ `Confirmed worktree removal: ${worktreeRemoved}; failed-command effects remain unconfirmed. `
|
|
886
|
+
+ "No direct deletion was attempted. Inspect this exact path and its Git registration before choosing recovery.", { cause: error });
|
|
843
887
|
}
|
|
844
|
-
return "removed";
|
|
845
888
|
}
|
|
846
889
|
async removeTaskClone(input) {
|
|
847
890
|
const state = await this.inspectTaskClone(input);
|
|
848
891
|
if (state !== "clean")
|
|
849
892
|
return state;
|
|
893
|
+
if (input.expectedCommit !== undefined
|
|
894
|
+
&& await resolveRefCommit(input.path, "HEAD") !== input.expectedCommit) {
|
|
895
|
+
throw new Error(`Unadopted Task clone HEAD changed since preparation; retained at ${input.path}.`);
|
|
896
|
+
}
|
|
850
897
|
await rm(managedPath(resolve(input.container), input.directory), { recursive: true });
|
|
851
898
|
return "removed";
|
|
852
899
|
}
|
|
@@ -545,7 +545,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
545
545
|
const taskSegment = taskWorkspaceRefSegment(workspaceIdentity === undefined ? task : { ...task, workspaceIdentity });
|
|
546
546
|
const root = this.#taskWorkspaceRoot(task.id);
|
|
547
547
|
const prepared = [];
|
|
548
|
-
const
|
|
548
|
+
const createdClones = [];
|
|
549
549
|
const baselines = new Map();
|
|
550
550
|
try {
|
|
551
551
|
// Resolve every remote baseline before creating any Task clone. A later
|
|
@@ -595,7 +595,14 @@ export class FileTaskWorkspacePreparer {
|
|
|
595
595
|
})
|
|
596
596
|
: await this.git.inspect(previous.path, "HEAD");
|
|
597
597
|
if (previous === undefined)
|
|
598
|
-
|
|
598
|
+
createdClones.push({
|
|
599
|
+
project,
|
|
600
|
+
entry: {
|
|
601
|
+
projectId: project.id, directory: binding.directory, access: "write",
|
|
602
|
+
path: destination, branch: identity.branch,
|
|
603
|
+
baseRef: baseline.recordedBaseRef, baseCommit: physical.baseCommit
|
|
604
|
+
}
|
|
605
|
+
});
|
|
599
606
|
const physicalBranch = await this.git.headRef(physical.root);
|
|
600
607
|
if (physical.root !== (previous?.path ?? destination)
|
|
601
608
|
|| physicalBranch !== (previous?.branch ?? identity.branch)) {
|
|
@@ -794,16 +801,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
794
801
|
// A failed or conflicted preparation owns no durable record: drop the
|
|
795
802
|
// branches too, so a retry mints a clean identity without half-created
|
|
796
803
|
// refs behind. Adopted (already catalogued) worktrees are never touched.
|
|
797
|
-
await this.#discardUnadoptedEntries(task, taskSegment,
|
|
798
|
-
const recorded = new Set(prepared.map(({ entry }) => entry.path));
|
|
799
|
-
for (const path of createdClonePaths) {
|
|
800
|
-
if (recorded.has(path))
|
|
801
|
-
continue;
|
|
802
|
-
const removal = await this.git.removeStrandedWorktree(path);
|
|
803
|
-
if (removal === "dirty") {
|
|
804
|
-
throw new Error(`Unadopted Task clone is dirty and was retained at ${path}; inspect it and retry.`);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
804
|
+
await this.#discardUnadoptedEntries(task, taskSegment, createdClones, MAIN_WORKTREE, true, undefined, error);
|
|
807
805
|
if (activate)
|
|
808
806
|
await removeWorkspaceView(root);
|
|
809
807
|
throw error;
|
|
@@ -1125,7 +1123,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1125
1123
|
});
|
|
1126
1124
|
}
|
|
1127
1125
|
catch (error) {
|
|
1128
|
-
await this.#discardUnadoptedEntries(lockedTask, taskSegment, prepared, item.id);
|
|
1126
|
+
await this.#discardUnadoptedEntries(lockedTask, taskSegment, prepared, item.id, false, undefined, error);
|
|
1129
1127
|
throw error;
|
|
1130
1128
|
}
|
|
1131
1129
|
}
|
|
@@ -1276,7 +1274,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1276
1274
|
return workspace;
|
|
1277
1275
|
}
|
|
1278
1276
|
catch (error) {
|
|
1279
|
-
await this.#discardUnadoptedEntries(lockedTask, taskSegment, prepared, managedWorktreeName(owner), true);
|
|
1277
|
+
await this.#discardUnadoptedEntries(lockedTask, taskSegment, prepared, managedWorktreeName(owner), true, undefined, error);
|
|
1280
1278
|
throw error;
|
|
1281
1279
|
}
|
|
1282
1280
|
}
|
|
@@ -1354,48 +1352,39 @@ export class FileTaskWorkspacePreparer {
|
|
|
1354
1352
|
return "missing";
|
|
1355
1353
|
const task = requireTask(this.store, workspace.owner.taskId);
|
|
1356
1354
|
const taskSegment = this.#taskSegment(task);
|
|
1355
|
+
const root = this.#executionLaneWorkspaceRoot(task.id, workspace.owner.executionGroupId, workspace.owner.executionLaneId);
|
|
1356
|
+
if (workspace.root !== root)
|
|
1357
|
+
throw new Error("Unadopted Lane root does not match its managed owner.");
|
|
1357
1358
|
const writable = workspace.entries.filter(({ access }) => access === "write");
|
|
1358
|
-
|
|
1359
|
+
const completed = [];
|
|
1360
|
+
let target = root;
|
|
1359
1361
|
try {
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
catch (error) {
|
|
1363
|
-
// A `project migrate` between preparation and compensation switches the
|
|
1364
|
-
// catalog, so the worktree's common-dir no longer matches the Project's
|
|
1365
|
-
// current repository. Fall back to removing the stranded worktree
|
|
1366
|
-
// through its own Git identity.
|
|
1367
|
-
if (!(error instanceof Error && error.message.includes("belongs to another project"))) {
|
|
1368
|
-
throw error;
|
|
1369
|
-
}
|
|
1370
|
-
let removed = false;
|
|
1362
|
+
// This owner was never adopted. Its captured identity is sufficient to
|
|
1363
|
+
// inspect its own Git; no catalog-error matching or filesystem fallback.
|
|
1371
1364
|
for (const entry of writable) {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1365
|
+
target = entry.path;
|
|
1366
|
+
const result = await this.git.removeStrandedWorktree(entry.path, {
|
|
1367
|
+
container: root, directory: entry.directory, taskSegment,
|
|
1368
|
+
roleName: managedWorktreeName(workspace.owner), expectedCommit: entry.baseCommit,
|
|
1369
|
+
deleteBranch: true
|
|
1370
|
+
});
|
|
1371
|
+
if (result === "dirty") {
|
|
1372
|
+
if (completed.length === 0)
|
|
1373
|
+
return "dirty";
|
|
1374
|
+
throw new Error(`Unadopted Lane is dirty and retained at ${entry.path}.`);
|
|
1375
|
+
}
|
|
1376
|
+
completed.push({ path: entry.path, result });
|
|
1376
1377
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1378
|
+
target = root;
|
|
1379
|
+
await removeWorkspaceView(root);
|
|
1380
|
+
return completed.some(({ result }) => result === "removed") ? "removed" : "missing";
|
|
1379
1381
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
const result = await this.git.removeWorktree({
|
|
1386
|
-
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
1387
|
-
container: dirname(entry.path),
|
|
1388
|
-
directory: entry.directory,
|
|
1389
|
-
taskSegment,
|
|
1390
|
-
roleName: managedWorktreeName(workspace.owner),
|
|
1391
|
-
deleteBranch: true
|
|
1392
|
-
});
|
|
1393
|
-
if (result === "dirty")
|
|
1394
|
-
return "dirty";
|
|
1395
|
-
removed ||= result === "removed";
|
|
1382
|
+
catch (error) {
|
|
1383
|
+
const result = { completed, failedPath: target,
|
|
1384
|
+
remaining: writable.map(({ path }) => path).filter(path => !completed.some(done => done.path === path)) };
|
|
1385
|
+
throw Object.assign(new Error(`Unadopted Lane cleanup failed: ${errorText(error)}. Effects: ${JSON.stringify(result)}. `
|
|
1386
|
+
+ "Failed-command effects are unknown; inspect the exact target before continuing.", { cause: error }), { result });
|
|
1396
1387
|
}
|
|
1397
|
-
await removeWorkspaceView(workspace.root);
|
|
1398
|
-
return removed ? "removed" : "missing";
|
|
1399
1388
|
}
|
|
1400
1389
|
async cleanupExecutionLaneWorkspacesForWorkItem(taskId, workItemId) {
|
|
1401
1390
|
let result = "missing";
|
|
@@ -1754,7 +1743,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1754
1743
|
});
|
|
1755
1744
|
}
|
|
1756
1745
|
catch (error) {
|
|
1757
|
-
await this.#discardUnadoptedEntries(task, taskSegment, prepared, this.#reviewWorktreeName(round), existing === null, new Set([...retained.values()].map(({ entry }) => entry.path)));
|
|
1746
|
+
await this.#discardUnadoptedEntries(task, taskSegment, prepared, this.#reviewWorktreeName(round), existing === null, new Set([...retained.values()].map(({ entry }) => entry.path)), error);
|
|
1758
1747
|
throw error;
|
|
1759
1748
|
}
|
|
1760
1749
|
}
|
|
@@ -2243,28 +2232,43 @@ export class FileTaskWorkspacePreparer {
|
|
|
2243
2232
|
return this.store.getConfig().defaultWorkspace ?? process.cwd();
|
|
2244
2233
|
}
|
|
2245
2234
|
async #discardUnadoptedEntries(task, taskSegment, prepared, roleName, deleteBranch = false, adoptedPaths = new Set(this.store.listManagedWorkspaces(task.id)
|
|
2246
|
-
.flatMap((workspace) => workspace.entries.map(({ path }) => path)))) {
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2235
|
+
.flatMap((workspace) => workspace.entries.map(({ path }) => path))), originalError) {
|
|
2236
|
+
const candidates = prepared.filter(({ entry }) => entry.access === "write" && !adoptedPaths.has(entry.path));
|
|
2237
|
+
const completed = [];
|
|
2238
|
+
for (const { project, entry } of candidates) {
|
|
2239
|
+
try {
|
|
2240
|
+
const removal = roleName === MAIN_WORKTREE
|
|
2241
|
+
? await this.git.removeTaskClone({
|
|
2242
|
+
path: entry.path, container: this.#taskWorkspaceRoot(task.id),
|
|
2243
|
+
directory: entry.directory, taskSegment, branch: entry.branch,
|
|
2244
|
+
expectedCommit: entry.baseCommit
|
|
2245
|
+
})
|
|
2246
|
+
: await this.git.removeWorktree({
|
|
2247
|
+
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
2248
|
+
container: dirname(entry.path),
|
|
2249
|
+
directory: entry.directory,
|
|
2250
|
+
taskSegment,
|
|
2251
|
+
roleName,
|
|
2252
|
+
...(deleteBranch ? { deleteBranch: true } : {})
|
|
2253
|
+
});
|
|
2250
2254
|
if (removal === "dirty") {
|
|
2251
|
-
throw new Error(`Unadopted
|
|
2255
|
+
throw new Error(`Unadopted workspace is dirty and was retained at ${entry.path}.`);
|
|
2252
2256
|
}
|
|
2257
|
+
completed.push({ path: entry.path, result: removal });
|
|
2253
2258
|
this.#resourceRegistrar().markPathsDeleted([entry.path]);
|
|
2254
|
-
continue;
|
|
2255
2259
|
}
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
...(deleteBranch ? { deleteBranch: true } : {})
|
|
2263
|
-
});
|
|
2264
|
-
if (removal === "dirty") {
|
|
2265
|
-
throw new Error(`Unadopted managed worktree is dirty and was retained at ${entry.path}; inspect it and retry.`);
|
|
2260
|
+
catch (cleanupError) {
|
|
2261
|
+
const remaining = candidates.map(({ entry }) => entry.path)
|
|
2262
|
+
.filter(path => !completed.some(done => done.path === path));
|
|
2263
|
+
const result = { completed, failedPath: entry.path, remaining };
|
|
2264
|
+
throw Object.assign(new AggregateError([originalError, cleanupError], `Workspace preparation failed: ${errorText(originalError)}. Compensation failed: ${errorText(cleanupError)}. `
|
|
2265
|
+
+ `Effects: ${JSON.stringify(result)}. Remaining paths may be partially changed; inspect before retrying.`), { result });
|
|
2266
2266
|
}
|
|
2267
|
-
|
|
2267
|
+
}
|
|
2268
|
+
if (originalError instanceof Error && completed.length > 0) {
|
|
2269
|
+
// Preserve the original error class (notably a safe CAS retry), while
|
|
2270
|
+
// making completed compensation visible to the caller's existing report.
|
|
2271
|
+
originalError.message += ` Workspace compensation: ${JSON.stringify(completed)}.`;
|
|
2268
2272
|
}
|
|
2269
2273
|
}
|
|
2270
2274
|
#taskRepositoryPath(taskId, projectId) {
|
|
@@ -2653,3 +2657,6 @@ function errorCode(error) {
|
|
|
2653
2657
|
? String(error.code)
|
|
2654
2658
|
: undefined;
|
|
2655
2659
|
}
|
|
2660
|
+
function errorText(error) {
|
|
2661
|
+
return error instanceof Error ? error.message : String(error);
|
|
2662
|
+
}
|
|
@@ -43,7 +43,7 @@ export function listResourceRecords(state) {
|
|
|
43
43
|
return Object.values(state.records);
|
|
44
44
|
}
|
|
45
45
|
export function parseResourceRegistryState(value) {
|
|
46
|
-
if (
|
|
46
|
+
if (!isRecord(value)) {
|
|
47
47
|
throw new Error("Resource registry root is not an object.");
|
|
48
48
|
}
|
|
49
49
|
const record = value;
|
|
@@ -51,15 +51,12 @@ export function parseResourceRegistryState(value) {
|
|
|
51
51
|
throw new Error(`Resource registry schemaVersion is ${String(record.schemaVersion)}; `
|
|
52
52
|
+ `expected ${RESOURCE_REGISTRY_SCHEMA_VERSION}.`);
|
|
53
53
|
}
|
|
54
|
-
if (
|
|
54
|
+
if (!isRecord(record.records)) {
|
|
55
55
|
throw new Error("Resource registry records is not an object.");
|
|
56
56
|
}
|
|
57
57
|
const records = {};
|
|
58
58
|
for (const [key, entry] of Object.entries(record.records)) {
|
|
59
|
-
const parsed = parseResourceRecord(entry);
|
|
60
|
-
if (parsed === undefined) {
|
|
61
|
-
throw new Error(`Resource registry record ${key} is malformed.`);
|
|
62
|
-
}
|
|
59
|
+
const parsed = parseResourceRecord(entry, key);
|
|
63
60
|
if (parsed.id !== key) {
|
|
64
61
|
throw new Error(`Resource registry record key ${key} does not match id ${parsed.id}.`);
|
|
65
62
|
}
|
|
@@ -70,47 +67,96 @@ export function parseResourceRegistryState(value) {
|
|
|
70
67
|
records: Object.freeze(records)
|
|
71
68
|
});
|
|
72
69
|
}
|
|
73
|
-
function parseResourceRecord(value) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
function parseResourceRecord(value, key) {
|
|
71
|
+
const invalid = (field) => {
|
|
72
|
+
throw new Error(`Resource registry record ${key} is malformed at ${field}; stored evidence was not repaired.`);
|
|
73
|
+
};
|
|
74
|
+
const object = (value, field) => isRecord(value) ? value : invalid(field);
|
|
75
|
+
const text = (value, field) => {
|
|
76
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
77
|
+
invalid(field);
|
|
78
|
+
};
|
|
79
|
+
const timestamp = (value, field) => {
|
|
80
|
+
text(value, field);
|
|
81
|
+
if (!Number.isFinite(Date.parse(value)))
|
|
82
|
+
invalid(field);
|
|
83
|
+
};
|
|
84
|
+
const member = (value, choices, field) => {
|
|
85
|
+
if (typeof value !== "string" || !choices.includes(value))
|
|
86
|
+
invalid(field);
|
|
87
|
+
};
|
|
88
|
+
const optionalText = (record, fields, prefix = "") => {
|
|
89
|
+
for (const field of fields) {
|
|
90
|
+
if (record[field] !== undefined)
|
|
91
|
+
text(record[field], `${prefix}${field}`);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const record = object(value, "record");
|
|
77
95
|
if (record.schemaVersion !== RESOURCE_REGISTRY_SCHEMA_VERSION)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
invalid("schemaVersion");
|
|
97
|
+
text(record.id, "id");
|
|
98
|
+
text(record.path, "path");
|
|
99
|
+
timestamp(record.updatedAt, "updatedAt");
|
|
100
|
+
member(record.kind, ["worktree", "deployment", "runtime-artifact"], "kind");
|
|
101
|
+
member(record.disposition, ["active", "releasable", "quarantined", "deleted",
|
|
102
|
+
"retained-dirty", "retained-unowned", "retained-unproven", "cleanup-failed"], "disposition");
|
|
103
|
+
member(record.cleanliness, ["clean", "dirty", "unknown", "n/a"], "cleanliness");
|
|
104
|
+
if (!Array.isArray(record.activeRefs))
|
|
105
|
+
invalid("activeRefs");
|
|
106
|
+
record.activeRefs.forEach((ref, index) => text(ref, `activeRefs[${index}]`));
|
|
107
|
+
const owner = object(record.owner, "owner");
|
|
108
|
+
text(owner.home, "owner.home");
|
|
109
|
+
member(owner.basis, ["durable-record", "marker", "descriptor", "naming-convention", "unattributed"], "owner.basis");
|
|
110
|
+
optionalText(owner, ["projectId", "taskId", "workItemId", "reviewRoundId", "integrationAttemptId"], "owner.");
|
|
111
|
+
for (const field of ["createdAt", "lastReferencedAt"]) {
|
|
112
|
+
if (record[field] !== undefined)
|
|
113
|
+
timestamp(record[field], field);
|
|
83
114
|
}
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return undefined;
|
|
88
|
-
const owner = record.owner;
|
|
89
|
-
if (typeof owner.home !== "string" || typeof owner.basis !== "string") {
|
|
90
|
-
return undefined;
|
|
115
|
+
if (record.sizeBytes !== undefined
|
|
116
|
+
&& (typeof record.sizeBytes !== "number" || !Number.isFinite(record.sizeBytes) || record.sizeBytes < 0)) {
|
|
117
|
+
invalid("sizeBytes");
|
|
91
118
|
}
|
|
119
|
+
if (record.blocker !== undefined && typeof record.blocker !== "string")
|
|
120
|
+
invalid("blocker");
|
|
121
|
+
if (record.git !== undefined) {
|
|
122
|
+
const git = object(record.git, "git");
|
|
123
|
+
text(git.repositoryPath, "git.repositoryPath");
|
|
124
|
+
optionalText(git, ["commonDir", "branch", "head"], "git.");
|
|
125
|
+
}
|
|
126
|
+
if (record.quarantine !== undefined) {
|
|
127
|
+
const quarantine = object(record.quarantine, "quarantine");
|
|
128
|
+
text(quarantine.path, "quarantine.path");
|
|
129
|
+
text(quarantine.originalPath, "quarantine.originalPath");
|
|
130
|
+
timestamp(quarantine.movedAt, "quarantine.movedAt");
|
|
131
|
+
member(quarantine.method, ["move", "git-worktree-remove"], "quarantine.method");
|
|
132
|
+
if (quarantine.gitRestore !== undefined) {
|
|
133
|
+
const restore = object(quarantine.gitRestore, "quarantine.gitRestore");
|
|
134
|
+
text(restore.repositoryPath, "quarantine.gitRestore.repositoryPath");
|
|
135
|
+
optionalText(restore, ["branch", "head"], "quarantine.gitRestore.");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (record.cleanupReceipt !== undefined) {
|
|
139
|
+
const receipt = object(record.cleanupReceipt, "cleanupReceipt");
|
|
140
|
+
timestamp(receipt.removedAt, "cleanupReceipt.removedAt");
|
|
141
|
+
member(receipt.method, ["git-worktree-remove", "quarantine-purge", "runtime-cleanup"], "cleanupReceipt.method");
|
|
142
|
+
}
|
|
143
|
+
// Validation enforces the existing persisted contract, without defaults,
|
|
144
|
+
// filtering, or a second historical reader. Constructors own new values.
|
|
145
|
+
const parsed = record;
|
|
92
146
|
return Object.freeze({
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
...(
|
|
104
|
-
cleanliness: (record.cleanliness ?? "unknown"),
|
|
105
|
-
activeRefs: Array.isArray(record.activeRefs)
|
|
106
|
-
? Object.freeze(record.activeRefs.filter((ref) => typeof ref === "string"))
|
|
107
|
-
: Object.freeze([]),
|
|
108
|
-
disposition: record.disposition,
|
|
109
|
-
...(typeof record.blocker === "string" ? { blocker: record.blocker } : {}),
|
|
110
|
-
...(record.quarantine === undefined ? {} : { quarantine: record.quarantine }),
|
|
111
|
-
...(record.cleanupReceipt === undefined
|
|
112
|
-
? {}
|
|
113
|
-
: { cleanupReceipt: record.cleanupReceipt }),
|
|
114
|
-
updatedAt: record.updatedAt
|
|
147
|
+
...parsed,
|
|
148
|
+
owner: Object.freeze({ ...parsed.owner }),
|
|
149
|
+
activeRefs: Object.freeze([...parsed.activeRefs]),
|
|
150
|
+
...(parsed.git === undefined ? {} : { git: Object.freeze({ ...parsed.git }) }),
|
|
151
|
+
...(parsed.quarantine === undefined ? {} : { quarantine: Object.freeze({
|
|
152
|
+
...parsed.quarantine,
|
|
153
|
+
...(parsed.quarantine.gitRestore === undefined ? {} : {
|
|
154
|
+
gitRestore: Object.freeze({ ...parsed.quarantine.gitRestore })
|
|
155
|
+
})
|
|
156
|
+
}) }),
|
|
157
|
+
...(parsed.cleanupReceipt === undefined ? {} : { cleanupReceipt: Object.freeze({ ...parsed.cleanupReceipt }) })
|
|
115
158
|
});
|
|
116
159
|
}
|
|
160
|
+
function isRecord(value) {
|
|
161
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
162
|
+
}
|
|
@@ -42,7 +42,7 @@ export class SqliteResourceRegistry {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
load() {
|
|
45
|
-
const rows = this.#db.prepare(`SELECT payload FROM ${SQLITE_RESOURCE_REGISTRY_TABLE}`).all();
|
|
45
|
+
const rows = this.#db.prepare(`SELECT id, payload FROM ${SQLITE_RESOURCE_REGISTRY_TABLE}`).all();
|
|
46
46
|
if (rows.length === 0)
|
|
47
47
|
return emptyResourceRegistry();
|
|
48
48
|
const records = {};
|
|
@@ -52,14 +52,12 @@ export class SqliteResourceRegistry {
|
|
|
52
52
|
parsed = JSON.parse(row.payload);
|
|
53
53
|
}
|
|
54
54
|
catch (error) {
|
|
55
|
-
throw new Error(`SQLite resource registry has a corrupt payload: `
|
|
55
|
+
throw new Error(`SQLite resource registry record ${row.id} has a corrupt payload: `
|
|
56
56
|
+ `${error instanceof Error ? error.message : "unknown error"}.`, { cause: error });
|
|
57
57
|
}
|
|
58
58
|
const state = parseResourceRegistryState({
|
|
59
59
|
schemaVersion: RESOURCE_REGISTRY_SCHEMA_VERSION,
|
|
60
|
-
records: { [
|
|
61
|
-
? String(parsed.id)
|
|
62
|
-
: ""]: parsed }
|
|
60
|
+
records: { [row.id]: parsed }
|
|
63
61
|
});
|
|
64
62
|
Object.assign(records, state.records);
|
|
65
63
|
}
|
|
@@ -5,6 +5,7 @@ import { ACP_CONFIGURATION_ORDER, acpRunConfigurationOptions, confirmAcpConfigur
|
|
|
5
5
|
import { handshakeObservationFrom, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
|
|
6
6
|
import { JsonLineChannel, terminateProcessGroup } from "./jsonLineChannel.js";
|
|
7
7
|
import { ProviderDeliveryUnknownError, ProviderTurnRejectedError } from "./providerErrors.js";
|
|
8
|
+
import { providerDeliveryFailureFrom } from "./agentError.js";
|
|
8
9
|
/**
|
|
9
10
|
* One Agent Client Protocol Session over an Agent's stdio.
|
|
10
11
|
*
|
|
@@ -86,9 +87,24 @@ export class AcpStructuredProviderSession {
|
|
|
86
87
|
this.mirror = mirror;
|
|
87
88
|
}
|
|
88
89
|
static async open(input) {
|
|
89
|
-
|
|
90
|
+
let receivingAttemptId;
|
|
91
|
+
const channel = new JsonLineChannel(input.child, input.mirror, error => {
|
|
92
|
+
// Before session/new has answered, the opening caller owns the failure.
|
|
93
|
+
if (!session.#sessionId)
|
|
94
|
+
return;
|
|
95
|
+
input.onDiagnostic?.({
|
|
96
|
+
nativeSessionId: session.#sessionId,
|
|
97
|
+
...((receivingAttemptId ?? session.#activeAttemptId) === undefined
|
|
98
|
+
? {} : { attemptId: receivingAttemptId ?? session.#activeAttemptId }),
|
|
99
|
+
failure: providerDeliveryFailureFrom(error, { phase: "turn-reconcile", inputDisposition: "unknown" })
|
|
100
|
+
});
|
|
101
|
+
});
|
|
90
102
|
const session = new AcpStructuredProviderSession(input.child, input.exit, input.processInstanceId, channel, input.onTerminal, input.mirror);
|
|
91
|
-
channel.onMessage((message) =>
|
|
103
|
+
channel.onMessage((message) => {
|
|
104
|
+
receivingAttemptId = session.#activeAttemptId;
|
|
105
|
+
session.#receive(message);
|
|
106
|
+
receivingAttemptId = undefined;
|
|
107
|
+
});
|
|
92
108
|
channel.onClose((error) => session.#fail(error));
|
|
93
109
|
const negotiated = readAcpInitializeResult(await session.#request("initialize", acpInitializeRequest(input.clientVersion)));
|
|
94
110
|
session.#negotiated = negotiated;
|
|
@@ -463,7 +479,7 @@ export class AcpStructuredProviderSession {
|
|
|
463
479
|
if (message.id === undefined)
|
|
464
480
|
this.#notification(method, message.params);
|
|
465
481
|
else
|
|
466
|
-
void this.#serve(method, message);
|
|
482
|
+
void this.#serve(method, message).catch(error => this.#fail(error));
|
|
467
483
|
}
|
|
468
484
|
#response(message) {
|
|
469
485
|
const id = typeof message.id === "number" ? message.id : undefined;
|
|
@@ -638,8 +654,8 @@ export class AcpStructuredProviderSession {
|
|
|
638
654
|
...(error === undefined ? { result: result ?? {} } : { error })
|
|
639
655
|
});
|
|
640
656
|
}
|
|
641
|
-
catch {
|
|
642
|
-
|
|
657
|
+
catch (error) {
|
|
658
|
+
this.#fail(error instanceof Error ? error : new Error(String(error)));
|
|
643
659
|
}
|
|
644
660
|
}
|
|
645
661
|
#fail(error) {
|
|
@@ -46,6 +46,7 @@ export function createAgentEndpointFactory(start = startStructuredProviderSessio
|
|
|
46
46
|
onStarted: (value) => emit({ type: "started", value }),
|
|
47
47
|
onTerminal: (value) => emit({ type: "terminal", value }),
|
|
48
48
|
onInput: (value) => emit({ type: "input", value }),
|
|
49
|
+
onDiagnostic: (value) => emit({ type: "diagnostic", value }),
|
|
49
50
|
onGoal: (value) => emit({ type: "goal", value })
|
|
50
51
|
});
|
|
51
52
|
endpoint = new BuiltinAgentEndpoint(opened.session, configuration, opened.recoveredTerminal);
|
|
@@ -193,7 +194,9 @@ class BuiltinAgentEndpoint {
|
|
|
193
194
|
});
|
|
194
195
|
}
|
|
195
196
|
observe(value) {
|
|
196
|
-
if (value.type
|
|
197
|
+
if (value.type === "diagnostic" && value.value.nativeSessionId !== this.nativeSessionId)
|
|
198
|
+
return;
|
|
199
|
+
if (value.type !== "goal" && value.type !== "diagnostic"
|
|
197
200
|
&& (value.value.nativeSessionId !== this.nativeSessionId || value.value.conversationId !== this.conversationId))
|
|
198
201
|
return;
|
|
199
202
|
if (value.type === "goal" && value.value !== null && value.value.conversationId !== this.conversationId)
|