@zq-silk/yui 0.15.7 → 0.15.8

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 (281) hide show
  1. package/ARCHITECTURE.md +192 -399
  2. package/README.md +127 -1149
  3. package/dist/agent/adapterCatalog.js +15 -2
  4. package/dist/agent/agent.js +23 -3
  5. package/dist/agent/argumentPolicy.js +7 -1
  6. package/dist/agent/connectionPlan.js +62 -0
  7. package/dist/agent/executionComponents.js +158 -0
  8. package/dist/agent/launchEnvironment.js +31 -3
  9. package/dist/agent/managedRuntimeEnvironment.js +3 -5
  10. package/dist/{turn/turn.js → agentRun/agentRun.js} +166 -109
  11. package/dist/{turn/turnIdentity.js → agentRun/runIdentity.js} +4 -4
  12. package/dist/brief/taskBrief.js +12 -0
  13. package/dist/cli/agentConfigurationPicker.js +13 -0
  14. package/dist/cli/commandCatalog.js +157 -70
  15. package/dist/cli/interactionCandidates.js +5 -5
  16. package/dist/cli/interactionPolicy.js +38 -8
  17. package/dist/cli/invocationRouter.js +1 -1
  18. package/dist/cli/managedDiagnostics.js +28 -0
  19. package/dist/cli/operatorWizard.js +1 -7
  20. package/dist/cli/roleOptionOrder.js +27 -0
  21. package/dist/cli/roleWizard.js +50 -14
  22. package/dist/cli/updateOrchestrator.js +1 -1
  23. package/dist/cli/updatePorts.js +3 -4
  24. package/dist/cli.js +178 -95
  25. package/dist/commands/agentCommands.js +72 -14
  26. package/dist/commands/capabilityCommands.js +9 -6
  27. package/dist/commands/configCommands.js +20 -20
  28. package/dist/commands/deliveryGuardPreflight.js +2 -2
  29. package/dist/commands/executionAuditCommands.js +24 -24
  30. package/dist/commands/globalRoleCommands.js +1 -1
  31. package/dist/commands/grantCommands.js +4 -4
  32. package/dist/commands/operatorCommands.js +1 -7
  33. package/dist/commands/projectCommands.js +4 -4
  34. package/dist/commands/resourcesCommands.js +2 -2
  35. package/dist/commands/roleConfiguration.js +25 -5
  36. package/dist/commands/roleRuntimeGuard.js +4 -5
  37. package/dist/commands/sessionCommands.js +3 -7
  38. package/dist/commands/taskActivationCommands.js +259 -0
  39. package/dist/commands/taskActor.js +28 -49
  40. package/dist/commands/taskCommands.js +1188 -710
  41. package/dist/commands/taskContextCommand.js +39 -583
  42. package/dist/commands/taskExecutionCommands.js +32 -32
  43. package/dist/commands/taskInputCommands.js +40 -104
  44. package/dist/commands/taskIntegrationCommands.js +3 -2
  45. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  46. package/dist/commands/taskNextActionCommand.js +8 -8
  47. package/dist/commands/taskOverviewCommand.js +33 -45
  48. package/dist/commands/taskRemoteDeliveryCommand.js +2 -2
  49. package/dist/commands/taskRoleRuntimeStatus.js +133 -102
  50. package/dist/commands/telemetryCommands.js +36 -38
  51. package/dist/config/configCatalog.js +4 -4
  52. package/dist/config/yuiConfig.js +8 -8
  53. package/dist/context/contextSnapshot.js +10 -10
  54. package/dist/context/dispatchContext.js +11 -11
  55. package/dist/context/roleSessionContext.js +6 -3
  56. package/dist/context/{turnContextPack.js → runContextPack.js} +146 -81
  57. package/dist/context/{turnInputContract.js → runInputContract.js} +73 -60
  58. package/dist/context/sessionBootstrapManifest.js +21 -2
  59. package/dist/context/sourceRunContext.js +30 -0
  60. package/dist/context/taskContext.js +458 -0
  61. package/dist/context/wakeNotification.js +27 -27
  62. package/dist/controller/agentRuntimeObserver.js +21 -24
  63. package/dist/controller/capabilityBridge.js +17 -6
  64. package/dist/controller/clientRuntime.js +65 -92
  65. package/dist/controller/controller.js +114 -188
  66. package/dist/controller/fileSchedulerStoreAdapter.js +787 -887
  67. package/dist/controller/jobControl.js +54 -85
  68. package/dist/controller/resourceInventory.js +8 -27
  69. package/dist/controller/resourceInventoryLinux.js +12 -13
  70. package/dist/controller/runtime.js +529 -479
  71. package/dist/controller/runtimeEventInbox.js +55 -25
  72. package/dist/controller/runtimeEventProcessor.js +22 -31
  73. package/dist/controller/{runtimeHookTurnFence.js → runtimeHookRunFence.js} +91 -115
  74. package/dist/controller/runtimeLaunchCoordinator.js +80 -426
  75. package/dist/controller/runtimeObservationHook.js +14 -18
  76. package/dist/controller/sessionNotify.js +16 -24
  77. package/dist/controller/sessionOwnerReconciliation.js +168 -50
  78. package/dist/controller/structuredProviderObservation.js +138 -99
  79. package/dist/coordination/workMailbox.js +3 -3
  80. package/dist/coordination/workMailboxQueue.js +36 -33
  81. package/dist/core/boundedRpc.js +8 -1
  82. package/dist/core/controllerClient.js +20 -1
  83. package/dist/core/controllerServer.js +4 -4
  84. package/dist/doctor/doctor.js +13 -2
  85. package/dist/domain/agentResultTransport.js +9 -9
  86. package/dist/execution/codexThreadNaming.js +2 -8
  87. package/dist/execution/executionHealth.js +51 -63
  88. package/dist/execution/reviewMainRun.js +137 -0
  89. package/dist/execution/workItemExecution.js +28 -29
  90. package/dist/execution/workItemExecutionProjection.js +99 -107
  91. package/dist/execution/workItemMainRun.js +141 -0
  92. package/dist/executor/agentAdapter.js +227 -20
  93. package/dist/executor/agentConfigurationCatalog.js +126 -4
  94. package/dist/executor/agentConfigurationProbe.js +162 -4
  95. package/dist/executor/agentExecutor.js +79 -78
  96. package/dist/executor/effectiveLaunch.js +105 -18
  97. package/dist/executor/executorRegistry.js +29 -44
  98. package/dist/executor/fileRoleLaunchPlanner.js +229 -154
  99. package/dist/executor/workspacePreflightClassification.js +16 -16
  100. package/dist/grant/capabilityGrant.js +6 -3
  101. package/dist/input/inputRequest.js +12 -10
  102. package/dist/integration/gitIntegrationService.js +4 -11
  103. package/dist/integration/integrationQueueService.js +4 -4
  104. package/dist/interaction/operatorPresentation.js +1 -1
  105. package/dist/kernel/builtinCapabilities.js +247 -12
  106. package/dist/kernel/capabilityRegistry.js +64 -18
  107. package/dist/kernel/instanceHost.js +12 -1
  108. package/dist/kernel/kernelPorts.js +2 -2
  109. package/dist/lifecycle/canonicalLifecycleEvent.js +44 -49
  110. package/dist/lifecycle/exactRunTerminalization.js +449 -0
  111. package/dist/message/message.js +62 -6
  112. package/dist/message/messageContinuation.js +204 -0
  113. package/dist/observability/executionAudit.js +70 -72
  114. package/dist/observability/faultClassification.js +2 -2
  115. package/dist/observability/orchestrationMetrics.js +8 -8
  116. package/dist/operator/operatorSessionHistory.js +1 -7
  117. package/dist/output/agentConfigurationPresentation.js +8 -3
  118. package/dist/output/agentRunConfigurationPresentation.js +128 -0
  119. package/dist/output/rolePresentation.js +54 -3
  120. package/dist/plugins/pluginChild.js +104 -0
  121. package/dist/plugins/pluginIntent.js +26 -0
  122. package/dist/plugins/pluginInterpreter.js +43 -0
  123. package/dist/plugins/pluginPackage.js +101 -0
  124. package/dist/plugins/pluginProcess.js +112 -0
  125. package/dist/plugins/pluginService.js +380 -0
  126. package/dist/profile/agentProfile.js +1 -1
  127. package/dist/repository/gitWorkspace.js +26 -4
  128. package/dist/repository/project.js +19 -4
  129. package/dist/repository/taskBaseFreshness.js +13 -13
  130. package/dist/repository/taskWorkspaceCoordinator.js +20 -27
  131. package/dist/repository/taskWorkspacePreparer.js +344 -83
  132. package/dist/resources/autoResourceGc.js +3 -3
  133. package/dist/resources/liveReferences.js +3 -3
  134. package/dist/resources/projectResource.js +123 -0
  135. package/dist/resources/projectResourceService.js +421 -0
  136. package/dist/resources/resourceDiscovery.js +6 -6
  137. package/dist/resources/resourceGc.js +1 -1
  138. package/dist/resources/resourceRegistrar.js +1 -1
  139. package/dist/resources/resourceTypes.js +1 -1
  140. package/dist/review/deltaRecheck.js +3 -3
  141. package/dist/review/reviewAcceptance.js +16 -16
  142. package/dist/review/reviewDecision.js +7 -7
  143. package/dist/review/reviewRound.js +21 -20
  144. package/dist/review/reviewerAvailability.js +2 -2
  145. package/dist/role/role.js +51 -7
  146. package/dist/role/taskRoleUpdate.js +30 -0
  147. package/dist/runtime/acpProtocol.js +425 -0
  148. package/dist/runtime/acpSession.js +731 -0
  149. package/dist/runtime/acpSessionConfiguration.js +260 -0
  150. package/dist/runtime/agentDriver.js +30 -11
  151. package/dist/runtime/agentEndpoint.js +278 -0
  152. package/dist/runtime/agentEndpointIdentity.js +86 -0
  153. package/dist/runtime/agentEndpointOwnership.js +239 -0
  154. package/dist/runtime/agentError.js +2 -10
  155. package/dist/runtime/agentHost.js +565 -314
  156. package/dist/runtime/agentRunConfiguration.js +258 -0
  157. package/dist/runtime/builtinAgentDrivers.js +134 -18
  158. package/dist/runtime/builtinAgentErrorMappers.js +55 -3
  159. package/dist/runtime/builtinTranscriptUsage.js +1 -1
  160. package/dist/runtime/claude-process-owner +0 -0
  161. package/dist/runtime/codexAppServerRuntime.js +38 -30
  162. package/dist/runtime/codexInteractiveHost.js +41 -6
  163. package/dist/runtime/continuationManager.js +2 -6
  164. package/dist/runtime/executionEnvironment.js +30 -0
  165. package/dist/runtime/firstProgressAdvisory.js +11 -11
  166. package/dist/runtime/index.js +4 -3
  167. package/dist/runtime/jsonLineChannel.js +109 -0
  168. package/dist/runtime/launchBroker.js +91 -16
  169. package/dist/runtime/launchDiagnostics.js +2 -2
  170. package/dist/runtime/lifecycleReservation.js +10 -18
  171. package/dist/runtime/managedCaller.js +61 -17
  172. package/dist/runtime/nativeSessionControl.js +102 -0
  173. package/dist/runtime/ports.js +6 -21
  174. package/dist/runtime/processExitObservation.js +8 -7
  175. package/dist/runtime/promptEnvelope.js +17 -6
  176. package/dist/runtime/providerContinuation.js +3 -9
  177. package/dist/runtime/providerContinuationReconciliationService.js +4 -13
  178. package/dist/runtime/providerControl.js +2 -7
  179. package/dist/runtime/providerRuntimeIdentity.js +110 -222
  180. package/dist/runtime/providerRuntimeReconciler.js +5 -9
  181. package/dist/runtime/runtimeBinding.js +0 -1
  182. package/dist/runtime/runtimeContinuationProjection.js +4 -7
  183. package/dist/runtime/runtimeDeadlines.js +9 -0
  184. package/dist/runtime/runtimeHealthPolicy.js +1 -1
  185. package/dist/runtime/runtimeObservation.js +29 -65
  186. package/dist/runtime/runtimeProjection.js +43 -51
  187. package/dist/runtime/runtimeSessionCandidate.js +1 -3
  188. package/dist/runtime/sessionLaunchRequest.js +3 -7
  189. package/dist/runtime/sessionOwnerIdentity.js +7 -54
  190. package/dist/runtime/sessionOwnerRegistry.js +22 -17
  191. package/dist/runtime/sessionReconciliation.js +4 -8
  192. package/dist/runtime/sessionTerminationGuard.js +70 -259
  193. package/dist/runtime/sessionTokenMetrics.js +5 -16
  194. package/dist/runtime/structuredProviderHost.js +237 -117
  195. package/dist/runtime/taskRuntimeIsolation.js +39 -122
  196. package/dist/runtime/tmuxAdapters.js +39 -86
  197. package/dist/scheduler/activeRoleRunDelivery.js +354 -0
  198. package/dist/scheduler/leaderWakeupProcessor.js +75 -266
  199. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  200. package/dist/scheduler/ports.js +80 -9
  201. package/dist/scheduler/{roleTurnLiveness.js → roleRunLiveness.js} +26 -30
  202. package/dist/scheduler/{roleTurnStall.js → roleRunStall.js} +128 -139
  203. package/dist/scheduler/taskExecutionProjection.js +120 -124
  204. package/dist/scheduler/taskObservabilityProjection.js +29 -29
  205. package/dist/scheduler/taskWake.js +11 -4
  206. package/dist/scheduler/wakeReason.js +9 -1
  207. package/dist/setup/setupCommand.js +1 -0
  208. package/dist/storage/migrations/agentRunContract.js +159 -0
  209. package/dist/storage/migrations/removeRuntimeGeneration.js +207 -0
  210. package/dist/storage/sqliteSchema.js +431 -5
  211. package/dist/storage/sqliteStore.js +375 -220
  212. package/dist/storage/storageVersions.js +1 -1
  213. package/dist/storage/storeRpc.js +10 -5
  214. package/dist/storage/taskStore.js +13 -11
  215. package/dist/storage/upgrade/upgradeOrchestrator.js +5 -7
  216. package/dist/surface/surfaceContributions.js +102 -0
  217. package/dist/task/completionReadiness.js +32 -6
  218. package/dist/task/deliveryGuard.js +16 -16
  219. package/dist/task/draftPlan.js +72 -12
  220. package/dist/task/nextAction.js +144 -128
  221. package/dist/task/remoteDelivery.js +6 -6
  222. package/dist/task/task.js +184 -18
  223. package/dist/task/taskActivation.js +301 -0
  224. package/dist/task/taskActivationService.js +392 -0
  225. package/dist/task/taskRecordReference.js +5 -4
  226. package/dist/task/taskRecordRetirement.js +1 -1
  227. package/dist/telemetry/sqliteTelemetryStore.js +55 -68
  228. package/dist/telemetry/telemetryConfig.js +14 -14
  229. package/dist/telemetry/telemetryWiring.js +2 -2
  230. package/dist/web/assets/assetManifest.js +2 -0
  231. package/dist/web/assets/client/app.js +120 -20
  232. package/dist/web/assets/client/components.js +87 -54
  233. package/dist/web/assets/client/i18n.js +83 -41
  234. package/dist/web/assets/client/markdown.js +1 -1
  235. package/dist/web/assets/client/taskSurface.js +353 -0
  236. package/dist/web/assets/client/view.js +49 -44
  237. package/dist/web/assets/shell.js +1 -1
  238. package/dist/web/assets/styles/cards.js +22 -4
  239. package/dist/web/controllerWeb.js +60 -0
  240. package/dist/web/webMutation.js +28 -0
  241. package/dist/web/webServer.js +118 -8
  242. package/dist/web/webSnapshot.js +81 -74
  243. package/dist/web/webTaskSurface.js +64 -0
  244. package/dist/workItem/dependencyGate.js +1 -1
  245. package/dist/workItem/workItem.js +80 -48
  246. package/dist/workspace/workItemChangeSetManager.js +16 -9
  247. package/docs/agent-result-consumption.md +94 -0
  248. package/docs/agent-runtime-drivers.md +91 -0
  249. package/docs/architecture/README.md +38 -0
  250. package/docs/architecture/capabilities-and-resources.md +79 -0
  251. package/docs/managed-turn-and-session-runtime.md +222 -0
  252. package/docs/observability/README.md +81 -0
  253. package/docs/plugin-sdk.md +290 -0
  254. package/docs/provider-runtime.md +163 -0
  255. package/docs/release-workflow.md +303 -0
  256. package/docs/roles-and-configuration.md +113 -0
  257. package/docs/sqlite-control-plane-design.md +76 -0
  258. package/docs/task-dag-semantics.md +57 -0
  259. package/docs/task-delivery.md +103 -0
  260. package/docs/task-local-identity.md +6 -6
  261. package/docs/testing/verification-levels.md +86 -0
  262. package/i18n/README.zh-CN.md +98 -739
  263. package/package.json +2 -2
  264. package/skills/yui-leader/SKILL.md +130 -103
  265. package/skills/yui-leader/references/integration.md +39 -0
  266. package/skills/yui-leader/references/replicated-execution.md +42 -0
  267. package/skills/yui-leader/references/task-plugins.md +33 -0
  268. package/skills/yui-operator/SKILL.md +30 -59
  269. package/skills/yui-reviewer/SKILL.md +35 -36
  270. package/skills/yui-runtime/SKILL.md +88 -24
  271. package/skills/yui-runtime/references/publication.md +22 -0
  272. package/skills/yui-runtime/references/recovery.md +64 -0
  273. package/skills/yui-worker/SKILL.md +37 -39
  274. package/dist/cli/roleOptionCatalog.js +0 -68
  275. package/dist/context/sourceTurnContext.js +0 -30
  276. package/dist/execution/reviewMainTurn.js +0 -161
  277. package/dist/execution/workItemMainTurn.js +0 -164
  278. package/dist/lifecycle/exactTurnTerminalization.js +0 -407
  279. package/dist/runtime/preallocatedNativeSession.js +0 -13
  280. package/dist/runtime/runtimeStopReceipt.js +0 -42
  281. package/dist/scheduler/activeRoleTurnDelivery.js +0 -315
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import Database from "better-sqlite3";
4
- import { DEFAULT_TURN_CAP, DEFAULT_TERMINAL_KEEP } from "./telemetryConfig.js";
4
+ import { DEFAULT_RUN_CAP, DEFAULT_TERMINAL_KEEP } from "./telemetryConfig.js";
5
5
  import { migrateSqliteSchema } from "../storage/sqliteSchema.js";
