@zq-silk/yui 0.2.0 → 0.4.2

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 (208) hide show
  1. package/ARCHITECTURE.md +603 -133
  2. package/README.md +806 -31
  3. package/dist/agent/agent.js +2 -1
  4. package/dist/agent/argumentPolicy.js +3 -1
  5. package/dist/agent/launchEnvironment.js +106 -0
  6. package/dist/agent/managedRuntimeEnvironment.js +34 -0
  7. package/dist/brief/taskBrief.js +11 -1
  8. package/dist/cli/agentConfigurationPicker.js +287 -0
  9. package/dist/cli/commandCatalog.js +488 -60
  10. package/dist/cli/completion.js +146 -22
  11. package/dist/cli/helpRenderer.js +3 -1
  12. package/dist/cli/interactionCandidates.js +53 -15
  13. package/dist/cli/interactionPolicy.js +267 -30
  14. package/dist/cli/interactiveSelection.js +6 -2
  15. package/dist/cli/invocationRouter.js +5 -1
  16. package/dist/cli/operatorWizard.js +87 -0
  17. package/dist/cli/roleOptionCatalog.js +1 -0
  18. package/dist/cli/roleWizard.js +185 -21
  19. package/dist/cli/updateCommand.js +62 -19
  20. package/dist/cli/updateOrchestrator.js +539 -0
  21. package/dist/cli/updatePorts.js +1119 -0
  22. package/dist/cli/upgradeCommand.js +112 -0
  23. package/dist/cli.js +1420 -86
  24. package/dist/commands/agentCommands.js +146 -3
  25. package/dist/commands/configCommands.js +126 -0
  26. package/dist/commands/controllerCommands.js +365 -0
  27. package/dist/commands/globalRoleCommands.js +168 -126
  28. package/dist/commands/jobCommands.js +18 -8
  29. package/dist/commands/operatorCommands.js +159 -9
  30. package/dist/commands/profileCommands.js +203 -0
  31. package/dist/commands/projectCommands.js +650 -0
  32. package/dist/commands/roleConfiguration.js +85 -24
  33. package/dist/commands/roleRuntimeGuard.js +12 -0
  34. package/dist/commands/roleSkillValidation.js +47 -0
  35. package/dist/commands/taskActor.js +127 -0
  36. package/dist/commands/taskCommands.js +4201 -313
  37. package/dist/commands/taskCompletionGate.js +131 -0
  38. package/dist/commands/taskContextCommand.js +244 -30
  39. package/dist/commands/taskInputCommands.js +177 -59
  40. package/dist/commands/taskIntegrationCommands.js +303 -0
  41. package/dist/commands/taskOverviewCommand.js +363 -0
  42. package/dist/commands/taskRoleRuntimeStatus.js +125 -19
  43. package/dist/commands/textInput.js +15 -0
  44. package/dist/completion/completionInstaller.js +26 -22
  45. package/dist/config/yuiConfig.js +4 -3
  46. package/dist/context/dispatchContext.js +90 -38
  47. package/dist/context/roleSessionContext.js +119 -0
  48. package/dist/controller/claudeLifecycleHook.js +203 -0
  49. package/dist/controller/clientRuntime.js +408 -56
  50. package/dist/controller/codexLifecycleHook.js +108 -0
  51. package/dist/controller/controller.js +1089 -32
  52. package/dist/controller/domainIdentity.js +505 -0
  53. package/dist/controller/ephemeralResourceReaper.js +131 -0
  54. package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
  55. package/dist/controller/providerHookRunFence.js +127 -0
  56. package/dist/controller/resourceCleanupLinux.js +286 -0
  57. package/dist/controller/resourceInventory.js +531 -0
  58. package/dist/controller/resourceInventoryLinux.js +610 -0
  59. package/dist/controller/runtime.js +629 -10
  60. package/dist/controller/runtimeEventInbox.js +564 -0
  61. package/dist/controller/runtimeEventProcessor.js +248 -0
  62. package/dist/controller/runtimeLaunchCoordinator.js +477 -0
  63. package/dist/controller/sessionNotify.js +121 -78
  64. package/dist/coordination/deadlineScheduler.js +15 -0
  65. package/dist/coordination/mailboxScheduler.js +108 -0
  66. package/dist/coordination/workMailbox.js +329 -0
  67. package/dist/coordination/workMailboxQueue.js +86 -0
  68. package/dist/core/controllerClient.js +19 -5
  69. package/dist/core/controllerEndpoint.js +37 -0
  70. package/dist/core/controllerServer.js +218 -10
  71. package/dist/core/protocol.js +6 -2
  72. package/dist/decision/decision.js +2 -1
  73. package/dist/doctor/doctor.js +681 -32
  74. package/dist/domain/validation.js +53 -0
  75. package/dist/errors/cliError.js +5 -3
  76. package/dist/event/taskEvent.js +7 -3
  77. package/dist/execution/codexThreadNaming.js +160 -0
  78. package/dist/execution/executionGroup.js +579 -0
  79. package/dist/executor/agentAdapter.js +255 -40
  80. package/dist/executor/agentConfigurationCatalog.js +326 -0
  81. package/dist/executor/agentConfigurationProbe.js +506 -0
  82. package/dist/executor/agentExecutor.js +625 -10
  83. package/dist/executor/codexConfigConflict.js +290 -0
  84. package/dist/executor/effectiveLaunch.js +340 -0
  85. package/dist/executor/executorRegistry.js +238 -36
  86. package/dist/executor/fileRoleLaunchPlanner.js +550 -40
  87. package/dist/executor/turnCompletion.js +126 -0
  88. package/dist/input/inputRequest.js +30 -9
  89. package/dist/integration/changeSet.js +36 -0
  90. package/dist/integration/checkResult.js +24 -0
  91. package/dist/integration/gitIntegrationService.js +695 -0
  92. package/dist/integration/integrationAttempt.js +142 -0
  93. package/dist/interaction/operatorPresentation.js +96 -0
  94. package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
  95. package/dist/lifecycle/exactRunTerminalization.js +572 -0
  96. package/dist/lifecycle/providerLifecycleMapping.js +190 -0
  97. package/dist/lifecycle/taskRoleSessionReset.js +124 -0
  98. package/dist/message/message.js +23 -7
  99. package/dist/milestone/milestone.js +2 -1
  100. package/dist/operator/operatorSessionHistory.js +124 -0
  101. package/dist/output/agentConfigurationPresentation.js +43 -0
  102. package/dist/output/rolePresentation.js +34 -10
  103. package/dist/output/terminal.js +8 -0
  104. package/dist/output/timePresentation.js +55 -0
  105. package/dist/profile/agentProfile.js +128 -0
  106. package/dist/repository/gitWorkspace.js +578 -24
  107. package/dist/repository/project.js +213 -0
  108. package/dist/repository/taskWorkspaceCoordinator.js +392 -0
  109. package/dist/repository/taskWorkspacePreparer.js +1688 -191
  110. package/dist/review/reviewConfig.js +11 -0
  111. package/dist/review/reviewRound.js +399 -0
  112. package/dist/review/taskFinalReviewContract.js +90 -0
  113. package/dist/role/role.js +124 -23
  114. package/dist/run/agentRun.js +155 -12
  115. package/dist/run/runIdentity.js +82 -0
  116. package/dist/runtime/exactControlPlane.js +472 -0
  117. package/dist/runtime/index.js +8 -0
  118. package/dist/runtime/lifecycleReservation.js +38 -0
  119. package/dist/runtime/ports.js +11 -0
  120. package/dist/runtime/preallocatedNativeSession.js +13 -0
  121. package/dist/runtime/promptEnvelope.js +30 -0
  122. package/dist/runtime/runtimeBinding.js +31 -0
  123. package/dist/runtime/runtimeOwner.js +14 -0
  124. package/dist/runtime/sessionLaunchRequest.js +62 -0
  125. package/dist/runtime/sessionTitle.js +54 -0
  126. package/dist/runtime/taskRuntimeIsolation.js +643 -0
  127. package/dist/runtime/tmuxAdapters.js +315 -0
  128. package/dist/runtime/turnCompletion.js +3 -0
  129. package/dist/runtime/validation.js +23 -0
  130. package/dist/scheduler/activeRoleRunDelivery.js +342 -32
  131. package/dist/scheduler/activeTaskProgress.js +63 -0
  132. package/dist/scheduler/leaderFailure.js +2 -1
  133. package/dist/scheduler/leaderWakeupProcessor.js +307 -66
  134. package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
  135. package/dist/scheduler/operatorNotification.js +44 -2
  136. package/dist/scheduler/ports.js +28 -1
  137. package/dist/scheduler/roleRunLiveness.js +131 -25
  138. package/dist/scheduler/roleRunStall.js +951 -0
  139. package/dist/scheduler/taskExecutionProjection.js +544 -0
  140. package/dist/scheduler/wakeupQueue.js +3 -0
  141. package/dist/setup/setupCommand.js +302 -52
  142. package/dist/storage/compatibleTaskStore.js +102 -0
  143. package/dist/storage/migration/baseline.js +78 -0
  144. package/dist/storage/migration/classifier.js +51 -0
  145. package/dist/storage/migration/compatibleCodec.js +53 -0
  146. package/dist/storage/migration/engine.js +147 -0
  147. package/dist/storage/migration/index.js +33 -0
  148. package/dist/storage/migration/planner.js +154 -0
  149. package/dist/storage/migration/productionRegistry.js +486 -0
  150. package/dist/storage/migration/registry.js +169 -0
  151. package/dist/storage/migration/report.js +54 -0
  152. package/dist/storage/migration/types.js +31 -0
  153. package/dist/storage/storageSchema.js +147 -123
  154. package/dist/storage/storageVersions.js +11 -0
  155. package/dist/storage/taskStore.js +1793 -197
  156. package/dist/storage/upgrade/homeClassification.js +156 -0
  157. package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
  158. package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
  159. package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
  160. package/dist/storage/upgrade/recordVersionScan.js +176 -0
  161. package/dist/storage/upgrade/recordVersions.js +159 -0
  162. package/dist/storage/upgrade/switchProgress.js +80 -0
  163. package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
  164. package/dist/storage/upgrade/upgradeReceipt.js +161 -0
  165. package/dist/storage/upgradeCoordination.js +186 -0
  166. package/dist/storage/upgradeFence.js +366 -0
  167. package/dist/task/task.js +132 -26
  168. package/dist/task/taskRecordReference.js +66 -0
  169. package/dist/tmux/commandExecutor.js +75 -2
  170. package/dist/tmux/tmuxManager.js +747 -49
  171. package/dist/version.js +23 -0
  172. package/dist/web/assets/assetManifest.js +62 -0
  173. package/dist/web/assets/client/app.js +631 -0
  174. package/dist/web/assets/client/components.js +605 -0
  175. package/dist/web/assets/client/dom.js +14 -0
  176. package/dist/web/assets/client/format.js +28 -0
  177. package/dist/web/assets/client/i18n.js +494 -0
  178. package/dist/web/assets/client/markdown.js +114 -0
  179. package/dist/web/assets/client/theme.js +32 -0
  180. package/dist/web/assets/client/view.js +458 -0
  181. package/dist/web/assets/fontData.js +12 -0
  182. package/dist/web/assets/fonts.js +12 -0
  183. package/dist/web/assets/shell.js +114 -0
  184. package/dist/web/assets/styles/cards.js +135 -0
  185. package/dist/web/assets/styles/layout.js +47 -0
  186. package/dist/web/assets/styles/markdown.js +29 -0
  187. package/dist/web/assets/styles/responsive.js +39 -0
  188. package/dist/web/assets/styles/tokens.js +101 -0
  189. package/dist/web/assets/styles/widgets.js +147 -0
  190. package/dist/web/tmuxWebTerminal.js +158 -0
  191. package/dist/web/webServer.js +463 -0
  192. package/dist/web/webSnapshot.js +148 -0
  193. package/dist/workItem/workItem.js +642 -23
  194. package/dist/workspace/gitChangeSetCapture.js +86 -0
  195. package/dist/workspace/workItemChangeSetManager.js +445 -0
  196. package/dist/worktree/managedWorkspace.js +202 -0
  197. package/docs/task-local-identity.md +62 -0
  198. package/i18n/README.zh-CN.md +406 -31
  199. package/package.json +10 -2
  200. package/skills/yui-leader/SKILL.md +601 -39
  201. package/skills/yui-operator/SKILL.md +255 -34
  202. package/skills/yui-reviewer/SKILL.md +57 -0
  203. package/skills/yui-worker/SKILL.md +214 -17
  204. package/dist/commands/repositoryCommands.js +0 -86
  205. package/dist/operator/operatorContext.js +0 -66
  206. package/dist/repository/repository.js +0 -55
  207. package/dist/scheduler/archivedTaskRuntime.js +0 -12
  208. package/dist/worktree/roleWorkspace.js +0 -62
