@zq-silk/yui 0.11.3 → 0.12.1

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 (108) hide show
  1. package/ARCHITECTURE.md +23 -4
  2. package/README.md +44 -22
  3. package/dist/cli/commandCatalog.js +13 -10
  4. package/dist/cli/interactionPolicy.js +5 -6
  5. package/dist/cli/updateCommand.js +3 -1
  6. package/dist/cli/updateOrchestrator.js +173 -28
  7. package/dist/cli/updatePorts.js +137 -8
  8. package/dist/cli/upgradeCommand.js +19 -9
  9. package/dist/cli.js +156 -57
  10. package/dist/commands/configCommands.js +13 -46
  11. package/dist/commands/executionAuditCommands.js +3 -2
  12. package/dist/commands/jobCommands.js +4 -10
  13. package/dist/commands/taskCommands.js +234 -200
  14. package/dist/commands/taskCompletionGate.js +29 -54
  15. package/dist/commands/taskContextCommand.js +61 -13
  16. package/dist/commands/taskInputCommands.js +8 -5
  17. package/dist/commands/taskNextActionCommand.js +56 -6
  18. package/dist/commands/taskOverviewCommand.js +17 -31
  19. package/dist/commands/taskRoleRuntimeStatus.js +55 -7
  20. package/dist/commands/taskWorkspaceCommands.js +5 -2
  21. package/dist/config/configCatalog.js +3 -3
  22. package/dist/config/yuiConfig.js +3 -7
  23. package/dist/context/wakeNotification.js +20 -5
  24. package/dist/controller/agentRuntimeObserver.js +247 -51
  25. package/dist/controller/clientRuntime.js +39 -3
  26. package/dist/controller/controller.js +31 -27
  27. package/dist/controller/fileSchedulerStoreAdapter.js +390 -332
  28. package/dist/controller/runtime.js +58 -3
  29. package/dist/controller/runtimeEventInbox.js +17 -7
  30. package/dist/controller/runtimeEventProcessor.js +12 -19
  31. package/dist/controller/runtimeHookRunFence.js +19 -4
  32. package/dist/controller/runtimeObservationHook.js +45 -0
  33. package/dist/controller/structuredProviderObservation.js +20 -3
  34. package/dist/core/controllerClient.js +20 -2
  35. package/dist/core/controllerServer.js +1 -0
  36. package/dist/execution/resourceBroker.js +5 -4
  37. package/dist/executor/agentExecutor.js +52 -50
  38. package/dist/executor/effectiveLaunch.js +8 -48
  39. package/dist/executor/fileRoleLaunchPlanner.js +129 -45
  40. package/dist/executor/workspacePreflightClassification.js +23 -2
  41. package/dist/interaction/operatorPresentation.js +33 -89
  42. package/dist/lifecycle/exactRunTerminalization.js +69 -5
  43. package/dist/observability/executionAudit.js +5 -0
  44. package/dist/observability/orchestrationMetrics.js +8 -3
  45. package/dist/profile/agentProfile.js +1 -1
  46. package/dist/release/cliHomeReleaseFence.js +123 -0
  47. package/dist/release/runtimeRelease.js +20 -0
  48. package/dist/repository/taskWorkspacePreparer.js +176 -60
  49. package/dist/resources/sqliteResourceRegistry.js +1 -1
  50. package/dist/review/deltaRecheck.js +12 -51
  51. package/dist/review/reviewAcceptance.js +26 -0
  52. package/dist/review/reviewConfig.js +0 -31
  53. package/dist/review/reviewDecision.js +113 -0
  54. package/dist/review/reviewOutcomeClassifier.js +1 -1
  55. package/dist/review/reviewRound.js +1 -1
  56. package/dist/review/reviewerAvailability.js +69 -0
  57. package/dist/run/recoveryProjection.js +45 -6
  58. package/dist/runtime/agentDriverObservation.js +24 -10
  59. package/dist/runtime/agentHost.js +159 -85
  60. package/dist/runtime/builtinAgentDrivers.js +4 -3
  61. package/dist/runtime/builtinTranscriptObserver.js +301 -64
  62. package/dist/runtime/builtinTranscriptUsage.js +9 -7
  63. package/dist/runtime/conversationSwitch.js +277 -0
  64. package/dist/runtime/index.js +2 -1
  65. package/dist/runtime/launchBroker.js +12 -0
  66. package/dist/runtime/processExitOutbox.js +88 -0
  67. package/dist/runtime/providerRuntimeIdentity.js +29 -1
  68. package/dist/runtime/runtimeHealthPolicy.js +5 -5
  69. package/dist/runtime/runtimeObservation.js +28 -0
  70. package/dist/runtime/runtimeProjection.js +22 -32
  71. package/dist/runtime/sessionTokenMetrics.js +181 -0
  72. package/dist/runtime/structuredProviderHost.js +7 -1
  73. package/dist/runtime/tmuxAdapters.js +4 -1
  74. package/dist/scheduler/activeRoleRunDelivery.js +34 -199
  75. package/dist/scheduler/activeTaskProgress.js +7 -6
  76. package/dist/scheduler/leaderWakeupProcessor.js +42 -138
  77. package/dist/scheduler/operatorEvent.js +34 -0
  78. package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
  79. package/dist/scheduler/roleRunStall.js +53 -31
  80. package/dist/scheduler/taskExecutionProjection.js +97 -76
  81. package/dist/scheduler/taskObservabilityProjection.js +10 -11
  82. package/dist/storage/migration/productionRegistry.js +379 -0
  83. package/dist/storage/sqliteSchema.js +66 -23
  84. package/dist/storage/sqliteStore.js +46 -23
  85. package/dist/storage/storeRpc.js +0 -1
  86. package/dist/storage/taskStore.js +15 -30
  87. package/dist/storage/upgrade/homeClassification.js +52 -0
  88. package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
  89. package/dist/storage/upgrade/recordVersions.js +3 -2
  90. package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
  91. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
  92. package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
  93. package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
  94. package/dist/task/completionReadiness.js +10 -6
  95. package/dist/task/nextAction.js +82 -58
  96. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  97. package/dist/web/assets/client/components.js +13 -3
  98. package/dist/web/assets/client/i18n.js +6 -2
  99. package/dist/web/webSnapshot.js +7 -1
  100. package/i18n/README.zh-CN.md +2 -2
  101. package/package.json +1 -1
  102. package/skills/yui-leader/SKILL.md +60 -20
  103. package/skills/yui-operator/SKILL.md +13 -4
  104. package/skills/yui-reviewer/SKILL.md +35 -11
  105. package/dist/context/sessionContextBudget.js +0 -71
  106. package/dist/lifecycle/contextBudgetRollover.js +0 -81
  107. package/dist/lifecycle/taskRoleSessionReset.js +0 -126
  108. package/dist/scheduler/operatorNotification.js +0 -59
