@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.
- package/ARCHITECTURE.md +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 !==
|
|
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
|
-
|
|
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"
|