@zq-silk/yui 0.11.3 → 0.12.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 (86) hide show
  1. package/ARCHITECTURE.md +23 -4
  2. package/README.md +36 -13
  3. package/dist/cli/commandCatalog.js +2 -4
  4. package/dist/cli.js +105 -43
  5. package/dist/commands/configCommands.js +12 -45
  6. package/dist/commands/executionAuditCommands.js +1 -1
  7. package/dist/commands/jobCommands.js +4 -10
  8. package/dist/commands/taskCommands.js +154 -171
  9. package/dist/commands/taskCompletionGate.js +29 -54
  10. package/dist/commands/taskContextCommand.js +55 -11
  11. package/dist/commands/taskInputCommands.js +8 -5
  12. package/dist/commands/taskNextActionCommand.js +56 -6
  13. package/dist/commands/taskOverviewCommand.js +17 -31
  14. package/dist/commands/taskRoleRuntimeStatus.js +24 -0
  15. package/dist/commands/taskWorkspaceCommands.js +5 -2
  16. package/dist/config/configCatalog.js +2 -2
  17. package/dist/config/yuiConfig.js +3 -7
  18. package/dist/context/wakeNotification.js +20 -5
  19. package/dist/controller/agentRuntimeObserver.js +247 -51
  20. package/dist/controller/clientRuntime.js +1 -1
  21. package/dist/controller/controller.js +8 -12
  22. package/dist/controller/fileSchedulerStoreAdapter.js +146 -198
  23. package/dist/controller/runtime.js +3 -3
  24. package/dist/controller/runtimeEventInbox.js +17 -7
  25. package/dist/controller/runtimeEventProcessor.js +12 -19
  26. package/dist/controller/runtimeObservationHook.js +45 -0
  27. package/dist/execution/resourceBroker.js +5 -4
  28. package/dist/executor/agentExecutor.js +4 -4
  29. package/dist/executor/effectiveLaunch.js +8 -48
  30. package/dist/executor/fileRoleLaunchPlanner.js +36 -16
  31. package/dist/executor/workspacePreflightClassification.js +23 -2
  32. package/dist/interaction/operatorPresentation.js +33 -89
  33. package/dist/lifecycle/exactRunTerminalization.js +1 -2
  34. package/dist/lifecycle/taskRoleSessionReset.js +5 -13
  35. package/dist/observability/orchestrationMetrics.js +8 -3
  36. package/dist/profile/agentProfile.js +1 -1
  37. package/dist/release/cliHomeReleaseFence.js +123 -0
  38. package/dist/repository/taskWorkspacePreparer.js +176 -60
  39. package/dist/resources/sqliteResourceRegistry.js +1 -1
  40. package/dist/review/deltaRecheck.js +12 -51
  41. package/dist/review/reviewAcceptance.js +26 -0
  42. package/dist/review/reviewConfig.js +0 -31
  43. package/dist/review/reviewDecision.js +113 -0
  44. package/dist/review/reviewOutcomeClassifier.js +1 -1
  45. package/dist/review/reviewRound.js +1 -1
  46. package/dist/review/reviewerAvailability.js +69 -0
  47. package/dist/runtime/agentDriverObservation.js +24 -10
  48. package/dist/runtime/builtinAgentDrivers.js +4 -3
  49. package/dist/runtime/builtinTranscriptObserver.js +301 -64
  50. package/dist/runtime/builtinTranscriptUsage.js +9 -7
  51. package/dist/runtime/index.js +1 -0
  52. package/dist/runtime/runtimeObservation.js +13 -0
  53. package/dist/runtime/runtimeProjection.js +16 -25
  54. package/dist/runtime/sessionTokenMetrics.js +181 -0
  55. package/dist/runtime/structuredProviderHost.js +7 -1
  56. package/dist/scheduler/activeRoleRunDelivery.js +4 -4
  57. package/dist/scheduler/activeTaskProgress.js +7 -6
  58. package/dist/scheduler/leaderWakeupProcessor.js +3 -5
  59. package/dist/scheduler/operatorEvent.js +34 -0
  60. package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
  61. package/dist/scheduler/roleRunStall.js +0 -14
  62. package/dist/scheduler/taskExecutionProjection.js +97 -76
  63. package/dist/scheduler/taskObservabilityProjection.js +10 -11
  64. package/dist/storage/migration/productionRegistry.js +379 -0
  65. package/dist/storage/sqliteSchema.js +18 -8
  66. package/dist/storage/sqliteStore.js +24 -20
  67. package/dist/storage/storeRpc.js +0 -1
  68. package/dist/storage/taskStore.js +15 -30
  69. package/dist/storage/upgrade/recordVersions.js +3 -2
  70. package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
  71. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
  72. package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
  73. package/dist/task/completionReadiness.js +10 -6
  74. package/dist/task/nextAction.js +82 -24
  75. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  76. package/dist/web/assets/client/components.js +13 -3
  77. package/dist/web/assets/client/i18n.js +6 -2
  78. package/dist/web/webSnapshot.js +4 -0
  79. package/i18n/README.zh-CN.md +2 -2
  80. package/package.json +1 -1
  81. package/skills/yui-leader/SKILL.md +53 -16
  82. package/skills/yui-operator/SKILL.md +9 -0
  83. package/skills/yui-reviewer/SKILL.md +28 -7
  84. package/dist/context/sessionContextBudget.js +0 -71
  85. package/dist/lifecycle/contextBudgetRollover.js +0 -81
  86. package/dist/scheduler/operatorNotification.js +0 -59