6
6
  import { CURRENT_DATABASE_FILENAME as COMMITTED_DATABASE_FILENAME } from "../storage/currentTaskStore.js";
7
7
  /**
@@ -18,7 +18,7 @@ import { CURRENT_DATABASE_FILENAME as COMMITTED_DATABASE_FILENAME } from "../sto
18
18
  *
19
19
  * The telemetry tables are maintained by the centralized schema baseline:
20
20
  * `telemetry` holds the bounded latest-per-key window and
21
- * `telemetry_aggregate` holds the authoritative per-Turn/generation
21
+ * `telemetry_aggregate` holds the authoritative per-AgentRun
22
22
  * summary, maintained by triggers so it survives window pruning.
23
23
  */
24
24
  const MAX_PAGE_LIMIT = 500;
@@ -41,7 +41,7 @@ export class SqliteTelemetryStore {
41
41
  this.mode = options.mode ?? "on";
42
42
  this.#path = join(home, COMMITTED_DATABASE_FILENAME);
43
43
  this.#terminalKeep = options.terminalKeep ?? DEFAULT_TERMINAL_KEEP;
44
- this.#turnCap = options.turnCap ?? DEFAULT_TURN_CAP;
44
+ this.#turnCap = options.runCap ?? DEFAULT_RUN_CAP;
45
45
  this.#maxPending = options.maxPending ?? 10_000;
46
46
  }
