@zq-silk/yui 0.6.2 → 0.6.4

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 (63) hide show
  1. package/ARCHITECTURE.md +28 -4
  2. package/README.md +60 -97
  3. package/dist/agent/argumentPolicy.js +1 -1
  4. package/dist/agent/managedRuntimeEnvironment.js +1 -0
  5. package/dist/cli/commandCatalog.js +19 -9
  6. package/dist/cli/interactionPolicy.js +4 -2
  7. package/dist/cli.js +77 -32
  8. package/dist/commands/taskCommands.js +46 -11
  9. package/dist/commands/taskContextCommand.js +1 -1
  10. package/dist/commands/taskRoleRuntimeStatus.js +170 -10
  11. package/dist/controller/agentRuntimeObserver.js +210 -0
  12. package/dist/controller/clientRuntime.js +3 -21
  13. package/dist/controller/controller.js +47 -7
  14. package/dist/controller/fileSchedulerStoreAdapter.js +522 -388
  15. package/dist/controller/runtime.js +9 -3
  16. package/dist/controller/runtimeEventInbox.js +49 -295
  17. package/dist/controller/runtimeEventProcessor.js +184 -321
  18. package/dist/controller/runtimeHookRunFence.js +226 -0
  19. package/dist/controller/runtimeLaunchCoordinator.js +91 -26
  20. package/dist/controller/runtimeObservationHook.js +112 -0
  21. package/dist/core/controllerServer.js +5 -0
  22. package/dist/executor/agentAdapter.js +18 -3
  23. package/dist/executor/fileRoleLaunchPlanner.js +64 -15
  24. package/dist/executor/managedClaudeRunner.js +121 -0
  25. package/dist/observability/executionAudit.js +6 -3
  26. package/dist/repository/taskWorkspacePreparer.js +1 -4
  27. package/dist/run/providerRetryConfig.js +8 -3
  28. package/dist/runtime/agentDriver.js +229 -0
  29. package/dist/runtime/agentDriverObservation.js +57 -0
  30. package/dist/runtime/builtinAgentDrivers.js +235 -0
  31. package/dist/runtime/builtinTranscriptObserver.js +290 -0
  32. package/dist/runtime/builtinTranscriptUsage.js +97 -0
  33. package/dist/runtime/exactControlPlane.js +2 -2
  34. package/dist/runtime/index.js +1 -1
  35. package/dist/runtime/ports.js +12 -1
  36. package/dist/runtime/runtimeObservation.js +297 -0
  37. package/dist/runtime/runtimeProjection.js +277 -0
  38. package/dist/runtime/sessionTerminationGuard.js +78 -22
  39. package/dist/runtime/tmuxAdapters.js +35 -0
  40. package/dist/scheduler/activeRoleRunDelivery.js +28 -13
  41. package/dist/scheduler/leaderWakeupProcessor.js +21 -2
  42. package/dist/scheduler/roleRunLiveness.js +2 -2
  43. package/dist/scheduler/roleRunStall.js +62 -114
  44. package/dist/storage/migration/productionRegistry.js +85 -0
  45. package/dist/storage/sqliteStore.js +3 -3
  46. package/dist/storage/storageVersions.js +1 -1
  47. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +2 -1
  48. package/dist/storage/upgrade/sqliteStateMigration.js +123 -0
  49. package/dist/telemetry/sqliteTelemetryStore.js +0 -28
  50. package/dist/telemetry/telemetryCompaction.js +1 -0
  51. package/dist/telemetry/telemetryConfig.js +4 -5
  52. package/dist/tmux/tmuxManager.js +136 -22
  53. package/dist/web/assets/client/view.js +1 -1
  54. package/dist/web/tmuxWebTerminal.js +17 -12
  55. package/dist/web/webSnapshot.js +1 -1
  56. package/dist/worktree/managedWorkspace.js +14 -0
  57. package/i18n/README.zh-CN.md +12 -7
  58. package/package.json +1 -1
  59. package/dist/controller/claudeLifecycleHook.js +0 -203
  60. package/dist/controller/codexLifecycleHook.js +0 -108
  61. package/dist/controller/providerHookRunFence.js +0 -156
  62. package/dist/lifecycle/providerLifecycleMapping.js +0 -190
  63. package/dist/telemetry/telemetryRouter.js +0 -32
@@ -13,7 +13,7 @@ import { createTaskMessage, taskMessageAuthorLabel } from "../message/message.js
13
13
  import { cancelInputRequest } from "../input/inputRequest.js";