@@ -1,7 +1,7 @@
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";
4
- import { rebindProviderRuntimeRun, validateProviderRuntimeBinding } from "../runtime/providerRuntimeIdentity.js";
3
+ import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
4
+ import { currentProviderActivation, endProviderActivation, rebindProviderRuntimeRun, validateProviderRuntimeBinding } from "../runtime/providerRuntimeIdentity.js";
5
5
  import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
6
6
  export function createRoleSessionSet(owner, activeAgentId, now) {
7
7
  const base = {
@@ -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
@@ -120,6 +120,20 @@ export function recordRoleAgentSession(set, input, now) {
120
120
  validateRoleSessionSet(updated);
121
121
  return updated;
122
122
  }
123
+ /**
124
+ * Atomically archives the quiescent old native Session and binds the Provider
125
+ * Conversation selected by an already-authorized switch. Authorization is
126
+ * deliberately owned by the Controller caller, not inferred here.
127
+ */
128
+ export function replaceTaskRoleAgentSessionForConversationSwitch(set, input, now) {
129
+ validateRoleSessionSet(set);
130
+ const existing = set.sessions[input.agentId];
131
+ if (existing === undefined || existing.nativeSessionId === input.nativeSessionId) {
132
+ return recordRoleAgentSession(set, input, now);
133
+ }
134
+ const terminalized = updateRoleAgentSessionStatus(set, input.agentId, "stopped", now);
135
+ return recordRoleAgentSession(terminalized, input, now);
136
+ }
123
137
  /**
124
138
  * Session titles and previews can originate in native Agent output. Keep them
125
139
  * single-line and inert before they are persisted or rendered in a terminal.
@@ -179,25 +193,6 @@ export function retireTaskRoleSessionsForWorkspace(set, now) {
179
193
  updatedAt: timestamp
180
194
  });
181
195
  }
182
- /**
183
- * Clears the Provider transport identity after its physical runtime is proven
184
- * stopped, without retiring workspace-bound Session records. Workspace
185
- * retirement remains a separate, stricter transaction after every supported
186
- * placeholder has been terminalized.
187
- */
188
- export function clearTaskRoleProviderRuntimeForCleanup(set, now) {
189
- validateRoleSessionSet(set);
190
- if (set.inFlight !== null) {
191
- throw new Error("Cannot clear a Task Role Provider runtime with unsettled Run state.");
192
- }
193
- if (set.providerBinding === null)
194
- return set;
195
- return validateRoleSessionSet({
196
- ...set,
197
- providerBinding: null,
198
- updatedAt: requireDate(now, "Provider Runtime cleanup timestamp")
199
- });
200
- }
201
196
  /**
202
197
  * Terminalizes only the aggregate-16 Claude placeholder shape after the
203
198
  * caller has fenced the Task store and proved that the exact Role has no live
@@ -264,7 +259,7 @@ export function rememberRoleAgentCompletedTurn(set, agentId, nativeSessionId, tu
264
259
  };
265
260
  return validateRoleSessionSet(updated);
266
261
  }
267
- export function roleAgentSessionResumeMode(set, agentId, desired, workspace) {
262
+ export function roleAgentSessionResumeMode(set, agentId, desired) {
268
263
  if (set === null)
269
264
  return "new";
270
265
  validateRoleSessionSet(set);
@@ -287,7 +282,7 @@ export function roleAgentSessionResumeMode(set, agentId, desired, workspace) {
287
282
  return "new";
288
283
  }
289
284
  const compatible = set.owner.scope === "task"
290
- ? effectiveLaunchSnapshotsCompatibleForTaskMain(session.effective, desired, workspace)
285
+ ? effectiveLaunchSnapshotsCompatibleForTaskSession(session.effective, desired)
291
286
  : effectiveLaunchSnapshotsCompatible(session.effective, desired);
292
287
  if (compatible)
293
288
  return "resume";
@@ -307,7 +302,10 @@ export function bindTaskRoleRun(set, fence, preparedAt, mode) {
307
302
  throw new Error("Task Role session set already has an in-flight Run.");
308
303
  }
309
304
  const timestamp = requireDate(preparedAt, "Task Role Run preparedAt");
310
- const providerBinding = mode === "resume" && set.providerBinding !== null
305
+ // A fresh Conversation is a two-phase replacement: keep the old binding as
306
+ // current evidence until the new Provider session is observed and atomically
307
+ // superseded. Homes with no prior Conversation still start from null.
308
+ const providerBinding = set.providerBinding !== null
311
309
  ? rebindProviderRuntimeRun(set.providerBinding, normalized.runId)
312
310
  : null;
313
311
  const updated = {
@@ -372,6 +370,35 @@ export function updateTaskRoleProviderRuntime(set, binding, updatedAt) {
372
370
  updatedAt: requireDate(updatedAt, "Provider Runtime Binding timestamp")
373
371
  });
374
372
  }
373
+ /**
374
+ * Records an exact local Host exit without forgetting the Provider
375
+ * Conversation. The ended Activation releases writer authority, while the
376
+ * current Conversation and any unsettled Turn remain durable recovery fences.
377
+ */
378
+ export function stopTaskRoleRuntimeAfterPhysicalExit(set, now) {
379
+ validateRoleSessionSet(set);
380
+ const active = set.sessions[set.activeAgentId];
381
+ if (active === undefined)
382
+ return set;
383
+ const activation = set.providerBinding === null
384
+ ? null
385
+ : currentProviderActivation(set.providerBinding);
386
+ if (active.status === "stopped") {
387
+ if (activation !== null) {
388
+ throw new Error("Stopped Task Role Session cannot retain a live Provider Activation.");
389
+ }
390
+ return set;
391
+ }
392
+ let updated = updateRoleAgentSessionStatus(set, set.activeAgentId, "stopped", now);
393
+ if (activation !== null) {
394
+ updated = updateTaskRoleProviderRuntime(updated, endProviderActivation(updated.providerBinding, activation.activationId, {
395
+ status: "ended",
396
+ endedAt: now.toISOString(),
397
+ reason: "runtime-physical-exit"
398
+ }), now);
399
+ }
400
+ return updated;
401
+ }
375
402
  export function markTaskRoleRunPushed(set, fence, pushedAt) {
376
403
  validateRoleSessionSet(set);
377
404
  assertTaskRoleSessionSet(set);
@@ -515,31 +542,6 @@ export function terminalizeTaskRoleRunSession(set, fence, terminalAt) {
515
542
  }
516
543
  return updated;
517
544
  }
518
- /**
519
- * Resets the current native generation after its active Run is terminal.
520
- * The Controller separately owns verified process cleanup.
521
- */
522
- export function resetTaskRoleSession(set, now) {
523
- validateRoleSessionSet(set);
524
- const timestamp = requireDate(now, "Task Role Session reset timestamp");
525
- const current = set.sessions[set.activeAgentId];
526
- const sessions = { ...set.sessions };
527
- delete sessions[set.activeAgentId];
528
- const history = current === undefined
529
- ? set.history
530
- : [
531
- ...(set.history ?? []),
532
- { ...current, status: "broken", updatedAt: timestamp }
533
- ];
534
- return validateRoleSessionSet({
535
- ...set,
536
- sessions,
537
- ...(history === undefined ? {} : { history }),
538
- inFlight: null,
539
- providerBinding: null,
540
- updatedAt: timestamp
541
- });
542
- }
543
545
  export function settleTaskRoleCompletion(set, expected, settledAt) {
544
546
  validateRoleSessionSet(set);
545
547
  assertTaskRoleSessionSet(set);
@@ -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";
@@ -11,20 +12,23 @@ import { compileRoleSessionContext, roleSessionKind } from "../context/roleSessi
11
12
  import { materializeSessionBootstrap } from "../context/sessionBootstrapManifest.js";
12
13
  import { serializeRunBootstrapEnvelope, serializeRunHostRecoveryEnvelope } from "../context/runContextContract.js";
13
14
  import { serializeProviderRetryEnvelope } from "../run/providerRetry.js";
15
+ import { serializeWorkflowOutcomeRequestEnvelope } from "../run/runControlRequest.js";
14
16
  import { prefixYuiTitleInput } from "../run/runIdentity.js";
17
+ import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
15
18
  import { resolveAgentAdapter } from "./agentAdapter.js";
16
19
  import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
17
20
  import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
18
21
  import { classifyWorkspacePreflight, formatWorkspacePreflightError } from "./workspacePreflightClassification.js";
19
22
  import { activeLiveRoleAgentSession } from "./agentExecutor.js";
20
- import { effectiveLaunchSnapshotsCompatibleForTaskMain, effectiveLaunchSnapshotsCompatible, effectiveRoleForLaunch, resolveEffectiveLaunch } from "./effectiveLaunch.js";
23
+ import { effectiveLaunchSnapshotsCompatibleForTaskSession, effectiveLaunchSnapshotsCompatible, effectiveRoleForLaunch, resolveEffectiveLaunch } from "./effectiveLaunch.js";
21
24
  import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeState, createExactControlPlaneDescriptor, createExactTaskRuntimeDescriptor, exactControlPlaneDigest, exactTaskRuntimeDescriptorPath, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
22
25
  import { detectRunningRelease } from "../release/runtimeRelease.js";
23
26
  import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
24
27
  import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
25
28
  import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
26
29
  import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
27
- import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
30
+ import { freshConversationLaunchAllowed } from "../runtime/conversationSwitch.js";
31
+ import { currentProviderActivation } from "../runtime/providerRuntimeIdentity.js";
28
32
  import { assertCodexLaunchOverridesAvailable, inspectCodexLaunchConfig } from "./codexConfigConflict.js";
29
33
  /** Builds managed native Agent launches from the authoritative Task records. */
30
34
  export class FileRoleLaunchPlanner {
@@ -35,6 +39,7 @@ export class FileRoleLaunchPlanner {
35
39
  #nativeAgentEnvironment;
36
40
  #createNativeSessionId;
37
41
  #cliPath;
42
+ #inspectWorkspacePhysicalState;
38
43
  #controlPlane;
39
44
  #resourceRegistrarValue;
40
45
  constructor(home, store, options = {}) {
@@ -51,6 +56,8 @@ export class FileRoleLaunchPlanner {
51
56
  this.#agentEnvironment = this.#selectConfiguredAgentEnvironment(sourceEnvironment);
52
57
  this.#nativeAgentEnvironment = this.#selectConfiguredNativeEnvironment(sourceEnvironment);
53
58
  this.#createNativeSessionId = options.createNativeSessionId ?? randomUUID;
59
+ this.#inspectWorkspacePhysicalState = options.inspectWorkspacePhysicalState
60
+ ?? inspectWorkspacePhysicalState;
54
61
  this.#cliPath = canonicalPath(options.cliPath
55
62
  ?? fileURLToPath(new URL("../cli.js", import.meta.url)));
56
63
  // Internal callbacks retain one exact command identity for receipt fencing.
@@ -143,7 +150,7 @@ export class FileRoleLaunchPlanner {
143
150
  const main = this.store.getTaskWorkspace(task.id);
144
151
  // Quick Win (EXE-04/EXE-08): classify workspace preflight failures so
145
152
  // 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 });
153
+ const preflight = classifyWorkspacePreflight(this.store, task, input.roleName, activeRun === null ? null : { id: activeRun.id, workspace: activeRun.workspace }, this.#inspectWorkspacePhysicalState);
147
154
  if (preflight !== null) {
148
155
  throw new Error(formatWorkspacePreflightError(preflight));
149
156
  }
@@ -215,11 +222,25 @@ export class FileRoleLaunchPlanner {
215
222
  const existing = sessionSet?.sessions[effective.agentId];
216
223
  const compatibleExisting = existing !== undefined
217
224
  && (input.mode === "resume"
218
- ? effectiveLaunchSnapshotsCompatibleForTaskMain(existing.effective, effective, runWorkspace ?? workspace)
225
+ ? effectiveLaunchSnapshotsCompatibleForTaskSession(existing.effective, effective)
219
226
  : effectiveLaunchSnapshotsCompatible(existing.effective, effective));
220
227
  if (input.mode === "resume" && !compatibleExisting) {
221
228
  throw new Error(`Task Role resume effective snapshot drifted: ${task.id}/${role.name}.`);
222
229
  }
230
+ if (input.mode === "new" && sessionSet !== null
231
+ && !freshConversationLaunchAllowed({
232
+ sessions: sessionSet,
233
+ events: this.store.listEvents(task.id),
234
+ mailbox: this.store.getWorkMailbox({
235
+ kind: "role",
236
+ taskId: task.id,
237
+ roleName: role.name
238
+ }),
239
+ roleName: role.name,
240
+ ...(input.runId === undefined ? {} : { candidateRunId: input.runId })
241
+ })) {
242
+ throw new Error(`Fresh Provider Conversation is not authorized: ${task.id}/${role.name}.`);
243
+ }
223
244
  return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, runWorkspace, effective, {
224
245
  purpose: activeRun?.purpose ?? "execution"
225
246
  });
@@ -322,9 +343,6 @@ export class FileRoleLaunchPlanner {
322
343
  const managedRun = owner.scope === "task" && input.runId !== undefined
323
344
  ? this.store.getAgentRun(owner.taskId, input.runId)
324
345
  : null;
325
- const managedSessionSet = owner.scope === "task" && input.runId !== undefined
326
- ? this.store.getTaskRoleSessionSet(owner.taskId, role.name)
327
- : null;
328
346
  const driver = builtinAgentDriverRegistry().require(builtinDriverIdForAdapter(configured.adapterId));
329
347
  if (owner.scope === "task" && input.runId !== undefined) {
330
348
  const admission = managedRuntimeAdmission(driver.capabilities);
@@ -401,6 +419,9 @@ export class FileRoleLaunchPlanner {
401
419
  let args = [...compiled.argv];
402
420
  let command = configured.command;
403
421
  let session;
422
+ if (binding.adapterId === "claude" && (managedControl || owner.scope === "global")) {
423
+ args.push("--plugin-dir", ensureClaudeLifecyclePlugin(this.home, this.#cliPath));
424
+ }
404
425
  if (binding.adapterId === "codex") {
405
426
  // Global/interactive Codex sessions still use notify for presentation.
406
427
  // Managed Runs use the structured Driver Hook as their sole lifecycle
@@ -423,8 +444,6 @@ export class FileRoleLaunchPlanner {
423
444
  : null;
424
445
  }
425
446
  else if (launchMode === "new") {
426
- if (managedControl)
427
- args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
428
447
  const nativeSessionId = requireText(preallocatedNativeSessionId, "Native session id");
429
448
  if (!managedControl)
430
449
  args.push("--session-id", nativeSessionId);
@@ -433,9 +452,6 @@ export class FileRoleLaunchPlanner {
433
452
  session = readySession(input.agentId, binding.adapterId, nativeSessionId, effective);
434
453
  }
435
454
  else {
436
- if (managedControl) {
437
- args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
438
- }
439
455
  session = readySession(input.agentId, binding.adapterId, resumeNativeSessionId, effective);
440
456
  }
441
457
  const managedClaudeRun = binding.adapterId === "claude"
@@ -465,45 +481,74 @@ export class FileRoleLaunchPlanner {
465
481
  if (runtimeDescriptor !== undefined && runtimeDescriptorSource !== undefined) {
466
482
  this.#writeExactTaskRuntimeDescriptor(runtimeDescriptor, runtimeDescriptorSource);
467
483
  }
468
- // rr13/rr26: Generate a per-Session DurableJob caller key for every
469
- // task-scope launch, including resume. Resume/ensure may reuse a live
470
- // host, so its candidate hash is committed by TmuxSessionHost only when
471
- // hostCreated=true; otherwise the live process keeps its existing key.
484
+ // Generate a candidate per-Provider-process DurableJob caller key for
485
+ // every task-scope launch. A reused live Conversation keeps the key that
486
+ // its process inherited; a new Host or a Conversation replacement commits
487
+ // the fresh candidate only after Provider dispatch is observed.
472
488
  let jobCallerKey;
473
489
  if (owner.scope === "task" && (input.mode === "new" || input.mode === "resume")) {
474
490
  jobCallerKey = randomBytes(32).toString("hex");
475
491
  }
476
- const carriesInitialTurn = managedControl
477
- && managedRun?.pushedAt === undefined
478
- && (managedSessionSet?.providerBinding === null
479
- || managedSessionSet?.providerBinding === undefined);
492
+ // Every managed launch for an unpushed Run owns that Run's first Provider
493
+ // write, whether it creates or resumes the native Conversation. Keeping
494
+ // resume on the same launch handshake prevents a durable AgentRun from
495
+ // existing without a corresponding Provider Turn.
496
+ if (managedControl && (managedRun === null || managedRun.status !== "active")) {
497
+ throw new Error(`Managed Run is no longer active: ${input.runId}.`);
498
+ }
499
+ const carriesInitialTurn = managedControl && (managedRun.pushedAt === undefined
500
+ || managedRun.providerRetry?.state === "dispatching"
501
+ || managedRun.controlRequest?.state === "dispatching");
480
502
  const providerAuthority = managedControl
481
- ? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.launchId)
503
+ ? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.launchId, input.mode)
482
504
  : undefined;
483
505
  const providerNativeSessionId = binding.adapterId === "claude"
484
506
  ? preallocatedNativeSessionId
485
507
  : resumeNativeSessionId;
486
- const providerControl = managedControl
508
+ const initialTurn = carriesInitialTurn
487
509
  ? {
488
- schemaVersion: 1,
489
- adapterId: binding.adapterId,
490
- transport: managedCompiled.transport,
491
- mode: resumeNativeSessionId === undefined ? "new" : "resume",
492
- ...(providerNativeSessionId === undefined
493
- ? {}
494
- : { nativeSessionId: providerNativeSessionId }),
495
- ...(sessionTitle === undefined ? {} : { sessionTitle }),
496
- authority: providerAuthority,
497
- ...(carriesInitialTurn
498
- ? {
499
- initialTurn: {
500
- attemptId: formatAgentRunReceiptId(owner.taskId, input.runId),
501
- boundedText: managedRunLaunchEnvelope(managedRun, input.mode, sessionTitle)
502
- }
503
- }
504
- : {})
510
+ attemptId: agentRunDeliveryReceiptId(managedRun),
511
+ boundedText: managedRunLaunchEnvelope(managedRun, input.mode, sessionTitle)
505
512
  }
506
513
  : undefined;
514
+ const providerControl = !managedControl
515
+ ? undefined
516
+ : initialTurn === undefined
517
+ ? {
518
+ schemaVersion: 1,
519
+ adapterId: binding.adapterId,
520
+ transport: managedCompiled.transport,
521
+ kind: "ensure",
522
+ mode: "resume",
523
+ nativeSessionId: requireText(providerNativeSessionId, "Managed Provider ensure native session id"),
524
+ ...(sessionTitle === undefined ? {} : { sessionTitle }),
525
+ authority: providerAuthority
526
+ }
527
+ : resumeNativeSessionId === undefined
528
+ ? {
529
+ schemaVersion: 1,
530
+ adapterId: binding.adapterId,
531
+ transport: managedCompiled.transport,
532
+ kind: "new",
533
+ mode: "new",
534
+ ...(providerNativeSessionId === undefined
535
+ ? {}
536
+ : { nativeSessionId: providerNativeSessionId }),
537
+ ...(sessionTitle === undefined ? {} : { sessionTitle }),
538
+ authority: providerAuthority,
539
+ initialTurn
540
+ }
541
+ : {
542
+ schemaVersion: 1,
543
+ adapterId: binding.adapterId,
544
+ transport: managedCompiled.transport,
545
+ kind: "resume",
546
+ mode: "resume",
547
+ nativeSessionId: requireText(providerNativeSessionId, "Managed Provider resume native session id"),
548
+ ...(sessionTitle === undefined ? {} : { sessionTitle }),
549
+ authority: providerAuthority,
550
+ initialTurn
551
+ };
507
552
  const launch = {
508
553
  command,
509
554
  args,
@@ -571,13 +616,26 @@ export class FileRoleLaunchPlanner {
571
616
  : {})
572
617
  };
573
618
  }
574
- #providerAuthorityForLaunch(taskId, roleName, launchId) {
619
+ #providerAuthorityForLaunch(taskId, roleName, launchId, mode) {
575
620
  const activationId = requireText(launchId, "Managed Provider Activation id");
576
621
  const binding = this.store.getTaskRoleSessionSet(taskId, roleName)?.providerBinding;
577
622
  if (binding === null || binding === undefined) {
578
623
  return { epoch: 1, owner: "controller", holderId: activationId };
579
624
  }
580
625
  if (binding.authority.owner === "controller") {
626
+ if (mode === "new") {
627
+ const activation = currentProviderActivation(binding);
628
+ if (activation === null || binding.authority.holderId !== activation.activationId) {
629
+ throw new Error(`Provider Activation is not exact: ${taskId}/${roleName}.`);
630
+ }
631
+ // An actor-requested switch first ends the idle old Activation (+1)
632
+ // and then binds the replacement Activation (+1).
633
+ return {
634
+ epoch: binding.authority.epoch + 2,
635
+ owner: "controller",
636
+ holderId: activationId
637
+ };
638
+ }
581
639
  return {
582
640
  epoch: binding.authority.epoch,
583
641
  owner: "controller",
@@ -641,9 +699,16 @@ function managedRunLaunchEnvelope(run, mode, title) {
641
699
  roleName: run.roleName,
642
700
  retry: run.providerRetry
643
701
  })
644
- : mode === "resume" && run.pushedAt !== undefined
645
- ? serializeRunHostRecoveryEnvelope(run.bootstrapEnvelope)
646
- : serializeRunBootstrapEnvelope(run.bootstrapEnvelope);
702
+ : run.controlRequest?.state === "dispatching"
703
+ ? serializeWorkflowOutcomeRequestEnvelope({
704
+ taskId: run.taskId,
705
+ runId: run.id,
706
+ roleName: run.roleName,
707
+ request: run.controlRequest
708
+ })
709
+ : mode === "resume" && run.pushedAt !== undefined
710
+ ? serializeRunHostRecoveryEnvelope(run.bootstrapEnvelope)
711
+ : serializeRunBootstrapEnvelope(run.bootstrapEnvelope);
647
712
  return title === undefined
648
713
  ? body
649
714
  : prefixYuiTitleInput(body, title);
@@ -698,11 +763,11 @@ function workspaceScopeEnvironment(environment, workspace) {
698
763
  ])))
699
764
  };
700
765
  }
701
- function ensureManagedClaudeLifecyclePlugin(home, cliPath) {
766
+ function ensureClaudeLifecyclePlugin(home, cliPath) {
702
767
  const root = join(resolve(home), "runtime", "claude-lifecycle-plugin");
703
768
  writeTextFileAtomically(join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify({
704
769
  name: "yui-runtime-lifecycle",
705
- description: "Yui-owned exact Run lifecycle transport",
770
+ description: "Yui-owned native lifecycle transport",
706
771
  version: "1.0.0"
707
772
  }, null, 2)}\n`);
708
773
  const command = {
@@ -766,6 +831,25 @@ function canonicalPath(path) {
766
831
  return absolute;
767
832
  }
768
833
  }
834
+ const inspectWorkspacePhysicalState = (entry) => {
835
+ const headResult = spawnSync("git", ["-C", entry.path, "rev-parse", "HEAD"], { encoding: "utf8", shell: false, timeout: 5_000 });
836
+ if (headResult.error !== undefined || headResult.status !== 0) {
837
+ throw new Error(`Managed workspace physical HEAD could not be inspected: ${entry.projectId} @ ${entry.path}.`);
838
+ }
839
+ const physicalCommit = headResult.stdout.trim().toLowerCase();
840
+ if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(physicalCommit)) {
841
+ throw new Error(`Managed workspace physical HEAD is invalid: ${entry.projectId}.`);
842
+ }
843
+ const ancestorResult = spawnSync("git", ["-C", entry.path, "merge-base", "--is-ancestor", entry.baseCommit, physicalCommit], { encoding: "utf8", shell: false, timeout: 5_000 });
844
+ if (ancestorResult.error !== undefined
845
+ || (ancestorResult.status !== 0 && ancestorResult.status !== 1)) {
846
+ throw new Error(`Managed workspace lineage could not be inspected: ${entry.projectId} @ ${entry.path}.`);
847
+ }
848
+ return {
849
+ physicalCommit,
850
+ recordedBaseIsAncestor: ancestorResult.status === 0
851
+ };
852
+ };
769
853
  function shellQuote(value) {
770
854
  return `'${value.replaceAll("'", "'\"'\"'")}'`;
771
855
  }
@@ -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;