@@ -7,7 +7,7 @@ import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_M
7
7
  import { hasRuntimeCleanupObligation, runtimeLifecycleSignalKey, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
8
8
  import { agentProcessReadinessProbe, ExecutorRegistry } from "../executor/executorRegistry.js";
9
9
  import { activeLiveRoleAgentSession, roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
10
- import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain, effectiveLaunchConfig, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
10
+ import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession, effectiveLaunchConfig, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
11
11
  import { AgentConfigurationCatalogService, validateAgentLaunchConfiguration } from "../executor/agentConfigurationCatalog.js";
12
12
  import { isTaskOwnedWorkspace, managedWorkspaceIdentity, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
13
13
  import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
@@ -754,7 +754,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
754
754
  throw applicationError("INVALID_PARAMS", `Configured Agent does not match Role: ${effective.agentId}.`);
755
755
  }
756
756
  validateLifecycleEnvironment(request.environment, agent);
757
- const mode = roleAgentSessionResumeMode(sessions, effective.agentId, effective, managedWorkspace);
757
+ const mode = roleAgentSessionResumeMode(sessions, effective.agentId, effective);
758
758
  const session = sessions?.sessions[effective.agentId];
759
759
  const owner = request.scope === "task"
760
760
  ? { scope: "task", taskId: request.taskId, roleName: request.roleName }
@@ -878,7 +878,7 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
878
878
  throw new Error(`Native session changed: ${request.owner.roleName}.`);
879
879
  }
880
880
  const sessionEffectiveCompatible = request.owner.scope === "task"
881
- ? effectiveLaunchSnapshotsCompatibleForTaskMain(session.effective, request.effective, request.managedWorkspace)
881
+ ? effectiveLaunchSnapshotsCompatibleForTaskSession(session.effective, request.effective)
882
882
  : effectiveLaunchSnapshotsCompatible(session.effective, request.effective);
883
883
  if (!sessionEffectiveCompatible) {
884
884
  throw new Error(`Native session effective launch changed: ${request.owner.roleName}.`);
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { closeSync, constants, existsSync, fchmodSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { withUpgradeCoordinationLock } from "../storage/upgradeCoordination.js";
5
- import { createRuntimeObservation } from "../runtime/runtimeObservation.js";
5
+ import { createRuntimeObservation, isRuntimeTokenEvidence } from "../runtime/runtimeObservation.js";
6
6
  export const MAX_RUNTIME_TURN_SUMMARY_BYTES = 32 * 1024;
7
7
  export const MAX_RUNTIME_EVENT_FILE_BYTES = 16 * 1024 * 1024;
8
8
  const RUNTIME_EVENT_DIRECTORY = join("runtime", "inbox");
@@ -41,7 +41,7 @@ export class FileRuntimeEventInbox {
41
41
  return this.publish(event);
42
42
  }
43
43
  enqueueTurnCompleted(input) {
44
- const normalized = normalizeCodexInput(input);
44
+ const normalized = normalizeNativeTurnCompletedInput(input);
45
45
  return this.publish(Object.freeze({
46
46
  schemaVersion: 1,
47
47
  id: runtimeEventId("native-turn-completed", normalized),
@@ -163,6 +163,15 @@ export class FileRuntimeEventInbox {
163
163
  // UpgradeFenceError after the cutover holder releases the lock.
164
164
  return withUpgradeCoordinationLock(this.home, () => {
165
165
  this.hooks.afterAdmission?.();
166
+ if (event.type === "runtime-observation"
167
+ && isRuntimeTokenEvidence(event.observation)) {
168
+ const existing = this.list().find((candidate) => (candidate.type === "runtime-observation"
169
+ && candidate.taskId === event.taskId
170
+ && candidate.observation.eventId === event.observation.eventId));
171
+ if (existing !== undefined) {
172
+ return { event: existing, created: false };
173
+ }
174
+ }
166
175
  return this.publishUnlocked(event);
167
176
  });
168
177
  }
@@ -281,7 +290,7 @@ function runtimeEventId(type, input) {
281
290
  ];
282
291
  return `turn-${createHash("sha256").update(JSON.stringify(common)).digest("hex")}`;
283
292
  }
284
- function normalizeCodexInput(input) {
293
+ function normalizeNativeTurnCompletedInput(input) {
285
294
  const scope = input.scope;
286
295
  if (scope !== "task" && scope !== "global")
287
296
  throw invalidEvent();
@@ -305,7 +314,8 @@ function normalizeCodexInput(input) {
305
314
  : { title: requireIdentityText(input.title, "Session title") }),
306
315
  summary: truncateUtf8(input.summary.trim(), MAX_RUNTIME_TURN_SUMMARY_BYTES)
307
316
  };
308
- if (common.adapterId !== "codex" || common.summary.length === 0)
317
+ if ((common.adapterId !== "codex" && common.adapterId !== "claude")
318
+ || common.summary.length === 0)
309
319
  throw invalidEvent();
310
320
  return scope === "task"
311
321
  ? { ...common, taskId: requireIdentityText(input.taskId, "Task id") }
@@ -333,7 +343,7 @@ function parseRuntimeEvent(value) {
333
343
  throw invalidEvent();
334
344
  switch (value.type) {
335
345
  case "runtime-observation": return parseRuntimeObservationEvent(value);
336
- case "native-turn-completed": return parseCodexEvent(value);
346
+ case "native-turn-completed": return parseNativeTurnCompletedEvent(value);
337
347
  case "durable-job-terminal": return parseDurableJobTerminalEvent(value);
338
348
  default: throw invalidEvent();
339
349
  }
@@ -378,7 +388,7 @@ function parseDurableJobTerminalEvent(value) {
378
388
  ...normalized
379
389
  });
380
390
  }
381
- function parseCodexEvent(value) {
391
+ function parseNativeTurnCompletedEvent(value) {
382
392
  const scope = value.scope;
383
393
  const expected = scope === "task"
384
394
  ? [
@@ -400,7 +410,7 @@ function parseCodexEvent(value) {
400
410
  || !hasExactKeys(value, expected))
401
411
  throw invalidEvent();
402
412
  const receivedAt = requireTimestamp(value.receivedAt);
403
- const normalized = normalizeCodexInput({
413
+ const normalized = normalizeNativeTurnCompletedInput({
404
414
  scope,
405
415
  ...(scope === "task" ? { taskId: value.taskId } : {}),
406
416
  roleName: value.roleName,
@@ -1,3 +1,4 @@
1
+ import { isRuntimeTokenEvidence } from "../runtime/runtimeObservation.js";
1
2
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
2
3
  const DEFAULT_MAX_RUNTIME_EVENTS_PER_DRAIN = 64;
3
4
  /** Folds immutable Hook facts in one bounded transaction before acknowledging them. */
@@ -111,7 +112,7 @@ export class FileRuntimeEventProcessor {
111
112
  const event = candidate.event;
112
113
  let outcome = "applied";
113
114
  if (event.type === "native-turn-completed") {
114
- outcome = this.applyCodex(event, now);
115
+ outcome = this.applyNativeTurnCompleted(event, now);
115
116
  }
116
117
  else if (event.type === "runtime-observation") {
117
118
  outcome = this.applyRuntimeObservation(event, now);
@@ -199,7 +200,7 @@ export class FileRuntimeEventProcessor {
199
200
  // transaction as the terminal transition.
200
201
  void event;
201
202
  }
202
- applyCodex(event, now) {
203
+ applyNativeTurnCompleted(event, now) {
203
204
  if (event.scope === "task") {
204
205
  const task = this.observer.getTask(event.taskId);
205
206
  const input = {
@@ -315,17 +316,10 @@ export function coalesceRuntimeProgress(events, instrumentation = {}) {
315
316
  }
316
317
  const representedByIndex = new Map();
317
318
  for (const indices of indicesByStream.values()) {
318
- const hasUsage = segment[indices[0]].observation.payload.usage !== undefined;
319
- const retainedPositions = hasUsage ? [0] : [];
320
- if (hasUsage) {
321
- for (let position = 1; position < indices.length; position += 1) {
322
- const previous = segment[indices[position - 1]].observation.payload.usage;
323
- const current = segment[indices[position]].observation.payload.usage;
324
- if (runtimeUsageTotal(current) < runtimeUsageTotal(previous)) {
325
- retainedPositions.push(position);
326
- }
327
- }
328
- }
319
+ const hasUsage = isRuntimeTokenEvidence(segment[indices[0]].observation);
320
+ // Usage facts and incomplete boundaries are authoritative history for
321
+ // read-only Session token projection, so ingress never coalesces them.
322
+ const retainedPositions = hasUsage ? indices.map((_, position) => position) : [];
329
323
  const lastPosition = indices.length - 1;
330
324
  if (retainedPositions.at(-1) !== lastPosition)
331
325
  retainedPositions.push(lastPosition);
@@ -424,12 +418,9 @@ function progressStreamKey(event) {
424
418
  fence.nativeSessionId ?? null,
425
419
  fence.runId ?? null,
426
420
  payload.activity,
427
- payload.usage === undefined ? "signal" : "usage"
421
+ isRuntimeTokenEvidence(event.observation) ? "usage" : "signal"
428
422
  ]);
429
423
  }
430
- function runtimeUsageTotal(usage) {
431
- return usage.inputTokens + usage.outputTokens;
432
- }
433
424
  function isRuntimeActivityEvent(event) {
434
425
  return event.type === "runtime-observation"
435
426
  && event.observation.kind === "activity.observed";
@@ -536,6 +527,8 @@ export function createAsyncRuntimeObserver(invoke) {
536
527
  observeRuntimeObservation: (input, now) => withNow("observeRuntimeObservation", input, now),
537
528
  observeRuntimeTurnCompleted: (input, now) => withNow("observeRuntimeTurnCompleted", input, now),
538
529
  observeGlobalRuntimeTurnCompleted: (input, now) => withNow("observeGlobalRuntimeTurnCompleted", input, now),
530
+ classifyRuntimeTurnCompleted: (input) => call("classifyRuntimeTurnCompleted", [input]),
531
+ classifyGlobalRuntimeTurnCompleted: (input) => call("classifyGlobalRuntimeTurnCompleted", [input]),
539
532
  observeObsoleteRuntimeEvent: (input, now) => withNow("observeObsoleteRuntimeEvent", input, now)
540
533
  };
541
534
  }
@@ -581,7 +574,7 @@ export class AsyncRuntimeEventProcessor {
581
574
  try {
582
575
  let outcome = "applied";
583
576
  if (event.type === "native-turn-completed") {
584
- outcome = await this.applyCodex(event, now);
577
+ outcome = await this.applyNativeTurnCompleted(event, now);
585
578
  }
586
579
  else if (event.type === "runtime-observation") {
587
580
  outcome = await this.applyRuntimeObservation(event, now);
@@ -652,7 +645,7 @@ export class AsyncRuntimeEventProcessor {
652
645
  }
653
646
  return outcome;
654
647
  }
655
- async applyCodex(event, now) {
648
+ async applyNativeTurnCompleted(event, now) {
656
649
  if (event.scope === "task") {
657
650
  const task = await this.observer.getTask(event.taskId);
658
651
  const input = {
@@ -12,6 +12,10 @@ import { resolveRuntimeHookRunFence } from "./runtimeHookRunFence.js";
12
12
  * provider-independent RuntimeObservation with an exact generation/Run fence.
13
13
  */
14
14
  export async function runRuntimeObservationHookCommand(stdinJson, environment = process.env, call = callController, now = new Date(), dependencies = {}) {
15
+ if (environment.YUI_SESSION_SCOPE === "global") {
16
+ await runGlobalRuntimeTurnHook(stdinJson, environment, call, now, dependencies);
17
+ return;
18
+ }
15
19
  const parsed = parseRuntimeObservationHook(stdinJson, environment, now, dependencies);
16
20
  const inbox = new FileRuntimeEventInbox(parsed.home);
17
21
  for (const observation of parsed.observations)
@@ -26,6 +30,47 @@ export async function runRuntimeObservationHookCommand(stdinJson, environment =
26
30
  })
27
31
  }, { timeoutMs: 100 }).catch(() => { });
28
32
  }
33
+ async function runGlobalRuntimeTurnHook(stdinJson, environment, call, now, dependencies) {
34
+ const payload = parseObject(stdinJson);
35
+ const hookEventName = requireIdentity(payload.hook_event_name, "Agent Driver Hook event name");
36
+ if (hookEventName !== "Stop" && hookEventName !== "StopFailure")
37
+ return;
38
+ const driverId = requireIdentity(environment.YUI_DRIVER_ID, "Agent Driver id");
39
+ const driver = (dependencies.drivers ?? builtinAgentDriverRegistry()).require(driverId);
40
+ if (driver.adapterId !== "claude" || environment.YUI_ADAPTER_ID !== driver.adapterId) {
41
+ throw new Error("Global runtime Hook requires the Claude adapter.");
42
+ }
43
+ const occurrenceId = `${now.toISOString()}:${(dependencies.sequence ?? monotonicSequence)()}`;
44
+ const nativeHook = Object.freeze({ hookEventName, payload, occurrenceId });
45
+ const nativeSessionId = requireIdentity(driver.runtime.nativeSessionId(nativeHook), "Agent Driver native Session id");
46
+ if (nativeSessionId !== requireIdentity(environment.YUI_NATIVE_SESSION_ID, "YUI native session id")) {
47
+ throw new Error("Global runtime Hook native Session does not match its launch envelope.");
48
+ }
49
+ const home = requireIdentity(environment.YUI_HOME, "YUI_HOME");
50
+ const roleName = requireIdentity(environment.YUI_ROLE, "Role name");
51
+ new FileRuntimeEventInbox(home, () => now).enqueueTurnCompleted({
52
+ scope: "global",
53
+ roleName,
54
+ agentId: requireIdentity(environment.YUI_AGENT_ID, "Agent id"),
55
+ adapterId: "claude",
56
+ launchId: requireIdentity(environment.YUI_LAUNCH_ID, "Launch id"),
57
+ nativeSessionId,
58
+ turnId: optionalIdentity(driver.runtime.nativeTurnId(nativeHook)) ?? occurrenceId,
59
+ ...(environment.YUI_SESSION_TITLE === undefined
60
+ ? {}
61
+ : { title: requireIdentity(environment.YUI_SESSION_TITLE, "Session title") }),
62
+ summary: globalHookSummary(payload, hookEventName)
63
+ });
64
+ await call(home, "scheduler.signal", { key: runtimeLifecycleSignalKey({ scope: "global", roleName }) }, { timeoutMs: 100 }).catch(() => { });
65
+ }
66
+ function globalHookSummary(payload, hookEventName) {
67
+ for (const field of ["last_assistant_message", "summary", "message", "error"]) {
68
+ const value = payload[field];
69
+ if (typeof value === "string" && value.trim().length > 0)
70
+ return value.trim();
71
+ }
72
+ return hookEventName === "Stop" ? "Native Turn completed." : "Native Turn failed.";
73
+ }
29
74
  export function parseRuntimeObservationHook(stdinJson, environment, now = new Date(), dependencies = {}) {
30
75
  const payload = parseObject(stdinJson);
31
76
  const drivers = dependencies.drivers ?? builtinAgentDriverRegistry();
@@ -3,9 +3,13 @@ import { requireIdentity, requireText, requireTimestamp } from "../domain/valida
3
3
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
4
4
  import { validateExecutionGroup } from "./executionGroup.js";
5
5
  export const RESOURCE_BROKER_POLICY_SCHEMA_VERSION = 1;
6
+ /** Compatibility/display default; observed tokens never close execution spend. */
7
+ export const DEFAULT_EXECUTION_STAGE_TOKEN_BUDGET_PER_LANE = 120_000;
6
8
  /** One shared hard-stop predicate for admission, recovery, and Leader routing. */
7
9
  export function executionStageSpendClosed(resources) {
8
- return resources.deadlineReached || resources.exhaustedBudgets.length > 0;
10
+ return resources.deadlineReached
11
+ || resources.exhaustedBudgets.includes("tool-calls")
12
+ || resources.exhaustedBudgets.includes("wall-clock");
9
13
  }
10
14
  const DEFAULT_ACTIVE_LANES = 4;
11
15
  const DEFAULT_ACTIVE_LANES_PER_TASK = 2;
@@ -147,9 +151,6 @@ export function projectExecutionStageResources(input) {
147
151
  const tokensObservable = input.usage.tokensObservable ?? true;
148
152
  const toolCallsObservable = input.usage.toolCallsObservable ?? true;
149
153
  const exhaustedBudgets = [];
150
- if (stage?.budget.maxTokens !== undefined
151
- && tokens >= stage.budget.maxTokens)
152
- exhaustedBudgets.push("tokens");
153
154
  if (toolCallsObservable && stage?.budget.maxToolCalls !== undefined
154
155
  && toolCalls >= stage.budget.maxToolCalls)
155
156
  exhaustedBudgets.push("tool-calls");
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { hasRecentTurnId, rememberRecentTurnId, validatePendingTurnCompletion, validateRecentTurnIds } from "./turnCompletion.js";
3
- import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain, effectiveLaunchSnapshotsCompatibleForTaskReview, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
3
+ import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
4
4
  import { rebindProviderRuntimeRun, validateProviderRuntimeBinding } from "../runtime/providerRuntimeIdentity.js";
5
5
  import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
6
6
  export function createRoleSessionSet(owner, activeAgentId, now) {
@@ -69,7 +69,7 @@ export function recordRoleAgentSession(set, input, now) {
69
69
  if (existing !== undefined && existing.nativeSessionId === nativeSessionId
70
70
  && !effectiveLaunchSnapshotsCompatible(existing.effective, effective)
71
71
  && !(set.owner.scope === "task"
72
- && effectiveLaunchSnapshotsCompatibleForTaskReview(existing.effective, effective))) {
72
+ && effectiveLaunchSnapshotsCompatibleForTaskSession(existing.effective, effective))) {
73
73
  throw new Error(`Role Agent session effective launch cannot change: ${agentId}.`);
74
74
  }
75
75
  if (existing !== undefined && existing.nativeSessionId !== nativeSessionId
@@ -264,7 +264,7 @@ export function rememberRoleAgentCompletedTurn(set, agentId, nativeSessionId, tu
264
264
  };
265
265
  return validateRoleSessionSet(updated);
266
266
  }
267
- export function roleAgentSessionResumeMode(set, agentId, desired, workspace) {
267
+ export function roleAgentSessionResumeMode(set, agentId, desired) {
268
268
  if (set === null)
269
269
  return "new";
270
270
  validateRoleSessionSet(set);
@@ -287,7 +287,7 @@ export function roleAgentSessionResumeMode(set, agentId, desired, workspace) {
287
287
  return "new";
288
288
  }
289
289
  const compatible = set.owner.scope === "task"
290
- ? effectiveLaunchSnapshotsCompatibleForTaskMain(session.effective, desired, workspace)
290
+ ? effectiveLaunchSnapshotsCompatibleForTaskSession(session.effective, desired)
291
291
  : effectiveLaunchSnapshotsCompatible(session.effective, desired);
292
292
  if (compatible)
293
293
  return "resume";
@@ -79,50 +79,20 @@ export function effectiveLaunchSnapshotsCompatible(existing, desired) {
79
79
  return isDeepStrictEqual(withoutDesiredRevision(existing), withoutDesiredRevision(desired));
80
80
  }
81
81
  /**
82
- * A Task-owned main workspace is a mutable checkout: Integration advances its
83
- * durable Git heads while an idle native Session keeps the immutable launch
84
- * configuration with which it started. A later Run may resume that exact
85
- * Session only when the current durable Task workspace proves that every
86
- * non-commit workspace identity and every other launch field is unchanged.
87
- * A Task-final Reviewer workspace is also mutable between semantic Rounds: it
88
- * keeps one Role-owned physical identity while its frozen commits and Round
89
- * identity advance. WorkItem and ExecutionLane workspaces remain strict.
82
+ * Task Role Sessions keep one physical workspace while Run-scoped facts move.
83
+ * Candidate commits, ReviewRound identity and desired-revision bookkeeping do
84
+ * not define a native Session. Agent, adapter, permission, model, sandbox,
85
+ * manifest, Role context and physical workspace identity still do.
90
86
  */
91
- export function effectiveLaunchSnapshotsCompatibleForTaskMain(existing, desired, workspace) {
87
+ export function effectiveLaunchSnapshotsCompatibleForTaskSession(existing, desired) {
92
88
  if (effectiveLaunchSnapshotsCompatible(existing, desired))
93
89
  return true;
94
90
  validateEffectiveLaunchSnapshot(existing);
95
91
  validateEffectiveLaunchSnapshot(desired);
96
- if (workspace === null || workspace === undefined)
92
+ if ((existing.reviewRoundId === undefined) !== (desired.reviewRoundId === undefined)) {
97
93
  return false;
98
- validateManagedWorkspace(workspace);
99
- const durableWorkspace = {
100
- root: workspace.root,
101
- entries: workspace.entries.map((entry) => ({ ...entry }))
102
- };
103
- if (!isDeepStrictEqual(desired.workspace, durableWorkspace))
104
- return false;
105
- if (workspace.owner.type === "review-round") {
106
- return effectiveLaunchSnapshotsCompatibleForTaskReview(existing, desired);
107
94
  }
108
- if (workspace.owner.type !== "task")
109
- return false;
110
- return isDeepStrictEqual(taskMainCompatibleSnapshot(existing), taskMainCompatibleSnapshot(desired));
111
- }
112
- /**
113
- * Same Task Reviewer Role, same stable physical workspace and launch policy,
114
- * but a new semantic ReviewRound and frozen head. The native Session keeps its
115
- * conversation; each AgentRun still records the new exact Round snapshot.
116
- */
117
- export function effectiveLaunchSnapshotsCompatibleForTaskReview(existing, desired) {
118
- validateEffectiveLaunchSnapshot(existing);
119
- validateEffectiveLaunchSnapshot(desired);
120
- if (existing.reviewRoundId === undefined
121
- || existing.reviewBaseCommit === undefined
122
- || desired.reviewRoundId === undefined
123
- || desired.reviewBaseCommit === undefined)
124
- return false;
125
- return isDeepStrictEqual(taskReviewCompatibleSnapshot(existing), taskReviewCompatibleSnapshot(desired));
95
+ return isDeepStrictEqual(taskSessionCompatibleSnapshot(existing), taskSessionCompatibleSnapshot(desired));
126
96
  }
127
97
  /** Preserves a fixed Session's launch configuration while freezing fresh Task-main Git facts. */
128
98
  export function effectiveLaunchWithTaskMainWorkspace(existing, workspace) {
@@ -139,17 +109,7 @@ export function effectiveLaunchWithTaskMainWorkspace(existing, workspace) {
139
109
  }
140
110
  });
141
111
  }
142
- function taskMainCompatibleSnapshot(snapshot) {
143
- const { sourceDesiredRevision: _sourceDesiredRevision, workspace, ...launch } = snapshot;
144
- return {
145
- ...launch,
146
- workspace: {
147
- root: workspace.root,
148
- entries: workspace.entries.map(({ baseCommit: _baseCommit, ...entry }) => entry)
149
- }
150
- };
151
- }
152
- function taskReviewCompatibleSnapshot(snapshot) {
112
+ function taskSessionCompatibleSnapshot(snapshot) {
153
113
  const { sourceDesiredRevision: _sourceDesiredRevision, reviewRoundId: _reviewRoundId, reviewBaseCommit: _reviewBaseCommit, workspace, ...launch } = snapshot;
154
114
  return {
155
115
  ...launch,
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
2
3
  import { realpathSync } from "node:fs";
3
4
  import { join, resolve } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
@@ -17,7 +18,7 @@ import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
17
18
  import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
18
19
  import { classifyWorkspacePreflight, formatWorkspacePreflightError } from "./workspacePreflightClassification.js";
19
20
  import { activeLiveRoleAgentSession } from "./agentExecutor.js";
20
- import { effectiveLaunchSnapshotsCompatibleForTaskMain, effectiveLaunchSnapshotsCompatible, effectiveRoleForLaunch, resolveEffectiveLaunch } from "./effectiveLaunch.js";
21
+ import { effectiveLaunchSnapshotsCompatibleForTaskSession, effectiveLaunchSnapshotsCompatible, effectiveRoleForLaunch, resolveEffectiveLaunch } from "./effectiveLaunch.js";
21
22
  import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeState, createExactControlPlaneDescriptor, createExactTaskRuntimeDescriptor, exactControlPlaneDigest, exactTaskRuntimeDescriptorPath, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
22
23
  import { detectRunningRelease } from "../release/runtimeRelease.js";
23
24
  import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
@@ -35,6 +36,7 @@ export class FileRoleLaunchPlanner {
35
36
  #nativeAgentEnvironment;
36
37
  #createNativeSessionId;
37
38
  #cliPath;
39
+ #inspectWorkspacePhysicalState;
38
40
  #controlPlane;
39
41
  #resourceRegistrarValue;
40
42
  constructor(home, store, options = {}) {
@@ -51,6 +53,8 @@ export class FileRoleLaunchPlanner {
51
53
  this.#agentEnvironment = this.#selectConfiguredAgentEnvironment(sourceEnvironment);
52
54
  this.#nativeAgentEnvironment = this.#selectConfiguredNativeEnvironment(sourceEnvironment);
53
55
  this.#createNativeSessionId = options.createNativeSessionId ?? randomUUID;
56
+ this.#inspectWorkspacePhysicalState = options.inspectWorkspacePhysicalState
57
+ ?? inspectWorkspacePhysicalState;
54
58
  this.#cliPath = canonicalPath(options.cliPath
55
59
  ?? fileURLToPath(new URL("../cli.js", import.meta.url)));
56
60
  // Internal callbacks retain one exact command identity for receipt fencing.
@@ -143,7 +147,7 @@ export class FileRoleLaunchPlanner {
143
147
  const main = this.store.getTaskWorkspace(task.id);
144
148
  // Quick Win (EXE-04/EXE-08): classify workspace preflight failures so
145
149
  // split-brain state is never reported as a transient Provider failure.
146
- const preflight = classifyWorkspacePreflight(this.store, task, input.roleName, activeRun === null ? null : { id: activeRun.id, workspace: activeRun.workspace });
150
+ const preflight = classifyWorkspacePreflight(this.store, task, input.roleName, activeRun === null ? null : { id: activeRun.id, workspace: activeRun.workspace }, this.#inspectWorkspacePhysicalState);
147
151
  if (preflight !== null) {
148
152
  throw new Error(formatWorkspacePreflightError(preflight));
149
153
  }
@@ -215,7 +219,7 @@ export class FileRoleLaunchPlanner {
215
219
  const existing = sessionSet?.sessions[effective.agentId];
216
220
  const compatibleExisting = existing !== undefined
217
221
  && (input.mode === "resume"
218
- ? effectiveLaunchSnapshotsCompatibleForTaskMain(existing.effective, effective, runWorkspace ?? workspace)
222
+ ? effectiveLaunchSnapshotsCompatibleForTaskSession(existing.effective, effective)
219
223
  : effectiveLaunchSnapshotsCompatible(existing.effective, effective));
220
224
  if (input.mode === "resume" && !compatibleExisting) {
221
225
  throw new Error(`Task Role resume effective snapshot drifted: ${task.id}/${role.name}.`);
@@ -322,9 +326,6 @@ export class FileRoleLaunchPlanner {
322
326
  const managedRun = owner.scope === "task" && input.runId !== undefined
323
327
  ? this.store.getAgentRun(owner.taskId, input.runId)
324
328
  : null;
325
- const managedSessionSet = owner.scope === "task" && input.runId !== undefined
326
- ? this.store.getTaskRoleSessionSet(owner.taskId, role.name)
327
- : null;
328
329
  const driver = builtinAgentDriverRegistry().require(builtinDriverIdForAdapter(configured.adapterId));
329
330
  if (owner.scope === "task" && input.runId !== undefined) {
330
331
  const admission = managedRuntimeAdmission(driver.capabilities);
@@ -401,6 +402,9 @@ export class FileRoleLaunchPlanner {
401
402
  let args = [...compiled.argv];
402
403
  let command = configured.command;
403
404
  let session;
405
+ if (binding.adapterId === "claude" && (managedControl || owner.scope === "global")) {
406
+ args.push("--plugin-dir", ensureClaudeLifecyclePlugin(this.home, this.#cliPath));
407
+ }
404
408
  if (binding.adapterId === "codex") {
405
409
  // Global/interactive Codex sessions still use notify for presentation.
406
410
  // Managed Runs use the structured Driver Hook as their sole lifecycle
@@ -423,8 +427,6 @@ export class FileRoleLaunchPlanner {
423
427
  : null;
424
428
  }
425
429
  else if (launchMode === "new") {
426
- if (managedControl)
427
- args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
428
430
  const nativeSessionId = requireText(preallocatedNativeSessionId, "Native session id");
429
431
  if (!managedControl)
430
432
  args.push("--session-id", nativeSessionId);
@@ -433,9 +435,6 @@ export class FileRoleLaunchPlanner {
433
435
  session = readySession(input.agentId, binding.adapterId, nativeSessionId, effective);
434
436
  }
435
437
  else {
436
- if (managedControl) {
437
- args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
438
- }
439
438
  session = readySession(input.agentId, binding.adapterId, resumeNativeSessionId, effective);
440
439
  }
441
440
  const managedClaudeRun = binding.adapterId === "claude"
@@ -473,10 +472,12 @@ export class FileRoleLaunchPlanner {
473
472
  if (owner.scope === "task" && (input.mode === "new" || input.mode === "resume")) {
474
473
  jobCallerKey = randomBytes(32).toString("hex");
475
474
  }
475
+ // Every managed launch for an unpushed Run owns that Run's first Provider
476
+ // write, whether it creates or resumes the native Conversation. Keeping
477
+ // resume on the same launch handshake prevents a durable AgentRun from
478
+ // existing without a corresponding Provider Turn.
476
479
  const carriesInitialTurn = managedControl
477
- && managedRun?.pushedAt === undefined
478
- && (managedSessionSet?.providerBinding === null
479
- || managedSessionSet?.providerBinding === undefined);
480
+ && managedRun?.pushedAt === undefined;
480
481
  const providerAuthority = managedControl
481
482
  ? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.launchId)
482
483
  : undefined;
@@ -698,11 +699,11 @@ function workspaceScopeEnvironment(environment, workspace) {
698
699
  ])))
699
700
  };
700
701
  }
701
- function ensureManagedClaudeLifecyclePlugin(home, cliPath) {
702
+ function ensureClaudeLifecyclePlugin(home, cliPath) {
702
703
  const root = join(resolve(home), "runtime", "claude-lifecycle-plugin");
703
704
  writeTextFileAtomically(join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify({
704
705
  name: "yui-runtime-lifecycle",
705
- description: "Yui-owned exact Run lifecycle transport",
706
+ description: "Yui-owned native lifecycle transport",
706
707
  version: "1.0.0"
707
708
  }, null, 2)}\n`);
708
709
  const command = {
@@ -766,6 +767,25 @@ function canonicalPath(path) {
766
767
  return absolute;
767
768
  }
768
769
  }
770
+ const inspectWorkspacePhysicalState = (entry) => {
771
+ const headResult = spawnSync("git", ["-C", entry.path, "rev-parse", "HEAD"], { encoding: "utf8", shell: false, timeout: 5_000 });
772
+ if (headResult.error !== undefined || headResult.status !== 0) {
773
+ throw new Error(`Managed workspace physical HEAD could not be inspected: ${entry.projectId} @ ${entry.path}.`);
774
+ }
775
+ const physicalCommit = headResult.stdout.trim().toLowerCase();
776
+ if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(physicalCommit)) {
777
+ throw new Error(`Managed workspace physical HEAD is invalid: ${entry.projectId}.`);
778
+ }
779
+ const ancestorResult = spawnSync("git", ["-C", entry.path, "merge-base", "--is-ancestor", entry.baseCommit, physicalCommit], { encoding: "utf8", shell: false, timeout: 5_000 });
780
+ if (ancestorResult.error !== undefined
781
+ || (ancestorResult.status !== 0 && ancestorResult.status !== 1)) {
782
+ throw new Error(`Managed workspace lineage could not be inspected: ${entry.projectId} @ ${entry.path}.`);
783
+ }
784
+ return {
785
+ physicalCommit,
786
+ recordedBaseIsAncestor: ancestorResult.status === 0
787
+ };
788
+ };
769
789
  function shellQuote(value) {
770
790
  return `'${value.replaceAll("'", "'\"'\"'")}'`;
771
791
  }
@@ -21,7 +21,7 @@ function ownerLabel(owner) {
21
21
  * This is a read-only check: it never mutates Git state, creates Sessions, or
22
22
  * writes recovery records.
23
23
  */
24
- export function classifyWorkspacePreflight(store, task, roleName, activeRun) {
24
+ export function classifyWorkspacePreflight(store, task, roleName, activeRun, inspectPhysical) {
25
25
  const main = store.getTaskWorkspace(task.id);
26
26
  const bindings = task.projectBindings.map(({ projectId, directory }) => ({ projectId, directory }));
27
27
  // 1. Owner validation: the Task main workspace must be a durable,
@@ -83,6 +83,27 @@ export function classifyWorkspacePreflight(store, task, roleName, activeRun) {
83
83
  };
84
84
  }
85
85
  }
86
+ // 3. Physical validation: committed work may advance a managed branch, so
87
+ // HEAD need not equal its recorded base. It must, however, still descend
88
+ // from that immutable boundary. A reset/repoint outside the lineage is
89
+ // physical drift and must fail before Provider launch.
90
+ if (inspectPhysical !== undefined) {
91
+ const effective = activeRun?.workspace ?? main;
92
+ for (const entry of effective.entries) {
93
+ const physical = inspectPhysical(entry);
94
+ if (!physical.recordedBaseIsAncestor) {
95
+ return {
96
+ kind: "physical-drift",
97
+ reason: `Managed workspace physical HEAD left its recorded lineage: ${task.id}/${roleName}.`,
98
+ taskId: task.id,
99
+ roleName,
100
+ projectId: entry.projectId,
101
+ expectedCommit: entry.baseCommit,
102
+ physicalCommit: physical.physicalCommit
103
+ };
104
+ }
105
+ }
106
+ }
86
107
  return null;
87
108
  }
88
109
  /**
@@ -108,7 +129,7 @@ export function formatWorkspacePreflightError(classification) {
108
129
  + "` to inspect the split state, then repair or re-sync the workspace.");
109
130
  break;
110
131
  case "physical-drift":
111
- lines.push(` Project ${classification.projectId}: expected ${classification.expectedCommit}, physical HEAD is ${classification.physicalCommit}.`);
132
+ lines.push(` Project ${classification.projectId}: recorded base ${classification.expectedCommit}, physical HEAD is ${classification.physicalCommit}.`);
112
133
  lines.push(" The physical workspace has drifted from the recorded baseline.");
113
134
  lines.push(" Use `yui task base status " + classification.taskId + "` to inspect the drift.");
114
135
  break;