47
47
  // -- TelemetrySink -------------------------------------------------------------
@@ -89,53 +89,52 @@ export class SqliteTelemetryStore {
89
89
  this.#db = null;
90
90
  }
91
91
  // -- TelemetryReader -----------------------------------------------------------
92
- count(taskId, turnId) {
92
+ count(taskId, runId) {
93
93
  const db = this.#ensureDb();
94
94
  if (db === null)
95
95
  return 0;
96
96
  if (taskId === undefined) {
97
97
  return db.prepare("SELECT COUNT(*) AS n FROM telemetry").get().n;
98
98
  }
99
- if (turnId === undefined) {
99
+ if (runId === undefined) {
100
100
  return db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE task_id = ?").get(taskId).n;
101
101
  }
102
- return db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE task_id = ? AND turn_id = ?").get(taskId, turnId).n;
102
+ return db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE task_id = ? AND turn_id = ?").get(taskId, runId).n;
103
103
  }
104
- list(taskId, turnId, page = { limit: 100, offset: 0 }) {
104
+ list(taskId, runId, page = { limit: 100, offset: 0 }) {
105
105
  const db = this.#ensureDb();
106
106
  if (db === null)
107
107
  return { items: [], nextOffset: null };
108
108
  const limit = Math.min(Math.max(1, Math.trunc(page.limit)), MAX_PAGE_LIMIT);
109
109
  const offset = Math.max(0, Math.trunc(page.offset));
110
- const rows = turnId === undefined
111
- ? db.prepare("SELECT task_id, role_name, turn_id, generation, progress_id, sequence, payload, received_at FROM telemetry WHERE task_id = ? ORDER BY received_at, progress_id LIMIT ? OFFSET ?").all(taskId, limit, offset)
112
- : db.prepare("SELECT task_id, role_name, turn_id, generation, progress_id, sequence, payload, received_at FROM telemetry WHERE task_id = ? AND turn_id = ? ORDER BY received_at, progress_id LIMIT ? OFFSET ?").all(taskId, turnId, limit, offset);
110
+ const rows = runId === undefined
111
+ ? db.prepare("SELECT task_id, role_name, turn_id, progress_id, sequence, payload, received_at FROM telemetry WHERE task_id = ? ORDER BY received_at, progress_id LIMIT ? OFFSET ?").all(taskId, limit, offset)
112
+ : db.prepare("SELECT task_id, role_name, turn_id, progress_id, sequence, payload, received_at FROM telemetry WHERE task_id = ? AND turn_id = ? ORDER BY received_at, progress_id LIMIT ? OFFSET ?").all(taskId, runId, limit, offset);
113
113
  const items = rows.map(rowToEntry);
114
- const total = this.count(taskId, turnId);
114
+ const total = this.count(taskId, runId);
115
115
  const nextOffset = offset + items.length < total ? offset + items.length : null;
116
116
  return { items, nextOffset };
117
117
  }
118
- aggregate(taskId, turnId) {
118
+ aggregate(taskId, runId) {
119
119
  const db = this.#ensureDb();
120
120
  if (db === null)
121
121
  return null;
122
- const rows = db.prepare("SELECT task_id, role_name, turn_id, generation, first_at, last_at, count, max_sequence, error_count FROM telemetry_aggregate WHERE task_id = ? AND turn_id = ?").all(taskId, turnId);
122
+ const rows = db.prepare("SELECT task_id, role_name, turn_id, first_at, last_at, count, max_sequence, error_count FROM telemetry_aggregate WHERE task_id = ? AND turn_id = ?").all(taskId, runId);
123
123
  if (rows.length === 0)
124
124
  return null;
125
125
  return mergeAggregates(rows);
126
126
  }
127
- aggregateGeneration(taskId, roleName, turnId, generation) {
127
+ aggregateRoleRun(taskId, roleName, runId) {
128
128
  const db = this.#ensureDb();
129
129
  if (db === null)
130
130
  return null;
131
- const row = db.prepare("SELECT task_id, role_name, turn_id, generation, first_at, last_at, count, max_sequence, error_count FROM telemetry_aggregate WHERE task_id = ? AND role_name = ? AND turn_id = ? AND generation = ?").get(taskId, roleName, turnId, generation);
131
+ const row = db.prepare("SELECT task_id, role_name, turn_id, first_at, last_at, count, max_sequence, error_count FROM telemetry_aggregate WHERE task_id = ? AND role_name = ? AND turn_id = ?").get(taskId, roleName, runId);
132
132
  if (row === undefined)
133
133
  return null;
134
134
  return {
135
135
  taskId: row.task_id,
136
136
  roleName: row.role_name,
137
- turnId: row.turn_id,
138
- generation: row.generation,
137
+ runId: row.turn_id,
139
138
  firstAt: row.first_at,
140
139
  lastAt: row.last_at,
141
140
  count: row.count,
@@ -143,16 +142,15 @@ export class SqliteTelemetryStore {
143
142
  errorCount: row.error_count
144
143
  };
145
144
  }
146
- listTurnAggregates(taskId) {
145
+ listRunAggregates(taskId) {
147
146
  const db = this.#ensureDb();
148
147
  if (db === null)
149
148
  return [];
150
- const rows = db.prepare("SELECT task_id, role_name, turn_id, generation, first_at, last_at, count, max_sequence, error_count FROM telemetry_aggregate WHERE task_id = ? ORDER BY turn_id, generation").all(taskId);
149
+ const rows = db.prepare("SELECT task_id, role_name, turn_id, first_at, last_at, count, max_sequence, error_count FROM telemetry_aggregate WHERE task_id = ? ORDER BY turn_id").all(taskId);
151
150
  return rows.map((row) => ({
152
151
  taskId: row.task_id,
153
152
  roleName: row.role_name,
154
- turnId: row.turn_id,
155
- generation: row.generation,
153
+ runId: row.turn_id,
156
154
  firstAt: row.first_at,
157
155
  lastAt: row.last_at,
158
156
  count: row.count,
@@ -164,52 +162,51 @@ export class SqliteTelemetryStore {
164
162
  return this.#applied;
165
163
  }
166
164
  // -- retention -----------------------------------------------------------------
167
- pruneGeneration(taskId, roleName, turnId, generation, keep = this.#terminalKeep) {
165
+ pruneRun(taskId, roleName, runId, keep = this.#terminalKeep) {
168
166
  const db = this.#ensureDb();
169
167
  if (db === null)
170
168
  return 0;
171
169
  const result = db.prepare(`DELETE FROM telemetry
172
- WHERE task_id = ? AND role_name = ? AND turn_id = ? AND generation = ?
173
- AND (task_id, role_name, turn_id, generation, progress_id) NOT IN (
174
- SELECT task_id, role_name, turn_id, generation, progress_id FROM telemetry
175
- WHERE task_id = ? AND role_name = ? AND turn_id = ? AND generation = ?
176
- ORDER BY COALESCE(sequence, -1) DESC, received_at DESC, progress_id ASC
170
+ WHERE task_id = ? AND role_name = ? AND turn_id = ?
171
+ AND (task_id, role_name, turn_id, progress_id) NOT IN (
172
+ SELECT task_id, role_name, turn_id, progress_id FROM telemetry
173
+ WHERE task_id = ? AND role_name = ? AND turn_id = ?
174
+ ORDER BY received_at DESC, COALESCE(sequence, -1) DESC, progress_id ASC
177
175
  LIMIT ?
178
- )`).run(taskId, roleName, turnId, generation, taskId, roleName, turnId, generation, keep);
176
+ )`).run(taskId, roleName, runId, taskId, roleName, runId, keep);
179
177
  return result.changes;
180
178
  }
181
- capTurn(taskId, turnId, cap = this.#turnCap) {
179
+ capRun(taskId, runId, cap = this.#turnCap) {
182
180
  const db = this.#ensureDb();
183
181
  if (db === null)
184
182
  return 0;
185
183
  const result = db.prepare(`DELETE FROM telemetry
186
184
  WHERE task_id = ? AND turn_id = ?
187
- AND (task_id, role_name, turn_id, generation, progress_id) NOT IN (
188
- SELECT task_id, role_name, turn_id, generation, progress_id FROM telemetry
185
+ AND (task_id, role_name, turn_id, progress_id) NOT IN (
186
+ SELECT task_id, role_name, turn_id, progress_id FROM telemetry
189
187
  WHERE task_id = ? AND turn_id = ?
190
- ORDER BY COALESCE(sequence, -1) DESC, received_at DESC, progress_id ASC
188
+ ORDER BY received_at DESC, COALESCE(sequence, -1) DESC, progress_id ASC
191
189
  LIMIT ?
192
- )`).run(taskId, turnId, taskId, turnId, cap);
190
+ )`).run(taskId, runId, taskId, runId, cap);
193
191
  return result.changes;
194
192
  }
195
- importGeneration(entries, aggregate) {
193
+ importRun(entries, aggregate) {
196
194
  const db = this.#ensureDb();
197
195
  if (db === null) {
198
196
  this.#dropped += entries.length;
199
197
  return;
200
198
  }
201
- const upsert = db.prepare(`INSERT INTO telemetry (task_id, role_name, turn_id, generation, progress_id, sequence, payload, received_at)
202
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
203
- ON CONFLICT(task_id, role_name, turn_id, generation, progress_id) DO UPDATE SET
199
+ const upsert = db.prepare(`INSERT INTO telemetry (task_id, role_name, turn_id, progress_id, sequence, payload, received_at)
200
+ VALUES (?, ?, ?, ?, ?, ?, ?)
201
+ ON CONFLICT(task_id, role_name, turn_id, progress_id) DO UPDATE SET
204
202
  sequence = excluded.sequence,
205
203
  payload = excluded.payload,
206
204
  received_at = excluded.received_at
207
- WHERE COALESCE(excluded.sequence, -1) > COALESCE(telemetry.sequence, -1)
208
- OR (excluded.sequence IS telemetry.sequence AND excluded.received_at >= telemetry.received_at)`);
205
+ WHERE excluded.received_at >= telemetry.received_at`);
209
206
  const upsertAggregate = db.prepare(`INSERT INTO telemetry_aggregate
210
- (task_id, role_name, turn_id, generation, first_at, last_at, count, max_sequence, error_count, updated_at)
211
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
212
- ON CONFLICT(task_id, role_name, turn_id, generation) DO UPDATE SET
207
+ (task_id, role_name, turn_id, first_at, last_at, count, max_sequence, error_count, updated_at)
208
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
209
+ ON CONFLICT(task_id, role_name, turn_id) DO UPDATE SET
213
210
  first_at = excluded.first_at,
214
211
  last_at = excluded.last_at,
215
212
  count = excluded.count,
@@ -218,9 +215,9 @@ export class SqliteTelemetryStore {
218
215
  updated_at = excluded.updated_at`);
219
216
  db.transaction(() => {
220
217
  for (const entry of entries) {
221
- upsert.run(entry.taskId, entry.roleName, entry.turnId, entry.generation, entry.progressId, entry.sequence ?? null, JSON.stringify(entry.payload), entry.receivedAt);
218
+ upsert.run(entry.taskId, entry.roleName, entry.runId, entry.progressId, entry.sequence ?? null, JSON.stringify(entry.payload), entry.receivedAt);
222
219
  }
223
- upsertAggregate.run(aggregate.taskId, aggregate.roleName, aggregate.turnId, aggregate.generation, aggregate.firstAt, aggregate.lastAt, aggregate.count, aggregate.maxSequence, aggregate.errorCount, aggregate.lastAt);
220
+ upsertAggregate.run(aggregate.taskId, aggregate.roleName, aggregate.runId, aggregate.firstAt, aggregate.lastAt, aggregate.count, aggregate.maxSequence, aggregate.errorCount, aggregate.lastAt);
224
221
  })();
225
222
  this.#applied += entries.length;
226
223
  }
@@ -277,26 +274,25 @@ export class SqliteTelemetryStore {
277
274
  }
278
275
  const batch = new Map(this.#pending);
279
276
  this.#pending.clear();
280
- const upsert = db.prepare(`INSERT INTO telemetry (task_id, role_name, turn_id, generation, progress_id, sequence, payload, received_at)
281
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
282
- ON CONFLICT(task_id, role_name, turn_id, generation, progress_id) DO UPDATE SET
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
283
280
  sequence = excluded.sequence,
284
281
  payload = excluded.payload,
285
282
  received_at = excluded.received_at
286
- WHERE COALESCE(excluded.sequence, -1) > COALESCE(telemetry.sequence, -1)
287
- OR (excluded.sequence IS telemetry.sequence AND excluded.received_at >= telemetry.received_at)`);
283
+ WHERE excluded.received_at >= telemetry.received_at`);
288
284
  try {
289
- const touchedTurns = new Map();
285
+ const touchedRuns = new Map();
290
286
  db.transaction(() => {
291
287
  for (const entry of batch.values()) {
292
- upsert.run(entry.taskId, entry.roleName, entry.turnId, entry.generation, entry.progressId, entry.sequence ?? null, JSON.stringify(entry.payload), entry.receivedAt);
293
- touchedTurns.set(`${entry.taskId}\0${entry.turnId}`, {
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}`, {
294
290
  taskId: entry.taskId,
295
- turnId: entry.turnId
291
+ runId: entry.runId
296
292
  });
297
293
  }
298
- for (const { taskId, turnId } of touchedTurns.values()) {
299
- this.capTurn(taskId, turnId);
294
+ for (const { taskId, runId } of touchedRuns.values()) {
295
+ this.capRun(taskId, runId);
300
296
  }
301
297
  })();
302
298
  this.#applied += batch.size;
@@ -308,25 +304,17 @@ export class SqliteTelemetryStore {
308
304
  }
309
305
  }
310
306
  function pendingKey(entry) {
311
- return `${entry.taskId}\u0000${entry.roleName}\u0000${entry.turnId}\u0000${entry.generation}\u0000${entry.progressId}`;
307
+ return `${entry.taskId}\u0000${entry.roleName}\u0000${entry.runId}\u0000${entry.progressId}`;
312
308
  }
313
- /** True when `candidate` should replace `current` (sequence/receivedAt only move forward). */
309
+ /** Transport counters may restart; the later recorded observation wins. */
314
310
  function isNewer(candidate, current) {
315
- if (candidate.sequence !== undefined && current.sequence !== undefined) {
316
- return candidate.sequence > current.sequence;
317
- }
318
- if (candidate.sequence !== undefined)
319
- return true;
320
- if (current.sequence !== undefined)
321
- return false;
322
311
  return Date.parse(candidate.receivedAt) >= Date.parse(current.receivedAt);
323
312
  }
324
313
  function rowToEntry(row) {
325
314
  return {
326
315
  taskId: row.task_id,
327
316
  roleName: row.role_name,
328
- turnId: row.turn_id,
329
- generation: row.generation,
317
+ runId: row.turn_id,
330
318
  progressId: row.progress_id,
331
319
  ...(row.sequence === null ? {} : { sequence: row.sequence }),
332
320
  payload: JSON.parse(row.payload),
@@ -356,8 +344,7 @@ function mergeAggregates(rows) {
356
344
  return {
357
345
  taskId: rows[0].task_id,
358
346
  roleName,
359
- turnId: rows[0].turn_id,
360
- generation: "*",
347
+ runId: rows[0].turn_id,
361
348
  firstAt,
362
349
  lastAt,
363
350
  count,
@@ -6,34 +6,34 @@
6
6
  * never changes canonical runtime-state persistence. Retention defaults are
7
7
  * the schema's own constants (§4.4); the environment only overrides them.
8
8
  */
9
- import { TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_TURN_CAP } from "../storage/sqliteSchema.js";
9
+ import { TELEMETRY_KEEP_PER_RUN, TELEMETRY_RUN_CAP } from "../storage/sqliteSchema.js";
10
10
  export const DEFAULT_TELEMETRY_MODE = "off";
11
- /** Terminal Turn/generation progress rows retained after prune. */
12
- export const DEFAULT_TERMINAL_KEEP = TELEMETRY_KEEP_PER_GENERATION;
13
- /** Hard cap of progress rows per Turn while it is still active. */
14
- export const DEFAULT_TURN_CAP = TELEMETRY_TURN_CAP;
11
+ /** Terminal AgentRun progress rows retained after prune. */
12
+ export const DEFAULT_TERMINAL_KEEP = TELEMETRY_KEEP_PER_RUN;
13
+ /** Hard cap of progress rows per AgentRun while it is still active. */
14
+ export const DEFAULT_RUN_CAP = TELEMETRY_RUN_CAP;
15
15
  /**
16
- * Absolute ceiling for the configurable Turn cap. Retention can be tuned but
16
+ * Absolute ceiling for the configurable AgentRun cap. Retention can be tuned but
17
17
  * never disabled: every Home keeps a computable upper bound on telemetry
18
- * rows (Tasks × Turns × cap).
18
+ * rows (Tasks × AgentRuns × cap).
19
19
  */
20
- export const MAX_TURN_CAP = 10_000_000;
20
+ export const MAX_RUN_CAP = 10_000_000;
21
21
  /**
22
- * Resolve the terminal-Turn retention window from the durable config value
22
+ * Resolve the terminal-AgentRun retention window from the durable config value
23
23
  * (default 200). Must be a positive integer.
24
24
  */
25
25
  export function resolveTerminalKeep(value) {
26
26
  return resolvePositiveInteger(value, DEFAULT_TERMINAL_KEEP, "telemetryTerminalKeep");
27
27
  }
28
28
  /**
29
- * Resolve the active-Turn hard cap from the durable config value (default
29
+ * Resolve the active-AgentRun hard cap from the durable config value (default
30
30
  * 50,000). Must be a positive integer not exceeding {@link MAX_TURN_CAP};
31
31
  * the cap cannot be disabled.
32
32
  */
33
- export function resolveTurnCap(value) {
34
- const cap = resolvePositiveInteger(value, DEFAULT_TURN_CAP, "telemetryTurnCap");
35
- if (cap > MAX_TURN_CAP) {
36
- throw new TypeError(`telemetryTurnCap must not exceed ${MAX_TURN_CAP.toLocaleString("en-US")} (retention cannot be disabled).`);
33
+ export function resolveRunCap(value) {
34
+ const cap = resolvePositiveInteger(value, DEFAULT_RUN_CAP, "telemetryRunCap");
35
+ if (cap > MAX_RUN_CAP) {
36
+ throw new TypeError(`telemetryTurnCap must not exceed ${MAX_RUN_CAP.toLocaleString("en-US")} (retention cannot be disabled).`);
37
37
  }
38
38
  return cap;
39
39
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { resolveTurnCap, resolveTerminalKeep } from "./telemetryConfig.js";
3
+ import { resolveRunCap, resolveTerminalKeep } from "./telemetryConfig.js";
4
4
  import { resolveTelemetryEnabled } from "../config/yuiConfig.js";
5
5
  import { SqliteTelemetryStore } from "./sqliteTelemetryStore.js";
6
6
  import { CURRENT_DATABASE_FILENAME as COMMITTED_DATABASE_FILENAME } from "../storage/currentTaskStore.js";
@@ -22,7 +22,7 @@ export function openSchedulerTelemetry(home, config) {
22
22
  const store = new SqliteTelemetryStore(home, {
23
23
  mode,
24
24
  terminalKeep: resolveTerminalKeep(config.telemetryTerminalKeep),
25
- turnCap: resolveTurnCap(config.telemetryTurnCap)
25
+ runCap: resolveRunCap(config.telemetryRunCap)
26
26
  });
27
27
  return { mode, sink: store, reader: store, retention: store };
28
28
  }
@@ -8,6 +8,7 @@ import { I18N_SCRIPT } from "./client/i18n.js";
8
8
  import { MARKDOWN_SCRIPT } from "./client/markdown.js";
9
9
  import { THEME_SCRIPT } from "./client/theme.js";
10
10
  import { VIEW_SCRIPT } from "./client/view.js";
11
+ import { TASK_SURFACE_SCRIPT } from "./client/taskSurface.js";
11
12
  import { DASHBOARD_HTML } from "./shell.js";
12
13
  import { FONT_FACE_STYLES } from "./fonts.js";
13
14
  import { FONT_WOFF2_BASE64 } from "./fontData.js";
@@ -33,6 +34,7 @@ export const WEB_ASSETS = Object.freeze({
33
34
  "/assets/js/theme.js": { contentType: "text/javascript; charset=utf-8", body: THEME_SCRIPT },
34
35
  "/assets/js/components.js": { contentType: "text/javascript; charset=utf-8", body: COMPONENTS_SCRIPT },
35
36
  "/assets/js/view.js": { contentType: "text/javascript; charset=utf-8", body: VIEW_SCRIPT },
37
+ "/assets/js/task-surface.js": { contentType: "text/javascript; charset=utf-8", body: TASK_SURFACE_SCRIPT },
36
38
  "/assets/app.js": { contentType: "text/javascript; charset=utf-8", body: APP_SCRIPT },
37
39
  ...fontAssets(),
38
40
  "/assets/vendor/xterm.mjs": vendorAsset("@xterm/xterm/lib/xterm.mjs", "text/javascript; charset=utf-8"),
@@ -47,9 +47,10 @@ const state = {
47
47
  detail: null,
48
48
  detailKey: null
49
49
  };
50
- const VALID_FILTERS = ["all", "active", "draft", "completed", "retired", "archived"];
50
+ const VALID_FILTERS = ["all", "active", "draft", "completed", "cancelled", "archived"];
51
51
  let terminalSession = null;
52
52
  let terminalStateKey = "terminal.closed";
53
+ const submittedRequests = new Set();
53
54
 
54
55
  const i18n = createI18n(elements.locale);
55
56
  createThemeController(elements.theme);
@@ -125,7 +126,18 @@ function syncUrlFromState(options) {
125
126
  function detailActions() {
126
127
  return {
127
128
  answerInput: answerInput,
128
- openTerminal: openTerminal
129
+ openTerminal: openTerminal,
130
+ inspect: inspectRecord,
131
+ sendMessage: (taskId, body, requestId) => submitMutation(taskId + "/messages",
132
+ "/api/tasks/" + encodeURIComponent(taskId) + "/messages", { body, requestId }),
133
+ updateTask: (taskId, patch, requestId) => submitMutation(taskId + "/metadata",
134
+ "/api/tasks/" + encodeURIComponent(taskId) + "/metadata", { patch, requestId }),
135
+ panels: (taskId) => requestJson("/api/tasks/" + encodeURIComponent(taskId) + "/panels",
136
+ { signal: AbortSignal.timeout(3000) }),
137
+ readPanel: (taskId, ref, input) => requestJson("/api/tasks/" + encodeURIComponent(taskId) + "/panels", {
138
+ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ref, input }),
139
+ signal: AbortSignal.timeout(3000)
140
+ })
129
141
  };
130
142
  }
131
143
 
@@ -171,9 +183,30 @@ function detailKeyOf(detail) {
171
183
  return text.length + ":" + hash;
172
184
  }
173
185
 
186
+ // The optional observation arrives after the core snapshot has already been
187
+ // rendered, so the render key has to cover the observed facts the detail
188
+ // actually draws or they would stay stale until the core snapshot changed.
189
+ // Only the rendered identity is included: observation timestamps advance on
190
+ // every poll and would re-render the detail continuously.
191
+ function runtimeSignatureOf(detail) {
192
+ const roles = (detail.runtime && detail.runtime.roles) || [];
193
+ return detail.runtimeStatus + "|" + roles.map(function (role) {
194
+ const session = role.runtimeSession;
195
+ return role.name + ":" + (session
196
+ ? session.nativeSessionId + "/" + session.status
197
+ : "-");
198
+ }).join(",");
199
+ }
200
+
174
201
  function renderCurrentDetail(force) {
175
202
  if (state.detail) {
176
- const key = i18n.getLocale() + "|" + state.detailKey;
203
+ // Polling must not replace an unsent draft or an in-flight form, including
204
+ // after the user has moved focus. This is view state, never Task state.
205
+ if (elements.detail.dataset.taskId === state.detail.task.id
206
+ && (elements.detail.querySelector('[data-unsent="true"]')
207
+ || elements.detail.contains(document.activeElement))) return;
208
+ const key = i18n.getLocale() + "|" + state.detailKey
209
+ + "|" + runtimeSignatureOf(state.detail);
177
210
  if (!force && key === renderedDetailKey) return;
178
211
  renderTaskDetail(
179
212
  elements.detail,
@@ -275,31 +308,76 @@ async function requestJson(path, options) {
275
308
  ...options,
276
309
  headers: {
277
310
  accept: "application/json",
311
+ "x-yui-web-token": token,
278
312
  ...(options && options.headers ? options.headers : {})
279
313
  }
280
314
  });
281
315
  if (!response.ok) {
282
316
  let message = "HTTP " + response.status;
317
+ let disposition = "unknown";
283
318
  try {
284
319
  const body = await response.json();
285
320
  if (body && body.error) message = body.error;
321
+ if (body && body.disposition === "not-submitted") disposition = "not-submitted";
286
322
  } catch {}
287
- throw new Error(message);
323
+ throw Object.assign(new Error(message), { disposition });
288
324
  }
289
325
  return response.json();
290
326
  }
291
327
 
328
+ async function submitMutation(key, path, body) {
329
+ if (submittedRequests.has(key)) throw new Error("An earlier submission is unresolved; read current facts.");
330
+ submittedRequests.add(key);
331
+ try {
332
+ const receipt = await requestJson(path, {
333
+ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body)
334
+ });
335
+ submittedRequests.delete(key);
336
+ return receipt;
337
+ } catch (error) {
338
+ if (error.disposition === "not-submitted") submittedRequests.delete(key);
339
+ throw error;
340
+ }
341
+ }
342
+
292
343
  async function loadTaskDetail(taskId, showLoading) {
293
344
  if (showLoading) {
294
345
  renderLoading(elements.detail, i18n.t, "loading.detail");
295
346
  elements.mainCol.scrollTop = 0;
296
347
  }
297
348
  const savedScrollTop = showLoading ? 0 : elements.mainCol.scrollTop;
298
- const detail = await requestJson("/api/tasks/" + encodeURIComponent(taskId));
349
+ const base = "/api/tasks/" + encodeURIComponent(taskId);
350
+ // Rendering consumes the current snapshot, not event pages. Reconnect uses
351
+ // this same read; the independent delta API retains its fixed-bound contract.
352
+ const core = await requestJson(base + "/context");
353
+ const taskEntry = core.records.find(function (entry) { return entry.ref.store === "task"; });
354
+ if (!taskEntry) throw new Error("Task reference unavailable.");
355
+ const task = taskEntry.omitted ? (await inspectRecord(taskId, taskEntry.ref)).value : taskEntry.value;
356
+ const previous = state.detail && state.detail.task.id === taskId ? state.detail : null;
357
+ const detail = {
358
+ task, core,
359
+ runtime: previous && previous.runtime,
360
+ runtimeStatus: previous ? previous.runtimeStatus : "waiting",
361
+ runtimeObservedAt: previous ? previous.runtimeObservedAt : new Date().toISOString()
362
+ };
299
363
  if (state.selected !== taskId) return;
300
364
  state.detail = detail;
301
- state.detailKey = detailKeyOf(detail);
302
- renderCurrentDetail(true);
365
+ state.detailKey = detailKeyOf(core);
366
+ renderCurrentDetail();
367
+ // Optional observation does not participate in core readiness or cursor.
368
+ void requestJson(base, { signal: AbortSignal.timeout(1000) }).then(function (runtime) {
369
+ if (state.detail !== detail) return;
370
+ detail.runtime = runtime;
371
+ detail.runtimeStatus = "available";
372
+ detail.runtimeObservedAt = new Date().toISOString();
373
+ updateRuntimePanel(detail);
374
+ }).catch(function () {
375
+ if (state.detail !== detail) return;
376
+ detail.runtime = null;
377
+ detail.runtimeStatus = "unavailable";
378
+ detail.runtimeObservedAt = new Date().toISOString();
379
+ updateRuntimePanel(detail);
380
+ });
303
381
  // Reveal the tab bar before measuring/scroll so anchors land correctly.
304
382
  setDetailActive(true);
305
383
  updateStickyOffsets();
@@ -314,6 +392,25 @@ async function loadTaskDetail(taskId, showLoading) {
314
392
  syncTabHighlight();
315
393
  }
316
394
 
395
+ function updateRuntimePanel(detail) {
396
+ const status = elements.detail.querySelector("[data-runtime-status]");
397
+ const value = elements.detail.querySelector("[data-runtime-value]");
398
+ if (status) status.textContent = detail.runtimeStatus + " · " + detail.runtimeObservedAt;
399
+ if (value) value.textContent = detail.runtime
400
+ ? JSON.stringify({ roles: detail.runtime.roles, runtimeHealth: detail.runtime.runtimeHealth }, null, 2)
401
+ : "";
402
+ // Patching the panel in place is not enough: the observation also feeds facts
403
+ // the detail rendered before it arrived. Re-render when the observed identity
404
+ // has actually changed — renderCurrentDetail still compares the render key,
405
+ // so an unchanged observation costs nothing and an unsent form is preserved.
406
+ if (state.detail === detail) renderCurrentDetail();
407
+ }
408
+
409
+ async function inspectRecord(taskId, ref) {
410
+ const query = new URLSearchParams({ store: ref.store, ref: ref.refId, digest: ref.digest });
411
+ return requestJson("/api/tasks/" + encodeURIComponent(taskId) + "/inspect?" + query);
412
+ }
413
+
317
414
  async function selectTask(taskId) {
318
415
  // Switching tasks drops the previous section; staying on the same task
319
416
  // (URL-driven loads, popstate) keeps it.
@@ -345,27 +442,27 @@ async function selectTask(taskId) {
345
442
  async function answerInput(input, answer) {
346
443
  if (!state.detail) return;
347
444
  const taskId = state.detail.task.id;
445
+ const key = taskId + "/input/" + input.id;
348
446
  try {
349
- await requestJson(
447
+ await submitMutation(key,
350
448
  "/api/tasks/" + encodeURIComponent(taskId)
351
449
  + "/inputs/" + encodeURIComponent(input.id) + "/answer",
352
- {
353
- method: "POST",
354
- headers: {
355
- "content-type": "application/json",
356
- "x-yui-web-token": token
357
- },
358
- body: JSON.stringify(answer)
359
- }
450
+ answer
360
451
  );
361
452
  showToast(i18n.t("input.answered"));
453
+ submittedRequests.delete(key);
362
454
  await refreshDashboard({ quiet: true });
363
- } catch {
364
- showToast(i18n.t("errors.answer"));
455
+ } catch (error) {
456
+ showToast(error.disposition === "not-submitted" ? error.message : i18n.getLocale().startsWith("zh")
457
+ ? "回答结果未知;请刷新检查原问题,不要盲目重发。"
458
+ : "Answer outcome unknown; refresh the original question before resubmitting.");
365
459
  }
366
460
  }
367
461
 
462
+ let refreshing = false;
368
463
  async function refreshDashboard(options) {
464
+ if (refreshing) return;
465
+ refreshing = true;
369
466
  const quiet = options && options.quiet;
370
467
  if (!quiet) {
371
468
  elements.refresh.disabled = true;
@@ -386,8 +483,10 @@ async function refreshDashboard(options) {
386
483
  if (previousInputs !== null && dashboard.counts.openInputs > previousInputs) {
387
484
  showToast(i18n.t("input.new"));
388
485
  }
389
- if (state.selected && !quiet) {
390
- try { await loadTaskDetail(state.selected, false); } catch {}
486
+ if (state.selected) {
487
+ try { await loadTaskDetail(state.selected, false); } catch {
488
+ showToast(i18n.getLocale().startsWith("zh") ? "连接不可用;保留上次读取。" : "Disconnected; showing the last read.");
489
+ }
391
490
  }
392
491
  } catch {
393
492
  if (!quiet) {
@@ -395,6 +494,7 @@ async function refreshDashboard(options) {
395
494
  showToast(i18n.t("errors.dashboard"));
396
495
  }
397
496
  } finally {
497
+ refreshing = false;
398
498
  if (!quiet) elements.refresh.disabled = false;
399
499
  }
400
500
  }