14
14
  import { recoverExactAgentRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
15
15
  import { resetTaskRoleSessionGeneration } from "../lifecycle/taskRoleSessionReset.js";
16
- import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
16
+ import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
17
17
  import { createAgentRun } from "../run/agentRun.js";
18
18
  import { matchYieldReceipt } from "../run/yieldReceipt.js";
19
19
  import { providerRetryConfig } from "../run/providerRetryConfig.js";
@@ -1268,7 +1268,7 @@ function listTaskRoles(args, store, options) {
1268
1268
  exactPositionals(args, 1, "Task role list usage: yui task role list <task>.");
1269
1269
  const task = requireTask(store, args[0]);
1270
1270
  const roles = store.listRoles(task.id);
1271
- const statuses = inspectTaskRoleRuntimeStatuses(task.id, roles, store, options.runtime?.inspectTaskRolePanes?.(task.id) ?? []);
1271
+ const statuses = inspectTaskRoleRuntimeStatuses(task.id, roles, store, options.runtime?.inspectTaskRolePanes?.(task.id) ?? [], options.now?.() ?? new Date());
1272
1272
  if (statuses.length === 0)
1273
1273
  return output("No roles assigned.\n", { roles: statuses });
1274
1274
  return output(`${renderTable(`Task roles: ${task.id}`, [
@@ -1293,7 +1293,7 @@ function taskRoleStatus(args, store, options) {
1293
1293
  exactPositionals(args, 2, "Task role status usage: yui task role status <task> <role>.");
1294
1294
  const task = requireTask(store, args[0]);
1295
1295
  const role = requireRole(store, task.id, args[1]);
1296
- const [status] = inspectTaskRoleRuntimeStatuses(task.id, [role], store, options.runtime?.inspectTaskRolePanes?.(task.id) ?? []);
1296
+ const [status] = inspectTaskRoleRuntimeStatuses(task.id, [role], store, options.runtime?.inspectTaskRolePanes?.(task.id) ?? [], options.now?.() ?? new Date());
1297
1297
  if (status === undefined)
1298
1298
  throw roleNotFound(role.name);
1299
1299
  return output(renderTaskRoleRuntimeStatus(status), { role: status });
@@ -1475,25 +1475,60 @@ function unbindTaskRole(args, store, options) {
1475
1475
  });
1476
1476
  return `Unbound Agent ${args[2]} from ${result.taskId}/${result.name}\n`;
1477
1477
  }
1478
- function enterTaskRole(args, store, options) {
1479
- exactPositionals(args, 2, "Task role enter usage: yui task role enter <task> <role>.");
1480
- const task = requireTask(store, args[0]);
1478
+ function enterTaskRole(args, store, _options) {
1479
+ const usage = "Task role enter usage: yui task role enter <task> <role> "
1480
+ + "[--read-only | --read-write].";
1481
+ const parsed = parseTail(args, new Set(), usage, new Set(["--read-only", "--read-write"]));
1482
+ exactPositionals(parsed.positionals, 2, usage);
1483
+ if (parsed.options.has("--read-only") && parsed.options.has("--read-write")) {
1484
+ throw usageError("--read-only and --read-write are mutually exclusive.", usage);
1485
+ }
1486
+ const task = requireTask(store, parsed.positionals[0]);
1481
1487
  if (task.status !== "active") {
1482
1488
  throw usageError(inactiveTaskMessage(task, "entering a role session"));
1483
1489
  }
1484
- const role = requireRole(store, task.id, args[1]);
1490
+ const role = requireRole(store, task.id, parsed.positionals[1]);
1491
+ const access = parsed.options.has("--read-write") ? "read-write" : "read-only";
1492
+ if (access === "read-write") {
1493
+ assertTaskRoleWritableAttachAvailable(store, task.id, role.name);
1494
+ }
1485
1495
  return {
1486
1496
  kind: "enter",
1487
1497
  taskId: task.id,
1488
1498
  roleName: role.name,
1489
- output: `Prepared role ${role.name} for ${task.id}\n`
1499
+ access,
1500
+ output: `Attaching to ${role.name} for ${task.id} (${access})\n`
1490
1501
  };
1491
1502
  }
1503
+ /** Re-run after the tmux writer lease exists to close attach/launch races. */
1504
+ export function assertTaskRoleWritableAttachAvailable(store, taskId, roleName, options = {}) {
1505
+ const role = requireRole(store, taskId, roleName);
1506
+ if (store.getActiveAgentRun(taskId, roleName) !== null) {
1507
+ throw usageError(`Role has an active managed Run; writable attach is unavailable: ${taskId}/${roleName}.`);
1508
+ }
1509
+ const session = store.getRoleSession(taskId, roleName);
1510
+ if (activeRoleAgentBinding(role).adapterId === "claude"
1511
+ && ((session !== null
1512
+ && session.status !== "stopped"
1513
+ && session.status !== "broken")
1514
+ || options.isManagedProcessRunning?.() === true)) {
1515
+ throw usageError(`A managed Claude process is still running; writable attach is unavailable: ${taskId}/${roleName}.`);
1516
+ }
1517
+ }
1492
1518
  function enterTaskRoleAlias(args, store, options) {
1493
- if (args.length < 1 || args.length > 2 || args.some((value) => value.trim().length === 0)) {
1494
- throw usageError("Task enter usage: yui task enter <task> [role].");
1519
+ const usage = "Task enter usage: yui task enter <task> [role] "
1520
+ + "[--read-only | --read-write].";
1521
+ const parsed = parseTail(args, new Set(), usage, new Set(["--read-only", "--read-write"]));
1522
+ if (parsed.positionals.length < 1 || parsed.positionals.length > 2
1523
+ || parsed.positionals.some((value) => value.trim().length === 0)) {
1524
+ throw usageError(usage);
1495
1525
  }
1496
- return enterTaskRole([args[0], args[1] ?? LEADER_ROLE], store, options);
1526
+ return enterTaskRole([
1527
+ parsed.positionals[0],
1528
+ parsed.positionals[1] ?? LEADER_ROLE,
1529
+ ...(parsed.options.has("--read-only") ? ["--read-only"] : []),
1530
+ ...(parsed.options.has("--read-write") ? ["--read-write"] : [])
1531
+ ], store, options);
1497
1532
  }
1498
1533
  function taskWorkCommand(args, store, options) {
1499
1534
  const [command, ...rest] = args;
@@ -303,7 +303,7 @@ function latestStallKind(events, runId) {
303
303
  const event = [...events]
304
304
  .filter((candidate) => candidate.type === "run.stalled" && candidate.payload.runId === runId)
305
305
  .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
306
- return event?.payload.kind ?? "execution-stalled";
306
+ return event?.payload.kind ?? "workflow-not-progressing";
307
307
  }
308
308
  function renderWorkItemReviews(rounds) {
309
309
  const latest = rounds.at(-1);
@@ -1,7 +1,11 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
3
3
  import { isRoleRunStalled, latestStallProgressAt } from "../scheduler/roleRunStall.js";
4
- export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes) {
4
+ import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
5
+ import { evaluateRuntimeAttention, projectRuntimeObservation, projectRuntimeTaskEvents, runtimeDisplayStatus } from "../runtime/runtimeProjection.js";
6
+ import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
7
+ import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
8
+ export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes, now = new Date()) {
5
9
  const taskOpenInputRequestCount = store.listInputRequests(taskId)
6
10
  .filter((request) => request.status === "open").length;
7
11
  const panesByRole = new Map();
@@ -10,7 +14,7 @@ export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes) {
10
14
  if (current === undefined || current.dead && !pane.dead)
11
15
  panesByRole.set(pane.roleName, pane);
12
16
  }
13
- return roles.map((role) => inspectTaskRoleRuntimeStatus(taskId, role, store, panesByRole.get(role.name), role.name === "leader" ? taskOpenInputRequestCount : 0));
17
+ return roles.map((role) => inspectTaskRoleRuntimeStatus(taskId, role, store, panesByRole.get(role.name), role.name === "leader" ? taskOpenInputRequestCount : 0, now));
14
18
  }
15
19
  export function renderTaskRoleRuntimeStatus(status) {
16
20
  const activeRun = status.activeRun === null
@@ -36,6 +40,23 @@ export function renderTaskRoleRuntimeStatus(status) {
36
40
  const workspaceDetails = status.workspace.managed
37
41
  ? status.workspace.entries.map((entry) => (` Project ${entry.directory} (${entry.access}) ${entry.branch} @ ${entry.baseCommit}`))
38
42
  : [];
43
+ const runtime = status.runtime === null
44
+ ? "not observable"
45
+ : [
46
+ `${status.runtime.driverId}: ${status.runtime.status}`,
47
+ `attention=${status.runtime.attention}`,
48
+ status.runtime.lastActivityAt === undefined
49
+ ? undefined
50
+ : `last activity=${status.runtime.lastActivityAt}`,
51
+ status.runtime.activeOperations.length === 0
52
+ ? undefined
53
+ : `operations=${status.runtime.activeOperations.join(",")}`,
54
+ status.runtime.observerStatus === undefined
55
+ ? undefined
56
+ : `observer=${status.runtime.observerStatus}${status.runtime.observerDetail === undefined
57
+ ? ""
58
+ : ` (${status.runtime.observerDetail})`}`
59
+ ].filter((value) => value !== undefined).join("; ");
39
60
  return [
40
61
  `Task Role status: ${status.taskId}/${status.roleName}`,
41
62
  "",
@@ -53,9 +74,10 @@ export function renderTaskRoleRuntimeStatus(status) {
53
74
  ` Active work ${activeWork}`,
54
75
  ` Active run ${activeRun}`,
55
76
  ` Run attention ${status.stall.active
56
- ? `needs-attention (${status.stall.kind ?? "execution-stalled"}; no durable progress since ${status.stall.progressAt ?? "unknown"})`
77
+ ? `needs-attention (${status.stall.kind ?? "workflow-not-progressing"}; no workflow progress since ${status.stall.progressAt ?? "unknown"})`
57
78
  : "none"}`,
58
79
  ` Native session ${nativeSession}`,
80
+ ` Agent runtime ${runtime}`,
59
81
  ` Runtime cleanup ${status.runtimeCleanupPending ? "pending" : "none"}`,
60
82
  ` Fresh launch ${status.freshLaunchAllowed ? "allowed" : "blocked"}`,
61
83
  ` tmux pane ${tmux}`,
@@ -97,7 +119,7 @@ export function taskRoleTmuxLabel(status) {
97
119
  ? "running"
98
120
  : `running (${status.tmux.currentCommand})`;
99
121
  }
100
- function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputRequestCount) {
122
+ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputRequestCount, now) {
101
123
  const activeRun = store.getActiveAgentRun(taskId, role.name);
102
124
  const activeWork = activeRun?.workItemId === undefined
103
125
  ? null
@@ -132,6 +154,7 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
132
154
  ? { managed: false, path: role.workspace }
133
155
  : { ...managedWorkspace, managed: true };
134
156
  const events = store.listEvents(taskId);
157
+ const runtime = projectTaskRoleRuntime(activeRun, nativeSession, tmux, events, now);
135
158
  const stalled = activeRun !== null && isRoleRunStalled(events, activeRun.id);
136
159
  const stallProgressAt = activeRun === null
137
160
  ? undefined
@@ -139,7 +162,7 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
139
162
  const stallKind = activeRun === null
140
163
  ? undefined
141
164
  : latestStallKind(events, activeRun.id);
142
- const health = calculateHealth(role, activeRun, nativeSession, recovery.runtimeCleanupPending, tmux, openInputRequestCount, stalled);
165
+ const health = calculateHealth(role, activeRun, nativeSession, recovery.runtimeCleanupPending, tmux, openInputRequestCount, stalled, runtime);
143
166
  const stall = activeRun === null
144
167
  ? { active: false }
145
168
  : {
@@ -165,6 +188,7 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
165
188
  nativeSession,
166
189
  tmux,
167
190
  workspace,
191
+ runtime,
168
192
  stall
169
193
  };
170
194
  }
@@ -172,11 +196,11 @@ function latestStallKind(events, runId) {
172
196
  const event = [...events]
173
197
  .filter((candidate) => candidate.type === "run.stalled" && candidate.payload.runId === runId)
174
198
  .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
175
- return event?.payload.kind === "delivery-stalled" || event?.payload.kind === "execution-stalled"
199
+ return event?.payload.kind === "delivery-stalled" || event?.payload.kind === "workflow-not-progressing"
176
200
  ? event.payload.kind
177
201
  : undefined;
178
202
  }
179
- function calculateHealth(role, activeRun, nativeSession, runtimeCleanupPending, tmux, openInputRequestCount, stalled) {
203
+ function calculateHealth(role, activeRun, nativeSession, runtimeCleanupPending, tmux, openInputRequestCount, stalled, runtime) {
180
204
  if (runtimeCleanupPending && nativeSession === null) {
181
205
  return {
182
206
  health: "needs-attention",
@@ -217,6 +241,49 @@ function calculateHealth(role, activeRun, nativeSession, runtimeCleanupPending,
217
241
  healthReason: "the live active Run has no durable progress in the configured stall window"
218
242
  };
219
243
  }
244
+ if (activeRun.deliveredAt !== undefined) {
245
+ if (runtime === null
246
+ || runtime.status === "runtime-unobservable"
247
+ || runtime.attention === "unobservable") {
248
+ return {
249
+ health: "needs-attention",
250
+ healthReason: "the host is present but the Agent Driver exposes no current runtime state"
251
+ };
252
+ }
253
+ if (runtime.attention === "quiet" || runtime.attention === "active-operation-quiet") {
254
+ return {
255
+ health: "needs-attention",
256
+ healthReason: runtime.attention === "active-operation-quiet"
257
+ ? "the Agent Driver reports an open operation but no recent structured runtime activity"
258
+ : "the Agent Driver has not reported recent structured runtime activity"
259
+ };
260
+ }
261
+ if (runtime.status === "broken" || runtime.status === "stopped") {
262
+ return {
263
+ health: "failed",
264
+ healthReason: `the Agent Driver runtime is ${runtime.status}`
265
+ };
266
+ }
267
+ if (runtime.status.startsWith("waiting-")) {
268
+ return {
269
+ health: runtime.status === "waiting-user" ? "blocked-input" : "waiting",
270
+ healthReason: `the Agent Driver is ${runtime.status.replaceAll("-", " ")}`
271
+ };
272
+ }
273
+ if (runtime.status === "ready") {
274
+ return {
275
+ health: "needs-attention",
276
+ healthReason: "the Agent turn ended while the workflow Run is still active"
277
+ };
278
+ }
279
+ if (["model-active", "tool-active", "subagent-active", "active-quiet"]
280
+ .includes(runtime.status)) {
281
+ return {
282
+ health: "running",
283
+ healthReason: `the Agent Driver reports ${runtime.status.replaceAll("-", " ")}`
284
+ };
285
+ }
286
+ }
220
287
  }
221
288
  if (activeRun === null && role.status === "running") {
222
289
  return { health: "needs-attention", healthReason: "the Role is running without an active Run" };
@@ -234,9 +301,11 @@ function calculateHealth(role, activeRun, nativeSession, runtimeCleanupPending,
234
301
  };
235
302
  }
236
303
  if (activeRun !== null) {
237
- if (activeRun.deliveredAt !== undefined) {
238
- return { health: "running", healthReason: "the active Run has a live tmux pane" };
239
- }
304
+ if (activeRun.deliveredAt !== undefined)
305
+ return {
306
+ health: "needs-attention",
307
+ healthReason: "the delivered Run has no authoritative Agent Driver state"
308
+ };
240
309
  if (activeRun.pushedAt !== undefined) {
241
310
  return {
242
311
  health: "awaiting-provider-acceptance",
@@ -249,6 +318,97 @@ function calculateHealth(role, activeRun, nativeSession, runtimeCleanupPending,
249
318
  ? { health: "ready", healthReason: "the native Agent pane is ready without active work" }
250
319
  : { health: "idle", healthReason: "there is no active work or live tmux pane" };
251
320
  }
321
+ function projectTaskRoleRuntime(run, session, tmux, events, now) {
322
+ if (run === null || session?.launchId === undefined)
323
+ return null;
324
+ let driverId;
325
+ try {
326
+ driverId = builtinDriverIdForAdapter(run.effective.adapterId);
327
+ }
328
+ catch {
329
+ return null;
330
+ }
331
+ const fence = {
332
+ taskId: run.taskId,
333
+ roleName: run.roleName,
334
+ runId: run.id,
335
+ agentId: run.effective.agentId,
336
+ driverId,
337
+ launchId: session.launchId,
338
+ sessionGenerationId: session.launchId,
339
+ nativeSessionId: session.nativeSessionId,
340
+ nativeTurnId: runtimeNativeTurnId(events, {
341
+ taskId: run.taskId,
342
+ roleName: run.roleName,
343
+ runId: run.id,
344
+ agentId: run.effective.agentId,
345
+ driverId,
346
+ launchId: session.launchId,
347
+ nativeSessionId: session.nativeSessionId,
348
+ receiptId: formatAgentRunReceiptId(run.taskId, run.id)
349
+ }) ?? run.id,
350
+ receiptId: formatAgentRunReceiptId(run.taskId, run.id)
351
+ };
352
+ let projection = projectRuntimeTaskEvents(fence, run.createdAt, events);
353
+ projection = projectRuntimeObservation(projection, createRuntimeObservation({
354
+ schemaVersion: 1,
355
+ eventId: `runtime-host-${run.id}`,
356
+ kind: "host.observed",
357
+ authority: "host",
358
+ receivedAt: run.updatedAt,
359
+ fence,
360
+ payload: { alive: tmux.state === "running" }
361
+ }));
362
+ const attention = evaluateRuntimeAttention(projection, now, {
363
+ runtimeSilenceMs: 5 * 60_000,
364
+ // Workflow attention has its own durable scheduler policy. This value is
365
+ // deliberately not consumed here; keeping it separate prevents token/tool
366
+ // activity from extending the workflow deadline.
367
+ semanticSilenceMs: 30 * 60_000
368
+ });
369
+ return {
370
+ driverId,
371
+ status: runtimeDisplayStatus(projection),
372
+ attention: attention.runtime,
373
+ ...(projection.lastRuntimeActivityAt === undefined
374
+ ? {}
375
+ : { lastActivityAt: projection.lastRuntimeActivityAt }),
376
+ activeOperations: Object.entries(projection.operations).map(([id, operation]) => (`${operation.kind}:${id}`)),
377
+ ...(projection.waitingReason === undefined
378
+ ? {}
379
+ : { waitingReason: projection.waitingReason }),
380
+ ...(projection.usage === undefined ? {} : { usage: projection.usage }),
381
+ ...(projection.observer.status === "unknown"
382
+ ? {}
383
+ : {
384
+ observerStatus: projection.observer.status,
385
+ ...(projection.observer.detail === undefined
386
+ ? {}
387
+ : { observerDetail: projection.observer.detail })
388
+ })
389
+ };
390
+ }
391
+ function runtimeNativeTurnId(events, expected) {
392
+ const observations = events
393
+ .map(runtimeObservationFromTaskEvent)
394
+ .filter((observation) => observation !== null
395
+ && observation.fence.taskId === expected.taskId
396
+ && observation.fence.roleName === expected.roleName
397
+ && observation.fence.runId === expected.runId
398
+ && observation.fence.agentId === expected.agentId
399
+ && observation.fence.driverId === expected.driverId
400
+ && observation.fence.launchId === expected.launchId
401
+ && observation.fence.nativeSessionId === expected.nativeSessionId
402
+ && observation.fence.receiptId === expected.receiptId
403
+ && observation.fence.nativeTurnId !== undefined)
404
+ .sort((left, right) => (left.receivedAt.localeCompare(right.receivedAt)
405
+ || (left.sequence ?? -1) - (right.sequence ?? -1)
406
+ || (left.ordinal ?? -1) - (right.ordinal ?? -1)
407
+ || left.eventId.localeCompare(right.eventId)));
408
+ return observations.filter(({ kind }) => kind === "turn.accepted").at(-1)
409
+ ?.fence.nativeTurnId
410
+ ?? observations.at(-1)?.fence.nativeTurnId;
411
+ }
252
412
  function activeRunDeliveryLabel(run) {
253
413
  if (run.deliveredAt !== undefined)
254
414
  return "delivered";
@@ -0,0 +1,210 @@
1
+ import { createHash } from "node:crypto";
2
+ import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
3
+ import { createRuntimeObservation, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
4
+ /**
5
+ * Controller-owned, provider-independent sampler. Drivers own source parsing;
6
+ * this component owns active-Run discovery, cursor lifetime, canonical event
7
+ * creation, and low-latency mailbox wakes.
8
+ */
9
+ export class AgentRuntimeObserver {
10
+ store;
11
+ inbox;
12
+ drivers;
13
+ #states = new Map();
14
+ #sequence = 0;
15
+ constructor(store, inbox, drivers = builtinAgentDriverRegistry()) {
16
+ this.store = store;
17
+ this.inbox = inbox;
18
+ this.drivers = drivers;
19
+ }
20
+ async sample(now = new Date()) {
21
+ const active = this.activeSources();
22
+ const activeKeys = new Set(active.map(({ key }) => key));
23
+ for (const key of this.#states.keys()) {
24
+ if (!activeKeys.has(key))
25
+ this.#states.delete(key);
26
+ }
27
+ const dirty = new Set();
28
+ await Promise.all(active.map(async ({ key, fence, source, freshSession, persistedState }) => {
29
+ const existingState = this.#states.get(key);
30
+ // Cursor state is intentionally process-local, but the latest canonical
31
+ // usage/activity baseline is durable. Rehydrate it after Controller
32
+ // restart so rereading the bounded transcript tail cannot manufacture a
33
+ // fresh activity edge from tokens that were already observed.
34
+ const state = existingState ?? { ...persistedState };
35
+ const driver = this.drivers.require(fence.driverId);
36
+ const observer = driver.runtime.observer;
37
+ if (observer === undefined)
38
+ return;
39
+ let sample;
40
+ try {
41
+ sample = await observer.sample(source, state.cursor);
42
+ }
43
+ catch (error) {
44
+ sample = Object.freeze({
45
+ cursor: state.cursor ?? Object.freeze({}),
46
+ status: "unavailable",
47
+ detail: error instanceof Error ? error.message : String(error)
48
+ });
49
+ }
50
+ state.cursor = sample.cursor;
51
+ const at = now.toISOString();
52
+ const sequence = this.#sequence++;
53
+ if (existingState === undefined && freshSession && state.usage === undefined) {
54
+ const zero = Object.freeze({ inputTokens: 0, outputTokens: 0 });
55
+ this.inbox.enqueueObservation(createRuntimeObservation({
56
+ schemaVersion: 1,
57
+ eventId: observationId("baseline", fence, source.sourceId, "zero"),
58
+ kind: "activity.observed",
59
+ authority: "controller",
60
+ receivedAt: at,
61
+ sequence,
62
+ ordinal: 1,
63
+ fence,
64
+ payload: { activity: "model", usage: zero }
65
+ }));
66
+ state.usage = zero;
67
+ dirty.add(`role:${fence.taskId}/${fence.roleName}`);
68
+ }
69
+ const health = JSON.stringify([sample.status, sample.detail ?? null]);
70
+ if (state.health !== health) {
71
+ this.inbox.enqueueObservation(createRuntimeObservation({
72
+ schemaVersion: 1,
73
+ eventId: observationId("health", fence, source.sourceId, health),
74
+ kind: "observer.health",
75
+ authority: "diagnostic",
76
+ receivedAt: at,
77
+ sequence,
78
+ ordinal: 0,
79
+ fence,
80
+ payload: {
81
+ sourceId: source.sourceId,
82
+ observerStatus: sample.status,
83
+ ...(sample.detail === undefined ? {} : { observerDetail: sample.detail })
84
+ }
85
+ }));
86
+ state.health = health;
87
+ dirty.add(`role:${fence.taskId}/${fence.roleName}`);
88
+ }
89
+ const usageChanged = sample.usage !== undefined
90
+ && !sameUsage(state.usage, sample.usage);
91
+ const activityChanged = sample.activityId !== undefined
92
+ && sample.activityId !== state.activityId;
93
+ if (usageChanged || (activityChanged && state.cursor !== undefined)) {
94
+ const usage = sample.usage;
95
+ this.inbox.enqueueObservation(createRuntimeObservation({
96
+ schemaVersion: 1,
97
+ eventId: observationId("activity", fence, source.sourceId, JSON.stringify([usage ?? null, sample.activityId ?? null])),
98
+ kind: "activity.observed",
99
+ authority: "driver-inferred",
100
+ receivedAt: at,
101
+ sequence,
102
+ ordinal: 2,
103
+ fence,
104
+ payload: {
105
+ activity: sample.activity ?? "model",
106
+ ...(sample.activityId === undefined ? {} : { activityId: sample.activityId }),
107
+ ...(usage === undefined ? {} : { usage })
108
+ }
109
+ }));
110
+ dirty.add(`role:${fence.taskId}/${fence.roleName}`);
111
+ }
112
+ if (sample.usage !== undefined)
113
+ state.usage = sample.usage;
114
+ if (sample.activityId !== undefined)
115
+ state.activityId = sample.activityId;
116
+ this.#states.set(key, state);
117
+ }));
118
+ return Object.freeze([...dirty].sort());
119
+ }
120
+ activeSources() {
121
+ const result = [];
122
+ for (const task of this.store.listTasks()) {
123
+ if (task.status !== "active")
124
+ continue;
125
+ const observations = this.store.listEvents(task.id)
126
+ .map(runtimeObservationFromTaskEvent)
127
+ .filter((value) => value !== null);
128
+ for (const run of this.store.listAgentRuns(task.id)) {
129
+ if (run.status !== "active"
130
+ || this.store.getActiveAgentRun(task.id, run.roleName)?.id !== run.id)
131
+ continue;
132
+ const accepted = observations
133
+ .filter((observation) => observation.kind === "turn.accepted"
134
+ && observation.fence.runId === run.id
135
+ && observation.fence.roleName === run.roleName
136
+ && observation.fence.agentId === run.effective.agentId
137
+ && observation.payload.observerSource !== undefined)
138
+ .sort(compareObservations)
139
+ .at(-1);
140
+ const source = accepted?.payload.observerSource;
141
+ if (accepted === undefined || source === undefined
142
+ || accepted.fence.taskId === undefined || accepted.fence.runId === undefined)
143
+ continue;
144
+ try {
145
+ if (this.drivers.require(accepted.fence.driverId).runtime.observer === undefined)
146
+ continue;
147
+ }
148
+ catch {
149
+ continue;
150
+ }
151
+ const fence = accepted.fence;
152
+ const exact = observations
153
+ .filter((observation) => runtimeObservationFenceMatches(fence, observation.fence))
154
+ .sort(compareObservations);
155
+ const persistedUsage = exact.filter((observation) => (observation.kind === "activity.observed"
156
+ && observation.payload.usage !== undefined)).at(-1);
157
+ const persistedHealth = exact.filter((observation) => (observation.kind === "observer.health"
158
+ && observation.payload.sourceId === source.sourceId)).at(-1);
159
+ result.push(Object.freeze({
160
+ key: JSON.stringify([
161
+ fence.driverId,
162
+ fence.sessionGenerationId,
163
+ fence.nativeSessionId,
164
+ fence.nativeTurnId,
165
+ fence.runId,
166
+ source.sourceId
167
+ ]),
168
+ fence,
169
+ source,
170
+ freshSession: run.mode === "new",
171
+ persistedState: Object.freeze({
172
+ ...(persistedUsage?.payload.usage === undefined
173
+ ? {}
174
+ : { usage: persistedUsage.payload.usage }),
175
+ ...(persistedUsage?.payload.activityId === undefined
176
+ ? {}
177
+ : { activityId: persistedUsage.payload.activityId }),
178
+ ...(persistedHealth === undefined
179
+ ? {}
180
+ : {
181
+ health: JSON.stringify([
182
+ persistedHealth.payload.observerStatus,
183
+ persistedHealth.payload.observerDetail ?? null
184
+ ])
185
+ })
186
+ })
187
+ }));
188
+ }
189
+ }
190
+ return result;
191
+ }
192
+ }
193
+ function compareObservations(left, right) {
194
+ return left.receivedAt.localeCompare(right.receivedAt)
195
+ || (left.sequence ?? -1) - (right.sequence ?? -1)
196
+ || (left.ordinal ?? -1) - (right.ordinal ?? -1)
197
+ || left.eventId.localeCompare(right.eventId);
198
+ }
199
+ function observationId(kind, fence, sourceId, value) {
200
+ return `runtime-observer-${createHash("sha256")
201
+ .update(JSON.stringify([kind, fence, sourceId, value]))
202
+ .digest("hex")}`;
203
+ }
204
+ function sameUsage(left, right) {
205
+ return left !== undefined
206
+ && left.inputTokens === right.inputTokens
207
+ && left.outputTokens === right.outputTokens
208
+ && left.cachedInputTokens === right.cachedInputTokens
209
+ && left.reasoningTokens === right.reasoningTokens;
210
+ }
@@ -405,22 +405,6 @@ export class FileTaskWorkflowRuntime {
405
405
  reconcileTask(taskId) {
406
406
  void this.#prepareAndScan(taskId).catch(this.clientOptions.onError ?? (() => { }));
407
407
  }
408
- async prepareTaskRoleEnter(input) {
409
- const task = this.store.getTask(input.taskId);
410
- if (task?.status === "active" && this.workspacePreparer !== undefined) {
411
- await this.workspacePreparer.prepareTaskWorkspace(task.id);
412
- }
413
- const environment = foregroundRoleEnvironment(this.store, { scope: "task", taskId: input.taskId, roleName: input.roleName }, this.clientOptions.environment ?? process.env);
414
- await callFileTaskController(this.home, "runtime.ensure-role-session", {
415
- scope: "task",
416
- taskId: input.taskId,
417
- roleName: input.roleName,
418
- ...(environment === undefined ? {} : { environment })
419
- }, {
420
- ...this.clientOptions,
421
- requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
422
- });
423
- }
424
408
  async stopTaskRoleSessions(taskId, roleNames) {
425
409
  const targets = [];
426
410
  for (const roleName of [...new Set(roleNames)]) {
@@ -508,7 +492,7 @@ export class FileTaskWorkflowRuntime {
508
492
  return this.tmux.inspectTaskRolePanes(taskId);
509
493
  }
510
494
  async prepareGlobalRoleEnter(roleName) {
511
- const environment = foregroundRoleEnvironment(this.store, { scope: "global", roleName }, this.clientOptions.environment ?? process.env);
495
+ const environment = foregroundGlobalRoleEnvironment(this.store, roleName, this.clientOptions.environment ?? process.env);
512
496
  await callFileTaskController(this.home, "runtime.ensure-role-session", {
513
497
  scope: "global",
514
498
  roleName,
@@ -532,10 +516,8 @@ export class FileTaskWorkflowRuntime {
532
516
  }
533
517
  }
534
518
  const MANAGED_RUNTIME_ENVIRONMENT = new Set(YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES);
535
- function foregroundRoleEnvironment(store, owner, source) {
536
- const role = owner.scope === "task"
537
- ? store.getRole?.(owner.taskId, owner.roleName)
538
- : store.getGlobalRole?.(owner.roleName);
519
+ function foregroundGlobalRoleEnvironment(store, roleName, source) {
520
+ const role = store.getGlobalRole?.(roleName);
539
521
  if (role === null || role === undefined)
540
522
  return undefined;
541
523
  const agent = store.getConfiguredAgent?.(role.activeAgentId);