@zq-silk/yui 0.15.12 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/ARCHITECTURE.md +3 -2
  2. package/ARCHITECTURE.zh-CN.md +3 -2
  3. package/dist/artifacts/artifactCommitLock.js +16 -55
  4. package/dist/artifacts/taskArtifactRepository.js +3 -3
  5. package/dist/cli/agentConfigurationPicker.js +1 -1
  6. package/dist/cli/commandCatalog.js +57 -41
  7. package/dist/cli/completion.js +18 -18
  8. package/dist/cli/completionWizard.js +1 -1
  9. package/dist/cli/dynamicCompletion.js +1 -1
  10. package/dist/cli/interactionPolicy.js +7 -3
  11. package/dist/cli/invocationAuthority.js +64 -0
  12. package/dist/cli/managedDiagnostics.js +1 -1
  13. package/dist/cli/parseRepeatable.js +35 -0
  14. package/dist/cli/updatePorts.js +26 -5
  15. package/dist/cli.js +1280 -1243
  16. package/dist/commands/agentCommands.js +4 -4
  17. package/dist/commands/capabilityCommands.js +1 -1
  18. package/dist/commands/configCommands.js +6 -57
  19. package/dist/commands/configOverview.js +2 -2
  20. package/dist/commands/durableJobCommands.js +12 -6
  21. package/dist/commands/globalRoleCommands.js +53 -29
  22. package/dist/commands/grantCommands.js +0 -3
  23. package/dist/commands/jobCommands.js +1 -4
  24. package/dist/commands/operatorCommands.js +3 -3
  25. package/dist/commands/projectCommands.js +1 -10
  26. package/dist/commands/resourcesCommands.js +10 -40
  27. package/dist/commands/roleConfiguration.js +2 -6
  28. package/dist/commands/taskActivationCommands.js +4 -8
  29. package/dist/commands/taskAgentCapabilities.js +9 -0
  30. package/dist/commands/taskCommandSupport.js +155 -0
  31. package/dist/commands/taskCommandTypes.js +1 -0
  32. package/dist/commands/taskCommands.js +271 -710
  33. package/dist/commands/taskCompletionGate.js +1 -1
  34. package/dist/commands/taskExecutionCommands.js +1 -1
  35. package/dist/commands/taskFactCommands.js +325 -0
  36. package/dist/commands/taskInputCommands.js +12 -42
  37. package/dist/commands/taskIntegrationCommands.js +17 -57
  38. package/dist/commands/taskNextActionCommand.js +7 -6
  39. package/dist/commands/taskPublicationAdoptCommand.js +3 -3
  40. package/dist/commands/taskPublicationCommands.js +1 -1
  41. package/dist/commands/taskPublicationVerifyCommand.js +1 -1
  42. package/dist/commands/taskRemoteDeliveryCommand.js +2 -94
  43. package/dist/commands/taskUpstreamCommands.js +19 -8
  44. package/dist/commands/workflowCommands.js +6 -1
  45. package/dist/completion/completionInstaller.js +26 -26
  46. package/dist/completion/completionState.js +26 -26
  47. package/dist/completion/fileCompletionManager.js +6 -11
  48. package/dist/config/configCatalog.js +0 -2
  49. package/dist/config/timeZone.js +14 -0
  50. package/dist/config/yuiConfig.js +0 -29
  51. package/dist/context/runContextPack.js +2 -3
  52. package/dist/context/taskCatalog.js +3 -5
  53. package/dist/context/taskContext.js +139 -151
  54. package/dist/context/wakeRunReferences.js +11 -0
  55. package/dist/controller/agentCapabilities.js +58 -0
  56. package/dist/controller/agentRuntimeObserver.js +6 -11
  57. package/dist/controller/clientRuntime.js +22 -14
  58. package/dist/controller/controller.js +9 -22
  59. package/dist/controller/fileSchedulerStoreAdapter.js +117 -212
  60. package/dist/controller/jobClient.js +4 -4
  61. package/dist/controller/jobControl.js +60 -25
  62. package/dist/controller/jobSupervisor.js +4 -4
  63. package/dist/controller/providerRetryDelivery.js +7 -7
  64. package/dist/controller/runtime.js +51 -44
  65. package/dist/controller/runtimeEventProcessor.js +0 -5
  66. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  67. package/dist/controller/taskAgentError.js +35 -0
  68. package/dist/coordination/workMailboxQueue.js +2 -24
  69. package/dist/core/controllerClient.js +2 -2
  70. package/dist/core/fileLockOwner.js +74 -0
  71. package/dist/decision/decision.js +17 -0
  72. package/dist/doctor/doctor.js +18 -9
  73. package/dist/event/taskEvent.js +12 -0
  74. package/dist/execution/workItemExecutionProjection.js +2 -2
  75. package/dist/executor/agentCapabilityConfig.js +43 -0
  76. package/dist/executor/agentConfigurationCatalog.js +49 -11
  77. package/dist/executor/agentExecutor.js +5 -8
  78. package/dist/executor/fileRoleLaunchPlanner.js +11 -53
  79. package/dist/executor/taskAgentCapabilities.js +64 -0
  80. package/dist/integration/deliveryObligation.js +1 -69
  81. package/dist/integration/gitIntegrationService.js +60 -84
  82. package/dist/integration/integrationAttempt.js +3 -14
  83. package/dist/integration/integrationSourceApplication.js +4 -12
  84. package/dist/integration/manifestTags.js +2 -2
  85. package/dist/job/durableJob.js +4 -25
  86. package/dist/job/jobAssignmentScope.js +22 -0
  87. package/dist/job/jobOperation.js +16 -0
  88. package/dist/job/jobRunner.js +2 -1
  89. package/dist/job/stepDirectory.js +15 -0
  90. package/dist/kernel/builtinCapabilities.js +15 -11
  91. package/dist/kernel/kernelPorts.js +1 -17
  92. package/dist/lifecycle/exactRunTerminalization.js +1 -9
  93. package/dist/message/inputControlResolution.js +3 -4
  94. package/dist/message/message.js +17 -20
  95. package/dist/message/messageContinuation.js +1 -1
  96. package/dist/milestone/milestone.js +11 -0
  97. package/dist/observability/orchestrationMetrics.js +24 -1
  98. package/dist/observability/runtimeIdentity.js +1 -17
  99. package/dist/output/agentConfigurationPresentation.js +2 -0
  100. package/dist/output/timePresentation.js +1 -14
  101. package/dist/plugins/pluginService.js +12 -4
  102. package/dist/release/releaseWorkflowPorts.js +45 -96
  103. package/dist/release/runtimeRelease.js +9 -15
  104. package/dist/repository/projectMaintenanceLock.js +23 -64
  105. package/dist/repository/taskBaseFreshness.js +1 -1
  106. package/dist/repository/taskWorkspaceCoordinator.js +13 -22
  107. package/dist/repository/taskWorkspacePreparer.js +29 -25
  108. package/dist/repository/workspaceCleanupInspection.js +5 -5
  109. package/dist/resources/autoResourceGc.js +4 -77
  110. package/dist/resources/liveReferences.js +7 -54
  111. package/dist/resources/resourceDiscovery.js +5 -118
  112. package/dist/resources/resourceGc.js +233 -181
  113. package/dist/resources/resourceRegistrar.js +5 -4
  114. package/dist/resources/resourceRegistry.js +3 -37
  115. package/dist/resources/resourceRegistryStore.js +13 -36
  116. package/dist/resources/sqliteResourceRegistry.js +43 -26
  117. package/dist/review/deltaRecheck.js +1 -1
  118. package/dist/review/reviewAcceptance.js +1 -1
  119. package/dist/review/reviewDecision.js +1 -1
  120. package/dist/review/reviewRound.js +8 -7
  121. package/dist/review/taskFinalReviewContractResolution.js +1 -1
  122. package/dist/runtime/acpProtocol.js +4 -51
  123. package/dist/runtime/acpSession.js +10 -75
  124. package/dist/runtime/acpSessionConfiguration.js +1 -8
  125. package/dist/runtime/agentEndpoint.js +3 -2
  126. package/dist/runtime/agentFailureContext.js +43 -0
  127. package/dist/runtime/agentHost.js +15 -15
  128. package/dist/runtime/codexInteractiveHost.js +2 -2
  129. package/dist/runtime/index.js +0 -1
  130. package/dist/runtime/managedCaller.js +21 -1
  131. package/dist/runtime/providerErrors.js +38 -0
  132. package/dist/runtime/runtimeObservation.js +0 -5
  133. package/dist/runtime/runtimeSessionCandidate.js +2 -3
  134. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  135. package/dist/runtime/structuredProviderHost.js +4 -41
  136. package/dist/runtime/tmuxAdapters.js +6 -8
  137. package/dist/scheduler/activeRoleRunDelivery.js +7 -7
  138. package/dist/scheduler/leaderWakeupProcessor.js +19 -6
  139. package/dist/scheduler/operatorEvent.js +12 -20
  140. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  141. package/dist/scheduler/ports.js +5 -8
  142. package/dist/scheduler/roleRunLiveness.js +4 -4
  143. package/dist/scheduler/roleRunStall.js +16 -23
  144. package/dist/scheduler/taskExecutionProjection.js +25 -33
  145. package/dist/scheduler/taskObservabilityProjection.js +0 -1
  146. package/dist/scheduler/taskWake.js +5 -10
  147. package/dist/scheduler/wakeReason.js +2 -0
  148. package/dist/scheduler/wakeupQueue.js +2 -4
  149. package/dist/setup/setupCommand.js +1 -1
  150. package/dist/storage/contextRecords.js +106 -0
  151. package/dist/storage/currentTaskStore.js +1 -1
  152. package/dist/storage/homeLayout.js +13 -17
  153. package/dist/storage/migrations/agentFailureContext.js +22 -0
  154. package/dist/storage/migrations/currentInputContract.js +86 -0
  155. package/dist/storage/migrations/currentRuntimeContract.js +228 -0
  156. package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
  157. package/dist/storage/migrations/integrationContinuation.js +5 -4
  158. package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
  159. package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
  160. package/dist/storage/migrations/verificationPlanV1.js +162 -0
  161. package/dist/storage/migrations/verificationPolicy.js +74 -0
  162. package/dist/storage/migrations/workItemHistory.js +46 -0
  163. package/dist/storage/persistenceWorker.js +12 -4
  164. package/dist/storage/recordValidation.js +87 -0
  165. package/dist/storage/sqliteSchema.js +142 -1
  166. package/dist/storage/sqliteStore.js +156 -162
  167. package/dist/storage/storageSchema.js +2 -14
  168. package/dist/storage/storageVersions.js +1 -1
  169. package/dist/storage/storeRpc.js +15 -7
  170. package/dist/storage/taskStore.js +3 -7
  171. package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
  172. package/dist/task/completionReadiness.js +5 -24
  173. package/dist/task/draftPlan.js +0 -53
  174. package/dist/task/nextAction.js +7 -13
  175. package/dist/task/remoteDeliveryService.js +89 -0
  176. package/dist/task/taskActivation.js +2 -25
  177. package/dist/task/taskActivationService.js +0 -2
  178. package/dist/task/taskRecordReference.js +0 -1
  179. package/dist/task/taskSubmission.js +3 -14
  180. package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
  181. package/dist/telemetry/sqliteTelemetryStore.js +49 -49
  182. package/dist/telemetry/telemetryWiring.js +4 -3
  183. package/dist/tmux/tmuxManager.js +29 -20
  184. package/dist/verification/gateArtifact.js +15 -24
  185. package/dist/verification/gateArtifactStore.js +14 -7
  186. package/dist/verification/verificationGateService.js +29 -88
  187. package/dist/verification/verificationPlan.js +27 -72
  188. package/dist/web/assets/client/app.js +1 -1
  189. package/dist/web/assets/client/taskSurface.js +4 -4
  190. package/dist/web/webServer.js +4 -6
  191. package/dist/web/webSnapshot.js +3 -83
  192. package/dist/web/webTaskSurface.js +17 -46
  193. package/dist/workItem/workItem.js +1 -12
  194. package/docs/architecture/capabilities-and-resources.md +14 -2
  195. package/docs/architecture/capabilities-and-resources.zh-CN.md +11 -1
  196. package/docs/managed-turn-and-session-runtime.md +83 -10
  197. package/docs/managed-turn-and-session-runtime.zh-CN.md +65 -11
  198. package/docs/plugin-sdk.md +4 -2
  199. package/docs/provider-runtime.md +3 -1
  200. package/docs/provider-runtime.zh-CN.md +4 -2
  201. package/docs/release-workflow.md +190 -7
  202. package/docs/release-workflow.zh-CN.md +139 -5
  203. package/docs/roles-and-configuration.md +29 -0
  204. package/docs/roles-and-configuration.zh-CN.md +21 -0
  205. package/docs/sqlite-control-plane-design.md +16 -3
  206. package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
  207. package/docs/task-dag-semantics.md +3 -3
  208. package/docs/task-dag-semantics.zh-CN.md +2 -2
  209. package/docs/task-delivery.md +91 -5
  210. package/docs/task-delivery.zh-CN.md +72 -4
  211. package/docs/task-discovery.md +10 -11
  212. package/docs/task-discovery.zh-CN.md +8 -9
  213. package/docs/testing/verification-levels.md +145 -9
  214. package/docs/testing/verification-levels.zh-CN.md +103 -8
  215. package/package.json +1 -1
  216. package/skills/yui-leader/SKILL.md +5 -0
  217. package/skills/yui-leader/references/integration.md +9 -5
  218. package/skills/yui-leader/references/planning.md +9 -3
  219. package/skills/yui-operator/SKILL.md +15 -10
  220. package/skills/yui-runtime/SKILL.md +6 -6
  221. package/skills/yui-runtime/references/recovery.md +47 -0
  222. package/dist/agent/agentRegistry.js +0 -10
  223. package/dist/agentRun/runIdentity.js +0 -22
  224. package/dist/commands/deliveryGuardPreflight.js +0 -30
  225. package/dist/commands/taskIntegrationQueueCommands.js +0 -226
  226. package/dist/commands/taskOverviewCommand.js +0 -377
  227. package/dist/completion/completionWizard.js +0 -125
  228. package/dist/context/dispatchContext.js +0 -110
  229. package/dist/context/wakeNotification.js +0 -144
  230. package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
  231. package/dist/integration/integrationQueueEntry.js +0 -191
  232. package/dist/integration/integrationQueueService.js +0 -810
  233. package/dist/release/fakeReleasePorts.js +0 -55
  234. package/dist/runtime/sessionOwnerRegistry.js +0 -137
  235. package/dist/task/deliveryGuard.js +0 -226
  236. /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