@@ -1,11 +1,21 @@
1
+ import { createHash } from "node:crypto";
2
+ import { hasRecentTurnId, rememberRecentTurnId, validatePendingTurnCompletion, validateRecentTurnIds } from "./turnCompletion.js";
3
+ import { effectiveLaunchSnapshotsCompatible, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
1
4
  export function createRoleSessionSet(owner, activeAgentId, now) {
2
- return {
3
- schemaVersion: 1,
5
+ const base = {
4
6
  owner: normalizeOwner(owner),
5
7
  activeAgentId: requireSafeIdentity(activeAgentId, "Active Agent id"),
6
8
  sessions: {},
7
9
  updatedAt: now.toISOString()
8
10
  };
11
+ return owner.scope === "global"
12
+ ? { ...base, schemaVersion: 3 }
13
+ : {
14
+ ...base,
15
+ schemaVersion: 4,
16
+ inFlight: null,
17
+ pendingTurnCompletion: null
18
+ };
9
19
  }
10
20
  export function activeRoleAgentSession(set) {
11
21
  if (set === null)
@@ -13,11 +23,33 @@ export function activeRoleAgentSession(set) {
13
23
  validateRoleSessionSet(set);
14
24
  return set.sessions[set.activeAgentId] ?? null;
15
25
  }
26
+ /** The immutable snapshot that remains actual until its native process terminates. */
27
+ export function activeLiveRoleAgentSession(set) {
28
+ const session = activeRoleAgentSession(set);
29
+ return session === null || session.status === "stopped" || session.status === "broken"
30
+ ? null
31
+ : session;
32
+ }
33
+ export function roleAgentSessionRef(session) {
34
+ const digest = createHash("sha256").update(JSON.stringify([
35
+ requireText(session.agentId, "Agent id"),
36
+ requireText(session.adapterId, "Agent adapter id"),
37
+ requireText(session.nativeSessionId, "Native session id")
38
+ ])).digest("hex");
39
+ return `op-${digest.slice(0, 16)}`;
40
+ }
16
41
  export function recordRoleAgentSession(set, input, now) {
17
42
  validateRoleSessionSet(set);
18
43
  const agentId = requireSafeIdentity(input.agentId, "Agent id");
19
44
  const adapterId = requireText(input.adapterId, "Agent adapter id");
20
45
  const nativeSessionId = requireText(input.nativeSessionId, "Native session id");
46
+ if (set.owner.scope === "task") {
47
+ const historical = (set.history ?? []).find((entry) => (entry.nativeSessionId === nativeSessionId
48
+ || (input.launchId !== undefined && entry.launchId === input.launchId)));
49
+ if (historical !== undefined) {
50
+ throw new Error("A new Task Role Session cannot reuse a historical native identity.");
51
+ }
52
+ }
21
53
  if (!isAgentSessionStatus(input.status)) {
22
54
  throw new Error(`Role Agent session status is invalid: ${input.status}.`);
23
55
  }
@@ -28,26 +60,74 @@ export function recordRoleAgentSession(set, input, now) {
28
60
  if (existing !== undefined && existing.adapterId !== adapterId) {
29
61
  throw new Error(`Role Agent session adapter cannot change: ${agentId}.`);
30
62
  }
63
+ const effective = validateEffectiveLaunchSnapshot(input.effective);
64
+ if (effective.agentId !== agentId || effective.adapterId !== adapterId) {
65
+ throw new Error(`Role Agent session effective identity is inconsistent: ${agentId}.`);
66
+ }
67
+ if (existing !== undefined && existing.nativeSessionId === nativeSessionId
68
+ && !effectiveLaunchSnapshotsCompatible(existing.effective, effective)) {
69
+ throw new Error(`Role Agent session effective launch cannot change: ${agentId}.`);
70
+ }
71
+ if (existing !== undefined && existing.nativeSessionId !== nativeSessionId
72
+ && existing.status !== "stopped" && existing.status !== "broken") {
73
+ throw new Error(`Live Role Agent native session cannot be replaced: ${agentId}.`);
74
+ }
31
75
  const timestamp = now.toISOString();
76
+ const continuing = existing?.nativeSessionId === nativeSessionId ? existing : undefined;
32
77
  const session = {
33
- schemaVersion: 1,
78
+ schemaVersion: 3,
34
79
  agentId,
35
80
  adapterId,
36
81
  nativeSessionId,
82
+ ...(input.launchId === undefined
83
+ ? continuing?.launchId === undefined ? {} : { launchId: continuing.launchId }
84
+ : { launchId: requireText(input.launchId, "Launch id") }),
85
+ ...optionalSessionText("title", input.title ?? continuing?.title),
86
+ ...optionalSessionText("preview", input.preview ?? continuing?.preview),
37
87
  policy: input.policy,
88
+ effective: continuing?.effective ?? effective,
38
89
  status: input.status,
39
- createdAt: existing?.createdAt ?? timestamp,
90
+ recentCompletedTurnIds: continuing?.recentCompletedTurnIds ?? [],
91
+ createdAt: continuing?.createdAt ?? timestamp,
40
92
  updatedAt: timestamp
41
93
  };
42
94
  validateRoleAgentSession(session, agentId);
95
+ const taskHistory = set.owner.scope === "task"
96
+ && existing !== undefined
97
+ && continuing === undefined
98
+ ? [...(set.history ?? []), existing]
99
+ : undefined;
100
+ const globalHistory = set.owner.scope === "global"
101
+ && existing !== undefined
102
+ && continuing === undefined
103
+ ? {
104
+ ...(set.history ?? {}),
105
+ [roleAgentSessionRef(existing)]: existing
106
+ }
107
+ : undefined;
43
108
  const updated = {
44
109
  ...set,
110
+ activeAgentId: agentId,
45
111
  sessions: { ...set.sessions, [agentId]: session },
112
+ ...(taskHistory === undefined ? {} : { history: taskHistory }),
113
+ ...(globalHistory === undefined ? {} : { history: globalHistory }),
46
114
  updatedAt: timestamp
47
115
  };
48
116
  validateRoleSessionSet(updated);
49
117
  return updated;
50
118
  }
119
+ /**
120
+ * Session titles and previews can originate in native Agent output. Keep them
121
+ * single-line and inert before they are persisted or rendered in a terminal.
122
+ */
123
+ export function normalizeRoleAgentSessionText(value) {
124
+ return value
125
+ .replaceAll(/\u001B\][^\u0007]*(?:\u0007|\u001B\\|\u009C)/gu, " ")
126
+ .replaceAll(/\u001B\[[0-?]*[ -/]*[@-~]/gu, " ")
127
+ .replaceAll(/[\u0000-\u001F\u007F-\u009F]/gu, " ")
128
+ .trim()
129
+ .replaceAll(/\s+/gu, " ");
130
+ }
51
131
  export function createRoleAgentSession(input, now) {
52
132
  const agentId = requireSafeIdentity(input.agentId, "Agent id");
53
133
  const set = createRoleSessionSet({ scope: "global", roleName: "session-factory" }, agentId, now);
@@ -75,26 +155,424 @@ export function updateRoleAgentSessionStatus(set, agentId, status, now) {
75
155
  validateRoleSessionSet(updated);
76
156
  return updated;
77
157
  }
78
- export function roleAgentSessionResumeMode(set, agentId) {
158
+ export function retireTaskRoleSessionsForWorkspace(set, now) {
159
+ validateRoleSessionSet(set);
160
+ if (set.inFlight !== null
161
+ || set.pendingTurnCompletion !== null) {
162
+ throw new Error("Cannot retire a Task Role session with unsettled Run state.");
163
+ }
164
+ const live = Object.values(set.sessions).find(({ status }) => status !== "stopped" && status !== "broken");
165
+ if (live !== undefined) {
166
+ throw new Error(`Task Role session must be stopped before workspace migration: ${live.agentId}.`);
167
+ }
168
+ const timestamp = now.toISOString();
169
+ return validateRoleSessionSet({
170
+ ...set,
171
+ // Native sessions may be scoped to their launch cwd. Every binding must
172
+ // receive a fresh identity after the Role workspace changes.
173
+ history: [...(set.history ?? []), ...Object.values(set.sessions)],
174
+ sessions: {},
175
+ updatedAt: timestamp
176
+ });
177
+ }
178
+ /**
179
+ * Terminalizes only the aggregate-16 Claude placeholder shape after the
180
+ * caller has fenced the Task store and proved that the exact Role has no live
181
+ * native pane. All other nonterminal durable Sessions remain blockers for the
182
+ * ordinary workspace-retirement path above.
183
+ */
184
+ export function retireConfirmedAbsentInactiveTaskRolePlaceholders(set, now) {
185
+ validateRoleSessionSet(set);
186
+ if (set.inFlight !== null || set.pendingTurnCompletion !== null) {
187
+ throw new Error("Cannot retire a Task Role placeholder with unsettled Run state.");
188
+ }
189
+ const timestamp = now.toISOString();
190
+ let changed = false;
191
+ const sessions = Object.fromEntries(Object.entries(set.sessions).map(([agentId, session]) => {
192
+ const isNeverStartedInactiveClaude = agentId !== set.activeAgentId
193
+ && session.adapterId === "claude"
194
+ && (session.status === "reserved" || session.status === "ready")
195
+ && session.launchId === undefined
196
+ && session.recentCompletedTurnIds.length === 0;
197
+ if (!isNeverStartedInactiveClaude)
198
+ return [agentId, session];
199
+ changed = true;
200
+ return [agentId, { ...session, status: "broken", updatedAt: timestamp }];
201
+ }));
202
+ if (!changed)
203
+ return set;
204
+ return validateRoleSessionSet({
205
+ ...set,
206
+ sessions,
207
+ updatedAt: timestamp
208
+ });
209
+ }
210
+ export function rememberRoleAgentCompletedTurn(set, agentId, nativeSessionId, turnId, now) {
211
+ validateRoleSessionSet(set);
212
+ const normalizedAgentId = requireSafeIdentity(agentId, "Agent id");
213
+ const session = set.sessions[normalizedAgentId];
214
+ if (session === undefined) {
215
+ throw new Error(`No Role session is recorded for Agent: ${normalizedAgentId}.`);
216
+ }
217
+ if (session.nativeSessionId !== requireText(nativeSessionId, "Native session id")) {
218
+ throw new Error("Completed Turn native session does not match the Role Agent session.");
219
+ }
220
+ const normalizedTurnId = requireSafeIdentity(turnId, "Turn id");
221
+ if (hasRecentTurnId(session.recentCompletedTurnIds, normalizedTurnId))
222
+ return set;
223
+ const recentCompletedTurnIds = rememberRecentTurnId(session.recentCompletedTurnIds, normalizedTurnId);
224
+ if (recentCompletedTurnIds === session.recentCompletedTurnIds)
225
+ return set;
226
+ const timestamp = requireDate(now, "Turn completedAt");
227
+ const updated = {
228
+ ...set,
229
+ sessions: {
230
+ ...set.sessions,
231
+ [normalizedAgentId]: {
232
+ ...session,
233
+ status: session.status === "stopped" || session.status === "broken"
234
+ ? session.status
235
+ : "ready",
236
+ recentCompletedTurnIds,
237
+ updatedAt: timestamp
238
+ }
239
+ },
240
+ updatedAt: timestamp
241
+ };
242
+ return validateRoleSessionSet(updated);
243
+ }
244
+ export function roleAgentSessionResumeMode(set, agentId, desired) {
79
245
  if (set === null)
80
246
  return "new";
81
247
  validateRoleSessionSet(set);
82
248
  const session = set.sessions[requireSafeIdentity(agentId, "Agent id")];
83
- return session === undefined ? "new" : "resume";
249
+ if (session === undefined)
250
+ return "new";
251
+ // A restored opaque host can be inspected/recovered only through its exact
252
+ // launch fence. It cannot be resumed or silently rebound to a new launch
253
+ // without a provider-native identity; an explicit verified stop still
254
+ // permits the normal fresh-generation path.
255
+ if (typeof session.nativeSessionId !== "string"
256
+ || session.nativeSessionId.trim().length === 0) {
257
+ if (session.status !== "stopped" && session.status !== "broken") {
258
+ throw new Error(`Role Agent session has no native Session identity: ${agentId}. `
259
+ + "Restore the exact native Session or explicitly stop it before starting a fresh Session.");
260
+ }
261
+ return "new";
262
+ }
263
+ if (effectiveLaunchSnapshotsCompatible(session.effective, desired))
264
+ return "resume";
265
+ if (session.status !== "stopped" && session.status !== "broken") {
266
+ throw new Error(`Role Agent session is incompatible with the next effective launch: ${agentId}. `
267
+ + "Stop the existing native process before starting a fresh Session.");
268
+ }
269
+ return "new";
270
+ }
271
+ export function bindTaskRoleRun(set, fence, preparedAt) {
272
+ validateRoleSessionSet(set);
273
+ assertTaskRoleSessionSet(set);
274
+ const normalized = normalizeTaskRoleRunFence(fence);
275
+ if (normalized.agentId !== set.activeAgentId) {
276
+ throw new Error("Task Role Run Agent does not match the active Agent.");
277
+ }
278
+ if (set.pendingTurnCompletion !== null) {
279
+ throw new Error("Task Role session set has an unsettled Turn completion.");
280
+ }
281
+ if (set.inFlight !== null) {
282
+ if (sameRunFence(set.inFlight, normalized))
283
+ return set;
284
+ throw new Error("Task Role session set already has an in-flight Run.");
285
+ }
286
+ const timestamp = requireDate(preparedAt, "Task Role Run preparedAt");
287
+ const updated = {
288
+ ...set,
289
+ inFlight: { ...normalized, preparedAt: timestamp },
290
+ updatedAt: timestamp
291
+ };
292
+ return validateRoleSessionSet(updated);
293
+ }
294
+ export function markTaskRoleRunPushed(set, fence, pushedAt) {
295
+ validateRoleSessionSet(set);
296
+ assertTaskRoleSessionSet(set);
297
+ const inFlight = requireMatchingInFlight(set, fence);
298
+ if (inFlight.pushedAt !== undefined)
299
+ return set;
300
+ const timestamp = requireDate(pushedAt, "Task Role Run pushedAt");
301
+ if (Date.parse(timestamp) < Date.parse(inFlight.preparedAt)) {
302
+ throw new Error("Task Role Run pushedAt must not be earlier than preparedAt.");
303
+ }
304
+ const updated = {
305
+ ...set,
306
+ inFlight: { ...inFlight, pushedAt: timestamp },
307
+ updatedAt: timestamp
308
+ };
309
+ return validateRoleSessionSet(updated);
310
+ }
311
+ export function markTaskRoleRunDelivered(set, fence, deliveredAt) {
312
+ validateRoleSessionSet(set);
313
+ assertTaskRoleSessionSet(set);
314
+ const inFlight = requireMatchingInFlight(set, fence);
315
+ if (inFlight.deliveredAt !== undefined)
316
+ return set;
317
+ const timestamp = requireDate(deliveredAt, "Task Role Run deliveredAt");
318
+ if (Date.parse(timestamp) < Date.parse(inFlight.preparedAt)) {
319
+ throw new Error("Task Role Run deliveredAt must not be earlier than preparedAt.");
320
+ }
321
+ const updated = {
322
+ ...set,
323
+ inFlight: {
324
+ ...inFlight,
325
+ ...(inFlight.pushedAt === undefined ? { pushedAt: timestamp } : {}),
326
+ deliveredAt: timestamp
327
+ },
328
+ updatedAt: timestamp
329
+ };
330
+ return validateRoleSessionSet(updated);
331
+ }
332
+ export function recordObservedTaskRoleCompletion(set, completion) {
333
+ validateRoleSessionSet(set);
334
+ assertTaskRoleSessionSet(set);
335
+ const observed = validatePendingTurnCompletion(completion);
336
+ assertCompletionOwner(set, observed);
337
+ const session = set.sessions[observed.agentId];
338
+ if (session !== undefined && session.nativeSessionId !== observed.nativeSessionId) {
339
+ throw new Error("Observed Turn native session does not match the Role Agent session.");
340
+ }
341
+ if (session?.recentCompletedTurnIds.includes(observed.turnId) === true)
342
+ return set;
343
+ if (set.pendingTurnCompletion !== null) {
344
+ if (samePendingTurnCompletion(set.pendingTurnCompletion, observed))
345
+ return set;
346
+ throw new Error("Task Role session set already has a pending Turn completion.");
347
+ }
348
+ const inFlight = set.inFlight;
349
+ if (inFlight === null) {
350
+ throw new Error("Observed Turn has no matching in-flight Run.");
351
+ }
352
+ if (inFlight.agentId !== observed.agentId || inFlight.runId !== observed.runId) {
353
+ throw new Error("Observed Turn Run does not match the in-flight Run.");
354
+ }
355
+ if (inFlight.pushedAt === undefined) {
356
+ throw new Error("Observed Turn Run must be pushed before completion is recorded.");
357
+ }
358
+ const updated = {
359
+ ...set,
360
+ pendingTurnCompletion: observed,
361
+ updatedAt: observed.observedAt
362
+ };
363
+ return validateRoleSessionSet(updated);
364
+ }
365
+ export function clearTaskRoleRun(set, fence, clearedAt) {
366
+ validateRoleSessionSet(set);
367
+ assertTaskRoleSessionSet(set);
368
+ const inFlight = requireMatchingInFlight(set, fence);
369
+ const timestamp = requireDate(clearedAt, "Task Role Run clearedAt");
370
+ if (Date.parse(timestamp) < Date.parse(inFlight.preparedAt)) {
371
+ throw new Error("Task Role Run clearedAt must not be earlier than preparedAt.");
372
+ }
373
+ const pending = set.pendingTurnCompletion;
374
+ const session = set.sessions[inFlight.agentId];
375
+ const sessions = pending !== null && session !== undefined
376
+ ? {
377
+ ...set.sessions,
378
+ [inFlight.agentId]: {
379
+ ...session,
380
+ recentCompletedTurnIds: rememberRecentTurnId(session.recentCompletedTurnIds, pending.turnId),
381
+ updatedAt: timestamp
382
+ }
383
+ }
384
+ : set.sessions;
385
+ const updated = {
386
+ ...set,
387
+ sessions,
388
+ inFlight: null,
389
+ pendingTurnCompletion: null,
390
+ updatedAt: timestamp
391
+ };
392
+ return validateRoleSessionSet(updated);
393
+ }
394
+ /**
395
+ * Applies an authoritative application-level Run terminal fact to the native
396
+ * session fence. A later native Hook is only advisory and must not be required
397
+ * to make the next Run dispatchable.
398
+ */
399
+ export function terminalizeTaskRoleRunSession(set, fence, terminalAt) {
400
+ validateRoleSessionSet(set);
401
+ const inFlight = set.inFlight;
402
+ const pending = set.pendingTurnCompletion;
403
+ let updated = pending !== null
404
+ && pending.agentId === fence.agentId
405
+ && pending.runId === fence.runId
406
+ ? settleTaskRoleCompletion(set, {
407
+ agentId: fence.agentId,
408
+ runId: fence.runId,
409
+ turnId: pending.turnId
410
+ }, terminalAt)
411
+ : inFlight === null
412
+ ? set
413
+ : clearTaskRoleRun(set, fence, terminalAt);
414
+ const session = updated.sessions[updated.activeAgentId];
415
+ if (session?.status === "running") {
416
+ updated = updateRoleAgentSessionStatus(updated, updated.activeAgentId, "ready", terminalAt);
417
+ }
418
+ return updated;
419
+ }
420
+ /**
421
+ * Resets the current native generation after its active Run is terminal.
422
+ * The Controller separately owns verified process cleanup.
423
+ */
424
+ export function resetTaskRoleSession(set, now) {
425
+ validateRoleSessionSet(set);
426
+ const timestamp = requireDate(now, "Task Role Session reset timestamp");
427
+ const current = set.sessions[set.activeAgentId];
428
+ const sessions = { ...set.sessions };
429
+ delete sessions[set.activeAgentId];
430
+ const history = current === undefined
431
+ ? set.history
432
+ : [
433
+ ...(set.history ?? []),
434
+ { ...current, status: "broken", updatedAt: timestamp }
435
+ ];
436
+ return validateRoleSessionSet({
437
+ ...set,
438
+ sessions,
439
+ ...(history === undefined ? {} : { history }),
440
+ inFlight: null,
441
+ pendingTurnCompletion: null,
442
+ updatedAt: timestamp
443
+ });
444
+ }
445
+ export function settleTaskRoleCompletion(set, expected, settledAt) {
446
+ validateRoleSessionSet(set);
447
+ assertTaskRoleSessionSet(set);
448
+ const agentId = requireSafeIdentity(expected.agentId, "Agent id");
449
+ const runId = requireSafeIdentity(expected.runId, "Run id");
450
+ const turnId = requireSafeIdentity(expected.turnId, "Turn id");
451
+ const pending = set.pendingTurnCompletion;
452
+ if (pending === null)
453
+ throw new Error("Task Role session set has no pending Turn completion.");
454
+ if (pending.agentId !== agentId
455
+ || pending.runId !== runId
456
+ || pending.turnId !== turnId) {
457
+ throw new Error("Pending Turn completion does not match the expected Turn.");
458
+ }
459
+ const inFlight = set.inFlight;
460
+ if (inFlight === null || inFlight.agentId !== agentId || inFlight.runId !== runId) {
461
+ throw new Error("Pending Turn completion does not match the in-flight Run.");
462
+ }
463
+ const session = set.sessions[agentId];
464
+ if (session === undefined || session.nativeSessionId !== pending.nativeSessionId) {
465
+ throw new Error("Pending Turn completion has no matching Role Agent native session.");
466
+ }
467
+ const timestamp = requireDate(settledAt, "Turn settledAt");
468
+ if (Date.parse(timestamp) < Date.parse(pending.observedAt)) {
469
+ throw new Error("Turn settledAt must not be earlier than observedAt.");
470
+ }
471
+ const updated = {
472
+ ...set,
473
+ sessions: {
474
+ ...set.sessions,
475
+ [agentId]: {
476
+ ...session,
477
+ recentCompletedTurnIds: rememberRecentTurnId(session.recentCompletedTurnIds, pending.turnId),
478
+ updatedAt: timestamp
479
+ }
480
+ },
481
+ inFlight: null,
482
+ pendingTurnCompletion: null,
483
+ updatedAt: timestamp
484
+ };
485
+ return validateRoleSessionSet(updated);
84
486
  }
85
487
  export function validateRoleSessionSet(set) {
86
- if (set.schemaVersion !== 1)
87
- throw new Error("Role session set schema version is invalid.");
88
488
  normalizeOwner(set.owner);
89
489
  requireSafeIdentity(set.activeAgentId, "Active Agent id");
90
490
  for (const [agentId, session] of Object.entries(set.sessions)) {
91
491
  validateRoleAgentSession(session, agentId);
92
492
  }
493
+ if (set.owner.scope === "global") {
494
+ if (set.schemaVersion !== 3) {
495
+ throw new Error("Global Role session set schema version is invalid.");
496
+ }
497
+ if (Object.hasOwn(set, "inFlight")
498
+ || Object.hasOwn(set, "pendingTurnCompletion")) {
499
+ throw new Error("Global Role session set must not contain Task Role lifecycle fields.");
500
+ }
501
+ const history = set.history;
502
+ if (history !== undefined) {
503
+ for (const [ref, session] of Object.entries(history)) {
504
+ requireSafeIdentity(ref, "Operator session ref");
505
+ validateRoleAgentSession(session);
506
+ if (session.status !== "stopped" && session.status !== "broken") {
507
+ throw new Error(`Operator history session must be stopped: ${ref}.`);
508
+ }
509
+ }
510
+ }
511
+ }
512
+ else {
513
+ if (set.schemaVersion !== 4) {
514
+ throw new Error("Task Role session set schema version is invalid.");
515
+ }
516
+ if (!Object.hasOwn(set, "inFlight")
517
+ || !Object.hasOwn(set, "pendingTurnCompletion")) {
518
+ throw new Error("Task Role session set must contain its Turn fence.");
519
+ }
520
+ const taskSet = set;
521
+ if (taskSet.history !== undefined) {
522
+ if (!Array.isArray(taskSet.history)) {
523
+ throw new Error("Task Role session history must be an array.");
524
+ }
525
+ const identities = new Set();
526
+ for (const session of taskSet.history) {
527
+ validateRoleAgentSession(session);
528
+ if (session.status !== "stopped" && session.status !== "broken") {
529
+ throw new Error("Task Role session history must be terminal.");
530
+ }
531
+ const key = taskRoleSessionIdentity(session);
532
+ if (identities.has(key)) {
533
+ throw new Error("Task Role session history contains a duplicate Session identity.");
534
+ }
535
+ identities.add(key);
536
+ }
537
+ for (const session of Object.values(taskSet.sessions)) {
538
+ if (identities.has(taskRoleSessionIdentity(session))) {
539
+ throw new Error("Active and historical Task Role Sessions must be distinct.");
540
+ }
541
+ }
542
+ }
543
+ const inFlight = taskSet.inFlight === null
544
+ ? null
545
+ : validateTaskRoleInFlight(taskSet.inFlight);
546
+ const pending = taskSet.pendingTurnCompletion === null
547
+ ? null
548
+ : validatePendingTurnCompletion(taskSet.pendingTurnCompletion);
549
+ if (inFlight === null && pending !== null) {
550
+ throw new Error("Pending Turn completion requires an in-flight Run.");
551
+ }
552
+ if (inFlight !== null && inFlight.agentId !== set.activeAgentId) {
553
+ throw new Error("Task Role in-flight Run Agent must be active.");
554
+ }
555
+ if (pending !== null) {
556
+ assertCompletionOwner(taskSet, pending);
557
+ if (inFlight?.agentId !== pending.agentId
558
+ || inFlight.runId !== pending.runId
559
+ || inFlight.pushedAt === undefined) {
560
+ throw new Error("Pending Turn completion must match a pushed in-flight Run.");
561
+ }
562
+ const session = taskSet.sessions[pending.agentId];
563
+ if (session === undefined) {
564
+ throw new Error("Pending Turn completion has no Role Agent session.");
565
+ }
566
+ if (session.nativeSessionId !== pending.nativeSessionId) {
567
+ throw new Error("Pending Turn native session does not match the Role Agent session.");
568
+ }
569
+ }
570
+ }
93
571
  requireText(set.updatedAt, "Role session set update timestamp");
94
572
  return set;
95
573
  }
96
574
  export function validateRoleAgentSession(session, expectedAgentId = session.agentId) {
97
- if (session.schemaVersion !== 1) {
575
+ if (session.schemaVersion !== 3) {
98
576
  throw new Error(`Role Agent session schema version is invalid: ${expectedAgentId}.`);
99
577
  }
100
578
  const agentId = requireSafeIdentity(session.agentId, "Agent id");
@@ -102,17 +580,154 @@ export function validateRoleAgentSession(session, expectedAgentId = session.agen
102
580
  throw new Error(`Role Agent session identity is inconsistent: ${expectedAgentId}.`);
103
581
  }
104
582
  requireText(session.adapterId, "Agent adapter id");
105
- requireText(session.nativeSessionId, "Native session id");
583
+ validateEffectiveLaunchSnapshot(session.effective);
584
+ if (session.effective.agentId !== agentId || session.effective.adapterId !== session.adapterId) {
585
+ throw new Error(`Role Agent session effective identity is inconsistent: ${agentId}.`);
586
+ }
587
+ // A restored opaque host may have no provider-native identity; its launch
588
+ // fence is still durable and exact recovery remains possible.
589
+ if (session.nativeSessionId === undefined) {
590
+ if (session.launchId === undefined) {
591
+ throw new Error("Role Agent session requires a native Session or launch id.");
592
+ }
593
+ }
594
+ else {
595
+ requireText(session.nativeSessionId, "Native session id");
596
+ }
597
+ if (session.launchId !== undefined)
598
+ requireSafeIdentity(session.launchId, "Launch id");
599
+ if (session.title !== undefined
600
+ && optionalSessionText("title", session.title).title !== session.title) {
601
+ throw new Error("Role Agent session title is invalid.");
602
+ }
603
+ if (session.preview !== undefined
604
+ && optionalSessionText("preview", session.preview).preview !== session.preview) {
605
+ throw new Error("Role Agent session preview is invalid.");
606
+ }
106
607
  if (session.policy !== "fixed" && session.policy !== "leader-controlled") {
107
608
  throw new Error(`Role Agent session policy is invalid: ${agentId}.`);
108
609
  }
109
610
  if (!isAgentSessionStatus(session.status)) {
110
611
  throw new Error(`Role Agent session status is invalid: ${agentId}.`);
111
612
  }
613
+ validateRecentTurnIds(session.recentCompletedTurnIds);
112
614
  requireText(session.createdAt, "Role Agent session creation timestamp");
113
615
  requireText(session.updatedAt, "Role Agent session update timestamp");
114
616
  return session;
115
617
  }
618
+ function taskRoleSessionIdentity(session) {
619
+ if (session.nativeSessionId !== undefined) {
620
+ return `${session.agentId}\0native\0${session.nativeSessionId}`;
621
+ }
622
+ if (session.launchId === undefined) {
623
+ throw new Error("Opaque Task Role session requires a launch identity.");
624
+ }
625
+ return `${session.agentId}\0launch\0${session.launchId}`;
626
+ }
627
+ function optionalSessionText(field, value) {
628
+ if (value === undefined)
629
+ return {};
630
+ if (typeof value !== "string" || value.includes("\0")) {
631
+ throw new Error(`Role Agent session ${field} is invalid.`);
632
+ }
633
+ const normalized = normalizeRoleAgentSessionText(value);
634
+ if (normalized.length === 0 || normalized.length > 1_024) {
635
+ throw new Error(`Role Agent session ${field} is invalid.`);
636
+ }
637
+ return { [field]: normalized };
638
+ }
639
+ function assertTaskRoleSessionSet(set) {
640
+ if (set.owner.scope !== "task") {
641
+ throw new Error("Turn fences require a Task Role session set.");
642
+ }
643
+ }
644
+ function normalizeTaskRoleRunFence(fence) {
645
+ return {
646
+ agentId: requireSafeIdentity(fence.agentId, "Agent id"),
647
+ runId: requireSafeIdentity(fence.runId, "Run id"),
648
+ receiptId: requireText(fence.receiptId, "Delivery receipt id")
649
+ };
650
+ }
651
+ function requireMatchingInFlight(set, fence) {
652
+ const normalized = normalizeTaskRoleRunFence(fence);
653
+ const inFlight = set.inFlight;
654
+ if (inFlight === null)
655
+ throw new Error("Task Role session set has no in-flight Run.");
656
+ if (inFlight.agentId !== normalized.agentId) {
657
+ throw new Error("Task Role Run Agent does not match the in-flight Run.");
658
+ }
659
+ if (inFlight.runId !== normalized.runId) {
660
+ throw new Error("Task Role Run does not match the in-flight Run.");
661
+ }
662
+ if (inFlight.receiptId !== normalized.receiptId) {
663
+ throw new Error("Task Role Run receipt does not match the in-flight Run.");
664
+ }
665
+ return inFlight;
666
+ }
667
+ function validateTaskRoleInFlight(value) {
668
+ const fields = Object.keys(value);
669
+ const unknown = fields.find((field) => !["agentId", "runId", "receiptId", "preparedAt", "pushedAt", "deliveredAt"].includes(field));
670
+ if (unknown !== undefined) {
671
+ throw new Error(`Task Role in-flight Run has unknown field: ${unknown}.`);
672
+ }
673
+ for (const required of ["agentId", "runId", "receiptId", "preparedAt"]) {
674
+ if (!Object.hasOwn(value, required)) {
675
+ throw new Error(`Task Role in-flight Run is missing field: ${required}.`);
676
+ }
677
+ }
678
+ const preparedAt = requirePersistedTimestamp(value.preparedAt, "Task Role Run preparedAt");
679
+ if (value.pushedAt !== undefined) {
680
+ const pushedAt = requirePersistedTimestamp(value.pushedAt, "Task Role Run pushedAt");
681
+ if (Date.parse(pushedAt) < Date.parse(preparedAt)) {
682
+ throw new Error("Task Role Run pushedAt must not be earlier than preparedAt.");
683
+ }
684
+ }
685
+ if (value.deliveredAt !== undefined) {
686
+ const deliveredAt = requirePersistedTimestamp(value.deliveredAt, "Task Role Run deliveredAt");
687
+ if (Date.parse(deliveredAt) < Date.parse(preparedAt)) {
688
+ throw new Error("Task Role Run deliveredAt must not be earlier than preparedAt.");
689
+ }
690
+ if (value.pushedAt === undefined) {
691
+ throw new Error("Task Role Run deliveredAt requires a prior pushedAt.");
692
+ }
693
+ }
694
+ requireSafeIdentity(value.agentId, "Agent id");
695
+ requireSafeIdentity(value.runId, "Run id");
696
+ requireText(value.receiptId, "Delivery receipt id");
697
+ return value;
698
+ }
699
+ function assertCompletionOwner(set, completion) {
700
+ if (completion.taskId !== set.owner.taskId) {
701
+ throw new Error("Observed Turn Task does not match the Task Role session set.");
702
+ }
703
+ if (completion.roleName !== set.owner.roleName) {
704
+ throw new Error("Observed Turn Role does not match the Task Role session set.");
705
+ }
706
+ if (completion.agentId !== set.activeAgentId) {
707
+ throw new Error("Observed Turn Agent does not match the active Agent.");
708
+ }
709
+ }
710
+ function sameRunFence(inFlight, fence) {
711
+ return inFlight.agentId === fence.agentId
712
+ && inFlight.runId === fence.runId
713
+ && inFlight.receiptId === fence.receiptId;
714
+ }
715
+ function samePendingTurnCompletion(left, right) {
716
+ return JSON.stringify(left) === JSON.stringify(right);
717
+ }
718
+ function requireDate(value, label) {
719
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
720
+ throw new Error(`${label} must be a valid date.`);
721
+ }
722
+ return value.toISOString();
723
+ }
724
+ function requirePersistedTimestamp(value, label) {
725
+ const timestamp = Date.parse(value);
726
+ if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString() !== value) {
727
+ throw new Error(`${label} must be a canonical timestamp.`);
728
+ }
729
+ return value;
730
+ }
116
731
  function normalizeOwner(owner) {
117
732
  const roleName = requireSafeIdentity(owner.roleName, "Role name");
118
733
  return owner.scope === "global"