@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
@@ -131,21 +131,10 @@ export function decideSubmissionRouting(input) {
131
131
  /**
132
132
  * Normalize a possibly-absent submission intent to its effective value.
133
133
  *
134
- * An absent intent is `discuss` (task-32 §2.5: "默认/旧客户端都按 discuss"), so
135
- * every old client and every message saved before the field existed keeps its
136
- * historical Leader-waking behaviour. Body text is never inspected.
137
- *
138
- * A legacy `--wake-policy none` carried the "save only, do not wake" meaning that
139
- * `record` now names, so a caller that supplied only that older signal maps to
140
- * `record` rather than silently becoming a Leader-waking `discuss`. An explicit
141
- * intent always wins over the wake-policy shorthand.
134
+ * The current default is `discuss`. Body text is never inspected.
142
135
  */
143
- export function normalizeSubmissionIntent(intent, wakePolicy) {
144
- if (intent !== undefined)
145
- return intent;
146
- if (wakePolicy === "none")
147
- return "record";
148
- return "discuss";
136
+ export function normalizeSubmissionIntent(intent) {
137
+ return intent ?? "discuss";
149
138
  }
150
139
  /**
151
140
  * Whether a retry aims at the same target the key was first bound to (§2.3). An
@@ -0,0 +1,29 @@
1
+ /** Runs on the existing persistence worker's connection, never the ingress
2
+ * thread. Telemetry keeps its existing idempotent keys and aggregate triggers;
3
+ * it neither bumps Task revisions nor creates durable RPC outbox records.
4
+ */
5
+ export function writeTelemetryBatch(db, entries, runCap) {
6
+ if (!Number.isSafeInteger(runCap) || runCap < 1 || entries.length > 256) {
7
+ throw new Error("Telemetry batch exceeds its bounded write contract.");
8
+ }
9
+ const upsert = db.prepare(`INSERT INTO telemetry (task_id, role_name, turn_id, progress_id, sequence, payload, received_at)
10
+ VALUES (?, ?, ?, ?, ?, ?, ?)
11
+ ON CONFLICT(task_id, role_name, turn_id, progress_id) DO UPDATE SET
12
+ sequence = excluded.sequence, payload = excluded.payload, received_at = excluded.received_at
13
+ WHERE excluded.received_at >= telemetry.received_at`);
14
+ const cap = db.prepare(`DELETE FROM telemetry WHERE task_id = ? AND turn_id = ?
15
+ AND (task_id, role_name, turn_id, progress_id) NOT IN (
16
+ SELECT task_id, role_name, turn_id, progress_id FROM telemetry
17
+ WHERE task_id = ? AND turn_id = ?
18
+ ORDER BY received_at DESC, COALESCE(sequence, -1) DESC, progress_id ASC LIMIT ?
19
+ )`);
20
+ db.transaction(() => {
21
+ const runs = new Map();
22
+ for (const entry of entries) {
23
+ upsert.run(entry.taskId, entry.roleName, entry.runId, entry.progressId, entry.sequence ?? null, JSON.stringify(entry.payload), entry.receivedAt);
24
+ runs.set(`${entry.taskId}\0${entry.runId}`, entry);
25
+ }
26
+ for (const { taskId, runId } of runs.values())
27
+ cap.run(taskId, runId, taskId, runId, runCap);
28
+ })();
29
+ }
@@ -1,15 +1,17 @@
1
+ import Database from "better-sqlite3";
1
2
  import { existsSync } from "node:fs";
2
3
  import { join } from "node:path";
3
- import Database from "better-sqlite3";
4
- import { DEFAULT_RUN_CAP, DEFAULT_TERMINAL_KEEP } from "./telemetryConfig.js";
5
- import { migrateSqliteSchema } from "../storage/sqliteSchema.js";
6
4
  import { CURRENT_DATABASE_FILENAME as COMMITTED_DATABASE_FILENAME } from "../storage/currentTaskStore.js";
5
+ import { migrateSqliteSchema } from "../storage/sqliteSchema.js";
6
+ import { AsyncTaskStoreClient } from "../storage/storeRpc.js";
7
+ import { DEFAULT_RUN_CAP, DEFAULT_TERMINAL_KEEP } from "./telemetryConfig.js";
7
8
  /**
8
9
  * Default sidecar implementation: the telemetry tables live inside the Home's
9
10
  * authoritative `yui.db`, so a Home has one durable database
10
11
  * has one file to manage, back up, and migrate. The store opens its own
11
- * connection to that file: the write path is a single serialized writer (the
12
- * "WAL worker") and stays isolated from the business store's connection.
12
+ * connection for cold synchronous reads/explicit retention. Ingestion writes
13
+ * use the existing persistence-worker RPC, shared with the Controller when
14
+ * available; standalone consumers lazily own and close their own client.
13
15
  *
14
16
  * Writes are best-effort and never block the Controller event loop: `observe`
15
17
  * only merges into a bounded in-memory queue; a background flush drains it in
@@ -28,6 +30,10 @@ export class SqliteTelemetryStore {
28
30
  #terminalKeep;
29
31
  #turnCap;
30
32
  #maxPending;
33
+ #home;
34
+ #writer;
35
+ #ownedWriter;
36
+ #flushing;
31
37
  #db = null;
32
38
  #failed = false;
33
39
  #lastError = null;
@@ -39,6 +45,8 @@ export class SqliteTelemetryStore {
39
45
  #closed = false;
40
46
  constructor(home, options = {}) {
41
47
  this.mode = options.mode ?? "on";
48
+ this.#home = home;
49
+ this.#writer = options.writer;
42
50
  this.#path = join(home, COMMITTED_DATABASE_FILENAME);
43
51
  this.#terminalKeep = options.terminalKeep ?? DEFAULT_TERMINAL_KEEP;
44
52
  this.#turnCap = options.runCap ?? DEFAULT_RUN_CAP;
@@ -48,8 +56,7 @@ export class SqliteTelemetryStore {
48
56
  observe(entry) {
49
57
  if (this.#closed)
50
58
  return;
51
- const db = this.#ensureDb();
52
- if (db === null) {
59
+ if (this.#failed) {
53
60
  this.#dropped++;
54
61
  return;
55
62
  }
@@ -84,9 +91,14 @@ export class SqliteTelemetryStore {
84
91
  if (this.#closed)
85
92
  return;
86
93
  this.#closed = true;
87
- await this.#flushPending();
88
- this.#db?.close();
89
- this.#db = null;
94
+ try {
95
+ await this.#flushPending();
96
+ }
97
+ finally {
98
+ await this.#ownedWriter?.close();
99
+ this.#db?.close();
100
+ this.#db = null;
101
+ }
90
102
  }
91
103
  // -- TelemetryReader -----------------------------------------------------------
92
104
  count(taskId, runId) {
@@ -230,6 +242,7 @@ export class SqliteTelemetryStore {
230
242
  return this.#db;
231
243
  if (this.#failed || this.#closed)
232
244
  return null;
245
+ let opening;
233
246
  try {
234
247
  // The telemetry tables live in the Home's authoritative database; the
235
248
  // store never creates `yui.db` itself. Wiring fails closed on Homes
@@ -237,17 +250,19 @@ export class SqliteTelemetryStore {
237
250
  if (!existsSync(this.#path)) {
238
251
  throw new Error(`Telemetry database not found: ${this.#path}`);
239
252
  }
240
- const db = new Database(this.#path);
253
+ const db = opening = new Database(this.#path);
241
254
  db.pragma("journal_mode = WAL");
242
255
  db.pragma("synchronous = FULL");
243
256
  db.pragma("foreign_keys = ON");
244
- db.pragma("busy_timeout = 5000");
257
+ // Cold diagnostic reads/retention must not wait behind a semantic writer.
258
+ db.pragma("busy_timeout = 0");
245
259
  db.pragma("wal_autocheckpoint = 1000");
246
260
  migrateSqliteSchema(db, { mode: "validate" });
247
261
  this.#db = db;
248
262
  return db;
249
263
  }
250
264
  catch (error) {
265
+ opening?.close();
251
266
  this.#failed = true;
252
267
  this.#lastError = error instanceof Error ? error.message : String(error);
253
268
  return null;
@@ -263,43 +278,28 @@ export class SqliteTelemetryStore {
263
278
  });
264
279
  handle.unref?.();
265
280
  }
266
- async #flushPending() {
267
- if (this.#pending.size === 0)
268
- return;
269
- const db = this.#ensureDb();
270
- if (db === null) {
271
- this.#dropped += this.#pending.size;
272
- this.#pending.clear();
273
- return;
274
- }
275
- const batch = new Map(this.#pending);
276
- this.#pending.clear();
277
- const upsert = db.prepare(`INSERT INTO telemetry (task_id, role_name, turn_id, progress_id, sequence, payload, received_at)
278
- VALUES (?, ?, ?, ?, ?, ?, ?)
279
- ON CONFLICT(task_id, role_name, turn_id, progress_id) DO UPDATE SET
280
- sequence = excluded.sequence,
281
- payload = excluded.payload,
282
- received_at = excluded.received_at
283
- WHERE excluded.received_at >= telemetry.received_at`);
284
- try {
285
- const touchedRuns = new Map();
286
- db.transaction(() => {
287
- for (const entry of batch.values()) {
288
- upsert.run(entry.taskId, entry.roleName, entry.runId, entry.progressId, entry.sequence ?? null, JSON.stringify(entry.payload), entry.receivedAt);
289
- touchedRuns.set(`${entry.taskId}\0${entry.runId}`, {
290
- taskId: entry.taskId,
291
- runId: entry.runId
292
- });
293
- }
294
- for (const { taskId, runId } of touchedRuns.values()) {
295
- this.capRun(taskId, runId);
296
- }
297
- })();
298
- this.#applied += batch.size;
299
- }
300
- catch (error) {
301
- this.#lastError = error instanceof Error ? error.message : String(error);
302
- this.#dropped += batch.size;
281
+ #flushPending() {
282
+ this.#flushing ??= this.#drain().finally(() => { this.#flushing = undefined; });
283
+ return this.#flushing;
284
+ }
285
+ async #drain() {
286
+ while (this.#pending.size > 0) {
287
+ const batch = [];
288
+ for (const [key, entry] of this.#pending) {
289
+ this.#pending.delete(key);
290
+ batch.push(entry);
291
+ if (batch.length === 256)
292
+ break;
293
+ }
294
+ try {
295
+ const writer = this.#writer ?? (this.#ownedWriter ??= new AsyncTaskStoreClient(this.#home, { readPoolSize: 1 }));
296
+ await writer.flushTelemetry(batch, this.#turnCap);
297
+ this.#applied += batch.length;
298
+ }
299
+ catch (error) {
300
+ this.#lastError = error instanceof Error ? error.message : String(error);
301
+ this.#dropped += batch.length;
302
+ }
303
303
  }
304
304
  }
305
305
  }
@@ -1,16 +1,16 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { resolveRunCap, resolveTerminalKeep } from "./telemetryConfig.js";
4
3
  import { resolveTelemetryEnabled } from "../config/yuiConfig.js";
5
- import { SqliteTelemetryStore } from "./sqliteTelemetryStore.js";
6
4
  import { CURRENT_DATABASE_FILENAME as COMMITTED_DATABASE_FILENAME } from "../storage/currentTaskStore.js";
5
+ import { SqliteTelemetryStore } from "./sqliteTelemetryStore.js";
6
+ import { resolveRunCap, resolveTerminalKeep } from "./telemetryConfig.js";
7
7
  /**
8
8
  * Open optional telemetry from the current Home's authoritative `yui.db`.
9
9
  * Disabled telemetry performs no database work. Once opened, telemetry fails
10
10
  * isolated: a broken sink increments its dropped counter without blocking the
11
11
  * semantic lane.
12
12
  */
13
- export function openSchedulerTelemetry(home, config) {
13
+ export function openSchedulerTelemetry(home, config, writer) {
14
14
  if (!resolveTelemetryEnabled(config.telemetryEnabled))
15
15
  return null;
16
16
  const mode = "on";
@@ -21,6 +21,7 @@ export function openSchedulerTelemetry(home, config) {
21
21
  }
22
22
  const store = new SqliteTelemetryStore(home, {
23
23
  mode,
24
+ writer,
24
25
  terminalKeep: resolveTerminalKeep(config.telemetryTerminalKeep),
25
26
  runCap: resolveRunCap(config.telemetryRunCap)
26
27
  });
@@ -509,7 +509,9 @@ export class TmuxManager {
509
509
  let output;
510
510
  try {
511
511
  output = this.run([
512
- "list-panes", "-s", "-t", this.sessionName(taskId), "-F",
512
+ // list-panes takes a pane target: the colon keeps this an exact
513
+ // session lookup instead of falling back to a similarly named one.
514
+ "list-panes", "-s", "-t", `=${this.sessionName(taskId)}:`, "-F",
513
515
  `#{window_name}${formatSeparator}#{pane_dead}${formatSeparator}#{pane_dead_status}${formatSeparator}#{pane_pid}${formatSeparator}#{pane_current_command}`
514
516
  ]);
515
517
  }
@@ -865,7 +867,7 @@ export class TmuxManager {
865
867
  this.recordTaskTargets(taskId);
866
868
  for (const session of [...sessions.filter((name) => name !== taskSession), taskSession]) {
867
869
  try {
868
- this.run(["kill-session", "-t", session]);
870
+ this.run(["kill-session", "-t", `=${session}`]);
869
871
  }
870
872
  catch (error) {
871
873
  if (!isExplicitlyAbsentTmuxSession(error))
@@ -882,7 +884,7 @@ export class TmuxManager {
882
884
  await this.recordTaskTargetsAsync(taskId);
883
885
  for (const session of [...sessions.filter((name) => name !== taskSession), taskSession]) {
884
886
  try {
885
- await this.runAsync(["kill-session", "-t", session]);
887
+ await this.runAsync(["kill-session", "-t", `=${session}`]);
886
888
  }
887
889
  catch (error) {
888
890
  if (!isExplicitlyAbsentTmuxSession(error))
@@ -897,11 +899,23 @@ export class TmuxManager {
897
899
  }
898
900
  killRole(taskId, roleName) {
899
901
  this.recordRoleTarget(taskId, roleName);
900
- this.run(["kill-window", "-t", this.target(taskId, roleName)]);
902
+ try {
903
+ this.run(["kill-window", "-t", this.exactTarget(taskId, roleName)]);
904
+ }
905
+ catch (error) {
906
+ if (!isExplicitlyAbsentTmuxSession(error))
907
+ throw error;
908
+ }
901
909
  }
902
910
  async killRoleAsync(taskId, roleName) {
903
911
  this.recordRoleTarget(taskId, roleName);
904
- await this.runAsync(["kill-window", "-t", this.target(taskId, roleName)]);
912
+ try {
913
+ await this.runAsync(["kill-window", "-t", this.exactTarget(taskId, roleName)]);
914
+ }
915
+ catch (error) {
916
+ if (!isExplicitlyAbsentTmuxSession(error))
917
+ throw error;
918
+ }
905
919
  }
906
920
  renameRole(taskId, oldRoleName, newRoleName) {
907
921
  const target = this.target(taskId, oldRoleName);
@@ -916,7 +930,7 @@ export class TmuxManager {
916
930
  }
917
931
  hasSession(taskId) {
918
932
  try {
919
- this.run(["has-session", "-t", this.sessionName(taskId)]);
933
+ this.run(["has-session", "-t", `=${this.sessionName(taskId)}`]);
920
934
  return true;
921
935
  }
922
936
  catch (error) {
@@ -927,7 +941,7 @@ export class TmuxManager {
927
941
  }
928
942
  async hasSessionAsync(taskId) {
929
943
  try {
930
- await this.runAsync(["has-session", "-t", this.sessionName(taskId)]);
944
+ await this.runAsync(["has-session", "-t", `=${this.sessionName(taskId)}`]);
931
945
  return true;
932
946
  }
933
947
  catch (error) {
@@ -941,7 +955,7 @@ export class TmuxManager {
941
955
  return [];
942
956
  try {
943
957
  return this.run([
944
- "list-windows", "-t", this.sessionName(taskId), "-F", "#{window_name}"
958
+ "list-windows", "-t", `=${this.sessionName(taskId)}`, "-F", "#{window_name}"
945
959
  ]).split("\n").map((name) => name.trim()).filter(Boolean);
946
960
  }
947
961
  catch (error) {
@@ -953,7 +967,7 @@ export class TmuxManager {
953
967
  async sessionWindowNamesAsync(taskId) {
954
968
  try {
955
969
  const names = (await this.runAsync([
956
- "list-windows", "-t", this.sessionName(taskId), "-F", "#{window_name}"
970
+ "list-windows", "-t", `=${this.sessionName(taskId)}`, "-F", "#{window_name}"
957
971
  ])).split("\n").map((name) => name.trim()).filter(Boolean);
958
972
  return { exists: true, names };
959
973
  }
@@ -1141,16 +1155,12 @@ function writableClientSessionPrefix(roleName) {
1141
1155
  function writableLeaseMatchesRole(sessionName, roleName) {
1142
1156
  if (!sessionName.startsWith(WRITABLE_CLIENT_SESSION_PREFIX))
1143
1157
  return false;
1144
- if (roleName === undefined)
1145
- return true;
1146
- if (sessionName.startsWith(HOST_WRITABLE_CLIENT_SESSION_PREFIX))
1158
+ if (/^yui-writer-host-[a-f0-9]{24}$/u.test(sessionName))
1147
1159
  return true;
1148
- if (sessionName.startsWith(ROLE_WRITABLE_CLIENT_SESSION_PREFIX)) {
1149
- return sessionName.startsWith(writableClientSessionPrefix(roleName));
1160
+ if (/^yui-writer-role-[a-f0-9]{24}-[a-f0-9]{24}$/u.test(sessionName)) {
1161
+ return roleName === undefined || sessionName.startsWith(writableClientSessionPrefix(roleName));
1150
1162
  }
1151
- // Conservative compatibility for writer leases created before Role-scoped
1152
- // lease names existed.
1153
- return true;
1163
+ throw runtimeError(`Unverified tmux writer lease: ${sessionName}. Preserve it and inspect its owner before retrying.`);
1154
1164
  }
1155
1165
  function writableClientRowsContainMatch(clients, taskSession, roleName, excludedLease, formatSeparator, encodedSeparator) {
1156
1166
  return clients.split("\n").some((line) => {
@@ -1169,9 +1179,8 @@ function writableClientRowsContainMatch(clients, taskSession, roleName, excluded
1169
1179
  || readOnly !== "0") {
1170
1180
  return false;
1171
1181
  }
1172
- // Current Yui clients publish a lease before attach. A direct or legacy
1173
- // writable tmux client has no Role identity, so conservatively fence every
1174
- // Role in that host.
1182
+ // A direct writable tmux client has no Role identity and fences the host.
1183
+ // A Yui lease must instead prove its exact current scope.
1175
1184
  return !sessionName.startsWith(WRITABLE_CLIENT_SESSION_PREFIX)
1176
1185
  || writableLeaseMatchesRole(sessionName, roleName);
1177
1186
  });
@@ -4,10 +4,11 @@ import { requireIdentity, requirePositiveInteger, requireText, requireTimestamp
4
4
  /**
5
5
  * Issue 08: the reusable exact-SHA gate evidence record.
6
6
  *
7
- * A GateArtifact binds one gate run to its full identity tuple: Project,
7
+ * A GateArtifact is the latest cached proof for its full identity tuple: Project,
8
8
  * candidate commit, VerificationPlan digest, toolchain digest, and (for L2)
9
- * the source/base/target boundary. The same tuple reuses the same successful
10
- * artifact; any tuple change invalidates it. Artifacts are stored in the
9
+ * the source/base/target boundary. Fresh execution withdraws the previous
10
+ * success; a failed or incomplete latest result is not reusable. Job and
11
+ * Integration records own execution history. Artifacts are stored in the
11
12
  * SQLite `gate_artifacts` / `gate_artifact_logs` tables, backend-neutral
12
13
  * through the GateArtifactStorePort.
13
14
  */
@@ -40,12 +41,11 @@ export function createGateArtifact(identity, metadata, now) {
40
41
  planVersion: requireText(metadata.planVersion, "GateArtifact planVersion"),
41
42
  planDigest: identity.planDigest,
42
43
  toolchainDigest: identity.toolchainDigest,
43
- ...(identity.boundary === undefined ? {} : { boundary: identity.boundary }),
44
+ boundary: identity.boundary,
44
45
  steps: Object.freeze([]),
45
46
  generator: requireText(metadata.generator, "GateArtifact generator"),
46
47
  status: "incomplete",
47
48
  outcome: "unknown",
48
- potentialReuseCount: 0,
49
49
  reuseCount: 0,
50
50
  createdAt: timestamp,
51
51
  lastUsedAt: timestamp
@@ -67,16 +67,7 @@ export function completeGateArtifact(artifact, steps, outcome, now) {
67
67
  lastUsedAt: timestamp
68
68
  });
69
69
  }
70
- /** Record a shadow potential-reuse observation (record mode). */
71
- export function recordGateArtifactPotentialReuse(artifact, now) {
72
- validateGateArtifact(artifact);
73
- return validateGateArtifact({
74
- ...artifact,
75
- potentialReuseCount: artifact.potentialReuseCount + 1,
76
- lastUsedAt: now.toISOString()
77
- });
78
- }
79
- /** Record an actual reuse (reuse/enforce mode). */
70
+ /** Record an actual reuse of complete successful evidence. */
80
71
  export function recordGateArtifactReuse(artifact, now) {
81
72
  validateGateArtifact(artifact);
82
73
  if (artifact.status !== "complete" || artifact.outcome !== "succeeded") {
@@ -93,7 +84,7 @@ export function validateGateArtifact(artifact) {
93
84
  throw new Error(`GateArtifact must use schemaVersion ${GATE_ARTIFACT_SCHEMA_VERSION}.`);
94
85
  }
95
86
  requireIdentity(artifact.projectId, "GateArtifact projectId");
96
- if (artifact.level !== "L1" && artifact.level !== "L2") {
87
+ if (artifact.level !== "L2") {
97
88
  throw new Error(`GateArtifact level is invalid: ${String(artifact.level)}.`);
98
89
  }
99
90
  requireCommit(artifact.commit, "GateArtifact commit");
@@ -106,17 +97,15 @@ export function validateGateArtifact(artifact) {
106
97
  commit: artifact.commit,
107
98
  planDigest: artifact.planDigest,
108
99
  toolchainDigest: artifact.toolchainDigest,
109
- ...(artifact.boundary === undefined ? {} : { boundary: artifact.boundary })
100
+ boundary: artifact.boundary
110
101
  })) {
111
102
  throw new Error("GateArtifact key does not match its identity tuple.");
112
103
  }
113
- if (artifact.level === "L2" && artifact.boundary === undefined) {
104
+ if (artifact.boundary === undefined) {
114
105
  throw new Error("An L2 GateArtifact requires a target boundary.");
115
106
  }
116
- if (artifact.boundary !== undefined) {
117
- requireText(artifact.boundary.targetRef, "GateArtifact boundary targetRef");
118
- requireCommit(artifact.boundary.baseHead, "GateArtifact boundary baseHead");
119
- }
107
+ requireText(artifact.boundary.targetRef, "GateArtifact boundary targetRef");
108
+ requireCommit(artifact.boundary.baseHead, "GateArtifact boundary baseHead");
120
109
  if (!["incomplete", "complete"].includes(artifact.status)) {
121
110
  throw new Error(`GateArtifact status is invalid: ${String(artifact.status)}.`);
122
111
  }
@@ -150,7 +139,8 @@ export function validateGateArtifact(artifact) {
150
139
  throw new Error(`GateArtifact step ${step.name} logBytes is invalid.`);
151
140
  }
152
141
  }
153
- requirePositiveInteger(artifact.potentialReuseCount + 1, "GateArtifact potentialReuseCount");
142
+ if (Object.hasOwn(artifact, "potentialReuseCount"))
143
+ throw new Error("GateArtifact contains retired shadow metrics.");
154
144
  requirePositiveInteger(artifact.reuseCount + 1, "GateArtifact reuseCount");
155
145
  requireTimestamp(artifact.createdAt, "GateArtifact createdAt");
156
146
  requireTimestamp(artifact.lastUsedAt, "GateArtifact lastUsedAt");
@@ -185,7 +175,8 @@ export function verifyGateArtifactLogs(artifact, logs) {
185
175
  });
186
176
  }
187
177
  export function isReusableGateArtifact(artifact) {
188
- return artifact.status === "complete" && artifact.outcome === "succeeded";
178
+ return artifact.status === "complete" && artifact.outcome === "succeeded"
179
+ && artifact.steps.length > 0 && artifact.steps.every(step => step.outcome === "passed" && step.exitCode === 0 && step.signal === null && !step.timedOut);
189
180
  }
190
181
  function requireCommit(value, label) {
191
182
  const normalized = requireText(value, label).toLowerCase();
@@ -30,19 +30,26 @@ export function findGateArtifact(store, identity) {
30
30
  * commit, plan digest, toolchain digest, and target ref. Unlike
31
31
  * {@link findGateArtifact} the base head is not part of the match: a release
32
32
  * consumes the gate that proved the exact frozen tree, regardless of which
33
- * base it integrated onto. Logs are verified before returning.
33
+ * base it integrated onto. The newest recorded matching result governs:
34
+ * failure/incompleteness/corruption must not fall through to an older success.
35
+ * Logs are verified before returning.
34
36
  */
35
37
  export async function findL2ArtifactForCommit(store, query) {
36
- const artifacts = store.findL2GateArtifactsForCommit(query);
37
- for (const artifact of artifacts) {
38
+ const recordedAt = (artifact) => Date.parse(artifact.completedAt ?? artifact.createdAt);
39
+ const artifacts = [...store.findL2GateArtifactsForCommit(query)]
40
+ .sort((left, right) => recordedAt(right) - recordedAt(left));
41
+ const latest = artifacts[0];
42
+ if (latest === undefined)
43
+ return null;
44
+ for (const artifact of artifacts.filter(value => recordedAt(value) === recordedAt(latest))) {
38
45
  if (!isReusableGateArtifact(artifact))
39
- continue;
46
+ return null;
40
47
  const logs = store.getGateArtifactLogs(artifact.key);
41
48
  const verification = verifyGateArtifactLogs(artifact, logs);
42
- if (verification.ok)
43
- return artifact;
49
+ if (!verification.ok)
50
+ return null;
44
51
  }
45
- return null;
52
+ return latest;
46
53
  }
47
54
  /**
48
55
  * Read each step's source log, hash it, and return the completed step