@@ -1,19 +1,13 @@
1
1
  /**
2
2
  * Persistent Resource registry (Issue 10).
3
3
  *
4
- * The registry is GC's own state. When the Home is SQLite-backed it lives in
5
- * the `resource_registry` table inside `yui.db`; otherwise it falls back to a
6
- * JSON file at `$YUI_HOME/runtime/resource-registry/registry.json`.
4
+ * The registry lives only in `resource_registry` inside the current `yui.db`.
5
+ * This module owns its value validation and quarantine paths, not persistence.
7
6
  */
8
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
9
- import { dirname, join, resolve } from "node:path";
7
+ import { join, resolve } from "node:path";
10
8
  import { RESOURCE_REGISTRY_SCHEMA_VERSION } from "./resourceTypes.js";
11
9
  export const RESOURCE_REGISTRY_DIRECTORY = "resource-registry";
12
- export const RESOURCE_REGISTRY_FILE = "registry.json";
13
10
  export const RESOURCE_QUARANTINE_DIRECTORY = "quarantine";
14
- export function resourceRegistryPath(home) {
15
- return join(resolve(home), "runtime", RESOURCE_REGISTRY_DIRECTORY, RESOURCE_REGISTRY_FILE);
16
- }
17
11
  export function resourceQuarantineRoot(home) {
18
12
  return join(resolve(home), "runtime", RESOURCE_REGISTRY_DIRECTORY, RESOURCE_QUARANTINE_DIRECTORY);
19
13
  }
@@ -29,34 +23,6 @@ export function emptyResourceRegistry() {
29
23
  records: Object.freeze({})
30
24
  });
31
25
  }
32
- /**
33
- * Load the registry. A missing registry is an empty Home. A corrupt or
34
- * unreadable registry fails closed: GC must never invent ownership from a
35
- * corrupt registry, and silently dropping quarantine receipts could let a
36
- * resource be released twice.
37
- */
38
- export function loadResourceRegistry(home) {
39
- const path = resourceRegistryPath(home);
40
- if (!existsSync(path))
41
- return emptyResourceRegistry();
42
- let parsed;
43
- try {
44
- parsed = JSON.parse(readFileSync(path, "utf8"));
45
- }
46
- catch (error) {
47
- throw new Error(`Resource registry is corrupt or unreadable at ${path}: `
48
- + `${error instanceof Error ? error.message : "unknown error"}. `
49
- + "Fix or remove the registry file before running GC.", { cause: error });
50
- }
51
- return parseResourceRegistryState(parsed);
52
- }
53
- export function saveResourceRegistry(home, state) {
54
- const path = resourceRegistryPath(home);
55
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
56
- const temporary = `${path}.tmp-${process.pid}`;
57
- writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
58
- renameSync(temporary, path);
59
- }
60
26
  export function upsertResourceRecord(state, record) {
61
27
  return Object.freeze({
62
28
  schemaVersion: RESOURCE_REGISTRY_SCHEMA_VERSION,
@@ -1,41 +1,18 @@
1
- /**
2
- * Resource registry store abstraction (Issue 10, DB-only optimal).
3
- *
4
- * The GC engine depends on this interface, not on a specific storage backend.
5
- * When the Home is SQLite-backed the registry lives in the `resource_registry`
6
- * table inside `yui.db`; otherwise it falls back to a JSON file.
7
- */
8
- import { join } from "node:path";
9
- import { existsSync } from "node:fs";
10
- import { loadResourceRegistry, saveResourceRegistry } from "./resourceRegistry.js";
11
1
  import { SqliteResourceRegistry } from "./sqliteResourceRegistry.js";
12
- /**
13
- * JSON-file-backed registry store (File-store Homes).
14
- */
15
- export class FileResourceRegistryStore {
16
- #home;
17
- constructor(home) {
18
- this.#home = home;
19
- }
20
- load() {
21
- return loadResourceRegistry(this.#home);
22
- }
23
- save(state) {
24
- saveResourceRegistry(this.#home, state);
25
- }
26
- close() {
27
- // No persistent connection to close.
28
- }
2
+ export function createResourceRegistryStore(home) {
3
+ return new SqliteResourceRegistry(home);
29
4
  }
30
- /**
31
- * Create the appropriate registry store for a Home.
32
- *
33
- * SQLite-backed Homes use the `resource_registry` table in `yui.db`;
34
- * File-store Homes fall back to the JSON file.
5
+ /** Connections created here never survive a synchronous read/write operation.
6
+ * A borrowed Store remains the caller's responsibility. No DB handle is held
7
+ * across filesystem scans, Git subprocesses or other asynchronous GC work.
35
8
  */
36
- export function createResourceRegistryStore(home) {
37
- if (existsSync(join(home, "yui.db"))) {
38
- return new SqliteResourceRegistry(home);
9
+ export function withResourceRegistry(home, borrowed, operation) {
10
+ const store = borrowed ?? createResourceRegistryStore(home);
11
+ try {
12
+ return operation(store);
13
+ }
14
+ finally {
15
+ if (borrowed === undefined)
16
+ store.close();
39
17
  }
40
- return new FileResourceRegistryStore(home);
41
18
  }
@@ -12,16 +12,15 @@
12
12
  */
13
13
  import { existsSync } from "node:fs";
14
14
  import { join } from "node:path";
15
+ import { isDeepStrictEqual } from "node:util";
15
16
  import Database from "better-sqlite3";
16
- import { RESOURCE_REGISTRY_SCHEMA_VERSION } from "./resourceTypes.js";
17
- import { emptyResourceRegistry, parseResourceRegistryState } from "./resourceRegistry.js";
18
17
  import { migrateSqliteSchema } from "../storage/sqliteSchema.js";
18
+ import { emptyResourceRegistry, parseResourceRegistryState } from "./resourceRegistry.js";
19
+ import { RESOURCE_REGISTRY_SCHEMA_VERSION } from "./resourceTypes.js";
19
20
  export const SQLITE_RESOURCE_REGISTRY_TABLE = "resource_registry";
20
21
  /**
21
- * A SQLite-backed resource registry store. Each `save` is a single
22
- * transaction that upserts every record and removes rows that disappeared
23
- * from the in-memory state, so the on-disk table always matches the state
24
- * the GC engine computed.
22
+ * Each save applies only the caller's delta. Unrelated registrations survive;
23
+ * a changed same-record snapshot fails closed instead of overwriting ownership.
25
24
  */
26
25
  export class SqliteResourceRegistry {
27
26
  #db;
@@ -31,10 +30,16 @@ export class SqliteResourceRegistry {
31
30
  throw new Error(`SQLite database not found at ${dbPath}`);
32
31
  }
33
32
  this.#db = new Database(dbPath);
34
- this.#db.pragma("journal_mode = WAL");
35
- this.#db.pragma("foreign_keys = ON");
36
- this.#db.pragma("busy_timeout = 5000");
37
- migrateSqliteSchema(this.#db, { mode: "validate" });
33
+ try {
34
+ this.#db.pragma("journal_mode = WAL");
35
+ this.#db.pragma("foreign_keys = ON");
36
+ this.#db.pragma("busy_timeout = 5000");
37
+ migrateSqliteSchema(this.#db, { mode: "validate" });
38
+ }
39
+ catch (error) {
40
+ this.#db.close();
41
+ throw error;
42
+ }
38
43
  }
39
44
  load() {
40
45
  const rows = this.#db.prepare(`SELECT payload FROM ${SQLITE_RESOURCE_REGISTRY_TABLE}`).all();
@@ -63,7 +68,13 @@ export class SqliteResourceRegistry {
63
68
  records: Object.freeze(records)
64
69
  });
65
70
  }
66
- save(state) {
71
+ save(state, previous) {
72
+ const next = parseResourceRegistryState(state);
73
+ const before = parseResourceRegistryState(previous);
74
+ const ids = [...new Set([...Object.keys(before.records), ...Object.keys(next.records)])]
75
+ .filter(id => !isDeepStrictEqual(before.records[id], next.records[id]));
76
+ if (ids.length === 0)
77
+ return;
67
78
  const upsert = this.#db.prepare(`INSERT INTO ${SQLITE_RESOURCE_REGISTRY_TABLE}
68
79
  (id, kind, path, disposition, task_id, payload, created_at, updated_at)
69
80
  VALUES
@@ -76,11 +87,24 @@ export class SqliteResourceRegistry {
76
87
  payload = excluded.payload,
77
88
  updated_at = excluded.updated_at`);
78
89
  const remove = this.#db.prepare(`DELETE FROM ${SQLITE_RESOURCE_REGISTRY_TABLE} WHERE id = ?`);
79
- const existingRows = this.#db.prepare(`SELECT id FROM ${SQLITE_RESOURCE_REGISTRY_TABLE}`).all();
80
- const existingIds = new Set(existingRows.map((row) => row.id));
81
- const currentIds = new Set(Object.keys(state.records));
82
90
  const tx = this.#db.transaction(() => {
83
- for (const record of Object.values(state.records)) {
91
+ const read = this.#db.prepare(`SELECT payload FROM ${SQLITE_RESOURCE_REGISTRY_TABLE} WHERE id = ?`);
92
+ for (const id of ids) {
93
+ const row = read.get(id);
94
+ const current = row === undefined ? undefined : parseResourceRegistryState({
95
+ schemaVersion: RESOURCE_REGISTRY_SCHEMA_VERSION,
96
+ records: { [id]: JSON.parse(row.payload) }
97
+ }).records[id];
98
+ if (!isDeepStrictEqual(current, before.records[id])) {
99
+ throw new Error(`Resource registry record changed since inspection: ${id}. Inspect and retry.`);
100
+ }
101
+ }
102
+ for (const id of ids) {
103
+ const record = next.records[id];
104
+ if (record === undefined) {
105
+ remove.run(id);
106
+ continue;
107
+ }
84
108
  upsert.run({
85
109
  id: record.id,
86
110
  kind: record.kind,
@@ -92,20 +116,13 @@ export class SqliteResourceRegistry {
92
116
  updated_at: record.updatedAt
93
117
  });
94
118
  }
95
- for (const id of existingIds) {
96
- if (!currentIds.has(id))
97
- remove.run(id);
98
- }
99
119
  });
100
- tx();
120
+ tx.immediate();
101
121
  }
102
122
  close() {
103
123
  this.#db.close();
104
124
  }
105
- }
106
- /**
107
- * Detect whether a Home uses SQLite storage (yui.db exists).
108
- */
109
- export function isSqliteHome(home) {
110
- return existsSync(join(home, "yui.db"));
125
+ transaction(operation) {
126
+ return this.#db.transaction(operation).immediate();
127
+ }
111
128
  }
@@ -8,7 +8,7 @@ export async function assessDeltaRecheck(input) {
8
8
  const { repositoryPaths, previousRound, candidate, git } = input;
9
9
  if (previousRound.status !== "completed"
10
10
  || previousRound.reviewerRunId === undefined
11
- || (previousRound.scope ?? "work-item") !== "task") {
11
+ || previousRound.scope !== "task") {
12
12
  return {
13
13
  kind: "ineligible",
14
14
  reason: "Delta recheck requires a semantic completed Task-final ReviewRound."
@@ -20,7 +20,7 @@ export function isCompletedTaskReviewEvidence(store, round) {
20
20
  return isCompletedTaskReviewEvidenceFromRuns(round, store.listRuns(round.taskId));
21
21
  }
22
22
  export function isCompletedTaskReviewEvidenceFromRuns(round, runs) {
23
- if ((round.scope ?? "work-item") !== "task"
23
+ if (round.scope !== "task"
24
24
  || round.taskCandidate === undefined
25
25
  || round.taskCandidate.projects.length === 0) {
26
26
  return false;
@@ -4,7 +4,7 @@ import { projectReviewerAvailability } from "./reviewerAvailability.js";
4
4
  /** Read-only facts for Leader judgment; this projection never blocks an action. */
5
5
  export function projectReviewDecision(input) {
6
6
  const { store, task, roles, runs, rounds, reviewConfig, currentCandidate } = input;
7
- const taskRounds = rounds.filter((round) => (round.scope ?? "work-item") === "task");
7
+ const taskRounds = rounds.filter((round) => round.scope === "task");
8
8
  const accepted = [...taskRounds]
9
9
  .filter((round) => isCompletedTaskReviewEvidence(store, round))
10
10
  .sort(compareRoundIdentity)
@@ -10,6 +10,7 @@ export function createReviewRound(id, taskId, workItemId, candidateId, reviewerR
10
10
  taskId: requireIdentity(taskId, "Task id"),
11
11
  workItemId: requireIdentity(workItemId, "Work Item id"),
12
12
  candidateId: requireIdentity(candidateId, "Candidate id"),
13
+ scope: "work-item",
13
14
  reviewerRoleName: requireIdentity(reviewerRoleName, "Reviewer Role"),
14
15
  reviewBaseCommit: requireCommit(reviewBaseCommit, "Review base commit"),
15
16
  requestedBy: validateReviewRequestSource(requestedBy),
@@ -134,12 +135,12 @@ export function retryReviewRound(round, requestedBy, now) {
134
135
  if (round.status !== "failed") {
135
136
  throw new Error(`ReviewRound ${round.id} is not retryable from ${round.status}.`);
136
137
  }
137
- const taskScope = (round.scope ?? "work-item") === "task";
138
+ const taskScope = round.scope === "task";
138
139
  return validateReviewRound({
139
140
  schemaVersion: round.schemaVersion,
140
141
  id: round.id,
141
142
  taskId: round.taskId,
142
- ...(round.scope === undefined ? {} : { scope: round.scope }),
143
+ scope: round.scope,
143
144
  ...(taskScope
144
145
  ? {}
145
146
  : {
@@ -185,9 +186,9 @@ export function retryReviewRound(round, requestedBy, now) {
185
186
  createdAt: round.createdAt
186
187
  });
187
188
  }
188
- /** Task-final compatibility wrapper for callers that require that scope. */
189
+ /** Scope guard for the Task-final retry operation. */
189
190
  export function retryTaskReviewRound(round, requestedBy, now) {
190
- if ((round.scope ?? "work-item") !== "task") {
191
+ if (round.scope !== "task") {
191
192
  throw new Error(`Only a Task-final ReviewRound can be retried in place: ${round.id}.`);
192
193
  }
193
194
  return retryReviewRound(round, requestedBy, now);
@@ -289,7 +290,7 @@ export function validateReviewRound(round) {
289
290
  validateTaskRecordReference({ taskId: round.taskId, localId: round.id }, "reviewRound");
290
291
  requireIdentity(round.reviewerRoleName, "Reviewer Role");
291
292
  requireCommit(round.reviewBaseCommit, "Review base commit");
292
- const scope = round.scope ?? "work-item";
293
+ const scope = round.scope;
293
294
  if (scope !== "work-item" && scope !== "task") {
294
295
  throw new Error(`ReviewRound scope is invalid: ${String(round.scope)}.`);
295
296
  }
@@ -441,10 +442,10 @@ function validateReviewExecutionGroup(group, round) {
441
442
  || assignment.taskId !== round.taskId
442
443
  || assignment.reviewRoundId !== round.id
443
444
  || assignment.reviewBaseCommit !== round.reviewBaseCommit
444
- || assignment.scope !== (round.scope ?? "work-item")) {
445
+ || assignment.scope !== round.scope) {
445
446
  throw new Error(`ReviewRound ExecutionGroup provenance is invalid: ${round.id}.`);
446
447
  }
447
- if ((round.scope ?? "work-item") === "work-item"
448
+ if (round.scope === "work-item"
448
449
  && (assignment.workItemId !== round.workItemId
449
450
  || assignment.candidateId !== round.candidateId)) {
450
451
  throw new Error(`ReviewRound ExecutionGroup WorkItem target is invalid: ${round.id}.`);
@@ -14,7 +14,7 @@ export function resolveRecordedTaskFinalReviewContract(taskId, workItems, review
14
14
  contract: candidate.taskFinalReviewContract,
15
15
  source: `Candidate ${item.id}/${candidate.id}`
16
16
  }]))),
17
- ...reviewRounds.flatMap((round) => ((round.scope ?? "work-item") !== "task"
17
+ ...reviewRounds.flatMap((round) => (round.scope !== "task"
18
18
  || round.taskFinalReviewContract === undefined
19
19
  ? []
20
20
  : [{
@@ -183,58 +183,15 @@ export function readAcpConfigOptions(value) {
183
183
  export const ACP_MODE_CONFIG_ID = "mode";
184
184
  export function readAcpSessionConfiguration(value) {
185
185
  const result = asObject(value);
186
- const modern = readAcpConfigOptions(result?.configOptions);
187
- if (modern !== undefined) {
188
- return Object.freeze({ options: modern, legacyModes: false });
186
+ const options = readAcpConfigOptions(result?.configOptions);
187
+ if (options === undefined) {
188
+ throw new Error("ACP Session must report configOptions; mode-only peers are unsupported.");
189
189
  }
190
- const legacy = readAcpSessionModes(result?.modes);
191
- return Object.freeze({
192
- options: legacy === undefined ? Object.freeze([]) : Object.freeze([legacy]),
193
- legacyModes: legacy !== undefined
194
- });
195
- }
196
- /**
197
- * Convert the legacy `modes` field into the one config option it describes.
198
- *
199
- * The shape is fixed by ACP: `currentModeId` plus `availableModes`. Presenting
200
- * it as a config option keeps mode selection in one vocabulary without
201
- * pretending the Agent supports the newer method.
202
- */
203
- function readAcpSessionModes(value) {
204
- const modes = asObject(value);
205
- const currentValue = modes === null ? undefined : optionalText(modes.currentModeId);
206
- if (modes === null || currentValue === undefined)
207
- return undefined;
208
- const values = [];
209
- if (Array.isArray(modes.availableModes)) {
210
- for (const entry of modes.availableModes) {
211
- const mode = asObject(entry);
212
- const id = mode === null ? undefined : optionalText(mode.id);
213
- if (mode === null || id === undefined)
214
- continue;
215
- values.push(Object.freeze({
216
- value: id,
217
- name: optionalText(mode.name) ?? id,
218
- ...(optionalText(mode.description) === undefined
219
- ? {}
220
- : { description: optionalText(mode.description) })
221
- }));
222
- }
223
- }
224
- return Object.freeze({
225
- id: ACP_MODE_CONFIG_ID,
226
- name: "Mode",
227
- category: "mode",
228
- currentValue,
229
- options: Object.freeze(values)
230
- });
190
+ return Object.freeze({ options });
231
191
  }
232
192
  export function acpSetConfigOptionRequest(sessionId, configId, value) {
233
193
  return { sessionId, configId, value };
234
194
  }
235
- export function acpSetModeRequest(sessionId, modeId) {
236
- return { sessionId, modeId };
237
- }
238
195
  export function acpCancelNotification(sessionId) {
239
196
  return { sessionId };
240
197
  }
@@ -330,10 +287,6 @@ export function readAcpSessionUpdate(params, sessionId) {
330
287
  ? undefined
331
288
  : Object.freeze({ kind: "config-options", options });
332
289
  }
333
- case "current_mode_update": {
334
- const modeId = optionalText(update.currentModeId);
335
- return modeId === undefined ? undefined : Object.freeze({ kind: "mode", modeId });
336
- }
337
290
  case "usage_update":
338
291
  return Object.freeze({
339
292
  kind: "usage",
@@ -1,10 +1,10 @@
1
1
  import { isAbsolute } from "node:path";
2
2
  import { MAX_RUN_RESULT_OUTPUT_BYTES } from "../domain/agentResultTransport.js";
3
- import { ACP_METHOD_NOT_FOUND_CODE, acpCancelNotification, acpDeclinePermission, acpInitializeRequest, acpNewSessionRequest, acpPermissionResult, acpPermissionSummary, acpPromptRequest, acpSetConfigOptionRequest, acpSetModeRequest, acpStopReasonDetail, acpTerminalStatus, asObject, optionalText, readAcpConfigOptions, readAcpInitializeResult, readAcpPermissionRequest, readAcpSessionConfiguration, readAcpSessionUpdate, readAcpStopReason } from "./acpProtocol.js";
3
+ import { ACP_METHOD_NOT_FOUND_CODE, acpCancelNotification, acpDeclinePermission, acpInitializeRequest, acpNewSessionRequest, acpPermissionResult, acpPermissionSummary, acpPromptRequest, acpSetConfigOptionRequest, acpStopReasonDetail, acpTerminalStatus, asObject, optionalText, readAcpConfigOptions, readAcpInitializeResult, readAcpPermissionRequest, readAcpSessionConfiguration, readAcpSessionUpdate, readAcpStopReason } from "./acpProtocol.js";
4
4
  import { ACP_CONFIGURATION_ORDER, acpRunConfigurationOptions, confirmAcpConfigurationStep, describeAcpConfigurationRejections, resolveAcpConfigurationField, verifyAcpConfiguration } from "./acpSessionConfiguration.js";
5
5
  import { handshakeObservationFrom, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
6
6
  import { JsonLineChannel, terminateProcessGroup } from "./jsonLineChannel.js";
7
- import { ProviderDeliveryUnknownError, ProviderTurnRejectedError } from "./structuredProviderHost.js";
7
+ import { ProviderDeliveryUnknownError, ProviderTurnRejectedError } from "./providerErrors.js";
8
8
  /**
9
9
  * One Agent Client Protocol Session over an Agent's stdio.
10
10
  *
@@ -71,20 +71,10 @@ export class AcpStructuredProviderSession {
71
71
  * the complete list, so this is replaced wholesale and never merged.
72
72
  */
73
73
  #configOptions = [];
74
- /** Legacy writes invalidate current evidence until the peer reports it again. */
75
- #unobservedConfigIds = new Set();
76
- /**
77
- * True when this Agent described its options through the legacy `modes` field
78
- * only. Mode changes then go to `session/set_mode`, because an Agent that
79
- * never advertised config options must not receive
80
- * `session/set_config_option`.
81
- */
82
- #legacyModes = false;
83
74
  /**
84
75
  * What Yui asked this Session for and what the Agent did with it, recorded so
85
76
  * a launch reports the configuration it actually runs under rather than the
86
- * one it requested. Each entry carries how strongly it was confirmed, because
87
- * a legacy mode-only peer cannot prove more than that it accepted the call.
77
+ * one it requested. Requested changes require a reported current value.
88
78
  */
89
79
  #appliedConfiguration = [];
90
80
  constructor(child, exit, processInstanceId, channel, onTerminal, mirror) {
@@ -293,7 +283,6 @@ export class AcpStructuredProviderSession {
293
283
  #readConfiguration(result) {
294
284
  const configuration = readAcpSessionConfiguration(result);
295
285
  this.#configOptions = configuration.options;
296
- this.#legacyModes = configuration.legacyModes;
297
286
  }
298
287
  /**
299
288
  * Push this launch's requested run configuration and verify the Agent applied
@@ -342,33 +331,11 @@ export class AcpStructuredProviderSession {
342
331
  continue;
343
332
  }
344
333
  const step = resolution.step;
345
- if (this.#legacyModes)
346
- this.#unobservedConfigIds.add(step.configId);
347
- // An Agent that only ever advertised legacy `modes` has no
348
- // `session/set_config_option` handler, so mode changes must use the method
349
- // it does implement. Sending the modern method to it would fail with
350
- // method-not-found and read as the Agent refusing the value.
351
- const result = this.#legacyModes
352
- ? await this.#request("session/set_mode", acpSetModeRequest(this.#sessionId, step.value))
353
- : await this.#request("session/set_config_option", acpSetConfigOptionRequest(this.#sessionId, step.configId, step.value), (response) => {
354
- const options = readAcpConfigOptions(asObject(response)?.configOptions);
355
- if (options !== undefined)
356
- this.#configOptions = options;
357
- });
358
- if (this.#legacyModes) {
359
- // `session/set_mode` answers with no options, so the only fact available
360
- // is that the Agent accepted the call. Yui's own view is left untouched:
361
- // writing the requested value into the cached list would manufacture the
362
- // very confirmation the protocol withheld, and the final verification
363
- // would then read Yui's own assumption back as the Agent's report.
364
- outcomes.push(Object.freeze({
365
- field,
366
- configId: step.configId,
367
- value: step.value,
368
- confirmation: "acknowledged"
369
- }));
370
- continue;
371
- }
334
+ const result = await this.#request("session/set_config_option", acpSetConfigOptionRequest(this.#sessionId, step.configId, step.value), (response) => {
335
+ const options = readAcpConfigOptions(asObject(response)?.configOptions);
336
+ if (options !== undefined)
337
+ this.#configOptions = options;
338
+ });
372
339
  // The answer is the whole option list, so it both confirms this step and
373
340
  // carries any change the Agent made alongside it.
374
341
  const options = readAcpConfigOptions(asObject(result)?.configOptions);
@@ -385,7 +352,7 @@ export class AcpStructuredProviderSession {
385
352
  // Every call has landed, so this is the configuration the prompt would run
386
353
  // under. Re-checking all requested values here is what catches an axis that a
387
354
  // later call reset after its own step had already been confirmed.
388
- const failures = verifyAcpConfiguration(desired, this.#configOptions, component, outcomes);
355
+ const failures = verifyAcpConfiguration(desired, this.#configOptions, component);
389
356
  if (failures.length > 0) {
390
357
  throw new Error(`ACP Session run configuration did not hold. ${failures.join(" ")}`);
391
358
  }
@@ -409,11 +376,6 @@ export class AcpStructuredProviderSession {
409
376
  * rather than as the launch's confirmed one. Reading the private field on every
410
377
  * call rather than caching a projection is what makes that true: a snapshot
411
378
  * taken at launch would keep reporting a configuration this Session has left.
412
- *
413
- * A legacy mode-only peer has no reported value to pair with its
414
- * `acknowledged` step, because Yui deliberately never wrote the requested value
415
- * into its own view. That absence is stated as `unobserved` instead of being
416
- * filled in from the request.
417
379
  */
418
380
  get runConfiguration() {
419
381
  if (this.#closed !== undefined) {
@@ -438,24 +400,8 @@ export class AcpStructuredProviderSession {
438
400
  })))
439
401
  });
440
402
  }
441
- /**
442
- * What the Agent reports for one listed axis.
443
- *
444
- * Almost always an observation: the option list ACP sends is the Agent's own
445
- * report. The exception is an axis Yui changed on a legacy peer, whose value
446
- * here still dates from the setup reply because `session/set_mode` returned
447
- * nothing to refresh it with. That entry is listed — the axis is real and its
448
- * enumeration is accurate — but its value is stale by construction, so it is
449
- * reported as unobserved rather than as the Agent's current answer.
450
- */
403
+ /** The current complete option list is the Agent's own report. */
451
404
  #axisValue(option) {
452
- if (this.#unobservedConfigIds.has(option.id)) {
453
- return Object.freeze({
454
- status: "unobserved",
455
- reason: "Yui set this axis with `session/set_mode`, which reports no "
456
- + `configuration, so the Agent last reported \`${option.currentValue}\` before that call.`
457
- });
458
- }
459
405
  return Object.freeze({ status: "observed", value: option.currentValue });
460
406
  }
461
407
  /** Read current evidence independently of the request's historical confirmation. */
@@ -631,17 +577,6 @@ export class AcpStructuredProviderSession {
631
577
  // already confirmed before any prompt was sent.
632
578
  if (update?.kind === "config-options") {
633
579
  this.#configOptions = update.options;
634
- this.#unobservedConfigIds.clear();
635
- return;
636
- }
637
- if (update?.kind === "mode") {
638
- for (const option of this.#configOptions) {
639
- if (option.category === "mode")
640
- this.#unobservedConfigIds.delete(option.id);
641
- }
642
- this.#configOptions = this.#configOptions.map((option) => option.category === "mode"
643
- ? Object.freeze({ ...option, currentValue: update.modeId })
644
- : option);
645
580
  return;
646
581
  }
647
582
  // Streamed content is Provider-visible progress, mirrored for the Turn
@@ -181,17 +181,10 @@ function resolveRequest(field, desired, options, component) {
181
181
  *
182
182
  * Only stated values are checked. An unrequested axis has no expectation to
183
183
  * violate, and `default` permission means Yui asked for nothing.
184
- *
185
- * Legacy `session/set_mode` peers answer with no option list, so their mode
186
- * cannot be re-read; `acknowledged` outcomes carry that and are excluded here
187
- * rather than being verified against a value Yui wrote into its own cache.
188
184
  */
189
- export function verifyAcpConfiguration(desired, options, component, outcomes) {
185
+ export function verifyAcpConfiguration(desired, options, component) {
190
186
  const failures = [];
191
187
  for (const field of ACP_CONFIGURATION_ORDER) {
192
- const outcome = outcomes.find((candidate) => candidate.field === field);
193
- if (outcome !== undefined && outcome.confirmation === "acknowledged")
194
- continue;
195
188
  const resolution = resolveAcpConfigurationField(field, desired, options, component);
196
189
  if (resolution.kind === "unrequested" || resolution.kind === "satisfied")
197
190
  continue;
@@ -1,8 +1,9 @@
1
- import { builtinAgentDriverRegistry } from "./builtinAgentDrivers.js";
2
1
  import { builtinAgentEndpointImplementation, requireBuiltinAgentEndpointImplementation } from "./agentEndpointIdentity.js";
2
+ import { builtinAgentDriverRegistry } from "./builtinAgentDrivers.js";
3
3
  import { CodexPreSubmissionError } from "./codexAppServerRuntime.js";
4
+ import { ProviderDeliveryUnknownError, ProviderTurnBusyError, ProviderTurnRejectedError } from "./providerErrors.js";
5
+ import { startStructuredProviderSession } from "./structuredProviderHost.js";
4
6
  export { builtinAgentEndpointImplementation } from "./agentEndpointIdentity.js";
5
- import { ProviderDeliveryUnknownError, ProviderTurnBusyError, ProviderTurnRejectedError, startStructuredProviderSession } from "./structuredProviderHost.js";
6
7
  /**
7
8
  * The only built-in managed execution factory. Product/protocol codecs remain
8
9
  * private to runtime; the Host consumes the same boundary for both providers.
@@ -0,0 +1,43 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isAbsolute } from "node:path";
3
+ import { requireIdentity } from "../domain/validation.js";
4
+ import { projectAgentCapabilityConfig, validateAgentCapabilityConfig } from "../executor/agentCapabilityConfig.js";
5
+ export function configuredAgentFingerprint(agent) {
6
+ // Environment entries name bindings, never their resolved secret values.
7
+ return createHash("sha256").update(JSON.stringify({
8
+ id: agent.id, component: agent.component, adapterId: agent.adapterId,
9
+ command: agent.command, baseArgs: agent.baseArgs, environment: agent.environment
10
+ })).digest("hex");
11
+ }
12
+ export function captureAgentFailureContext(effective, agent) {
13
+ const value = effective === undefined || agent === null
14
+ ? { status: "unavailable", reason: "The failing execution did not report a complete launch configuration." }
15
+ : agent.id !== effective.agentId || agent.adapterId !== effective.adapterId || agent.component !== effective.component
16
+ ? { status: "unavailable", reason: "The configured Agent no longer matches the failing execution's implementation." }
17
+ : { status: "recorded", agentId: effective.agentId, cwd: effective.workspace.root,
18
+ config: projectAgentCapabilityConfig(effective),
19
+ agentFingerprint: configuredAgentFingerprint(agent) };
20
+ return JSON.stringify(value);
21
+ }
22
+ export function readAgentFailureContext(value) {
23
+ if (value === undefined)
24
+ throw new Error("Agent failure is missing its configuration context.");
25
+ const parsed = JSON.parse(value);
26
+ if (parsed?.status === "unavailable" && typeof parsed.reason === "string" && parsed.reason.length > 0)
27
+ return parsed;
28
+ if (parsed?.status !== "recorded" || !/^[a-f0-9]{64}$/.test(parsed.agentFingerprint)) {
29
+ throw new Error("Agent failure configuration context is invalid.");
30
+ }
31
+ if (Object.keys(parsed).some(key => !["status", "agentId", "cwd", "config", "agentFingerprint"].includes(key))) {
32
+ throw new Error("Agent failure context contains unsupported fields.");
33
+ }
34
+ requireIdentity(parsed.agentId, "Failure Agent id");
35
+ if (typeof parsed.cwd !== "string" || !isAbsolute(parsed.cwd) || parsed.cwd.includes("\0")) {
36
+ throw new Error("Agent failure working directory is invalid.");
37
+ }
38
+ validateAgentCapabilityConfig(parsed.config);
39
+ return parsed;
40
+ }
41
+ export function failureCapabilitiesCommand(taskId, roleName, eventId) {
42
+ return `yui task role capabilities ${taskId} ${roleName} --error ${eventId} --refresh`;
43
+ }