@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.
- package/ARCHITECTURE.md +23 -4
- package/README.md +44 -22
- package/dist/cli/commandCatalog.js +13 -10
- package/dist/cli/interactionPolicy.js +5 -6
- package/dist/cli/updateCommand.js +3 -1
- package/dist/cli/updateOrchestrator.js +173 -28
- package/dist/cli/updatePorts.js +137 -8
- package/dist/cli/upgradeCommand.js +19 -9
- package/dist/cli.js +156 -57
- package/dist/commands/configCommands.js +13 -46
- package/dist/commands/executionAuditCommands.js +3 -2
- package/dist/commands/jobCommands.js +4 -10
- package/dist/commands/taskCommands.js +234 -200
- package/dist/commands/taskCompletionGate.js +29 -54
- package/dist/commands/taskContextCommand.js +61 -13
- package/dist/commands/taskInputCommands.js +8 -5
- package/dist/commands/taskNextActionCommand.js +56 -6
- package/dist/commands/taskOverviewCommand.js +17 -31
- package/dist/commands/taskRoleRuntimeStatus.js +55 -7
- package/dist/commands/taskWorkspaceCommands.js +5 -2
- package/dist/config/configCatalog.js +3 -3
- package/dist/config/yuiConfig.js +3 -7
- package/dist/context/wakeNotification.js +20 -5
- package/dist/controller/agentRuntimeObserver.js +247 -51
- package/dist/controller/clientRuntime.js +39 -3
- package/dist/controller/controller.js +31 -27
- package/dist/controller/fileSchedulerStoreAdapter.js +390 -332
- package/dist/controller/runtime.js +58 -3
- package/dist/controller/runtimeEventInbox.js +17 -7
- package/dist/controller/runtimeEventProcessor.js +12 -19
- package/dist/controller/runtimeHookRunFence.js +19 -4
- package/dist/controller/runtimeObservationHook.js +45 -0
- package/dist/controller/structuredProviderObservation.js +20 -3
- package/dist/core/controllerClient.js +20 -2
- package/dist/core/controllerServer.js +1 -0
- package/dist/execution/resourceBroker.js +5 -4
- package/dist/executor/agentExecutor.js +52 -50
- package/dist/executor/effectiveLaunch.js +8 -48
- package/dist/executor/fileRoleLaunchPlanner.js +129 -45
- package/dist/executor/workspacePreflightClassification.js +23 -2
- package/dist/interaction/operatorPresentation.js +33 -89
- package/dist/lifecycle/exactRunTerminalization.js +69 -5
- package/dist/observability/executionAudit.js +5 -0
- package/dist/observability/orchestrationMetrics.js +8 -3
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +123 -0
- package/dist/release/runtimeRelease.js +20 -0
- package/dist/repository/taskWorkspacePreparer.js +176 -60
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/deltaRecheck.js +12 -51
- package/dist/review/reviewAcceptance.js +26 -0
- package/dist/review/reviewConfig.js +0 -31
- package/dist/review/reviewDecision.js +113 -0
- package/dist/review/reviewOutcomeClassifier.js +1 -1
- package/dist/review/reviewRound.js +1 -1
- package/dist/review/reviewerAvailability.js +69 -0
- package/dist/run/recoveryProjection.js +45 -6
- package/dist/runtime/agentDriverObservation.js +24 -10
- package/dist/runtime/agentHost.js +159 -85
- package/dist/runtime/builtinAgentDrivers.js +4 -3
- package/dist/runtime/builtinTranscriptObserver.js +301 -64
- package/dist/runtime/builtinTranscriptUsage.js +9 -7
- package/dist/runtime/conversationSwitch.js +277 -0
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/launchBroker.js +12 -0
- package/dist/runtime/processExitOutbox.js +88 -0
- package/dist/runtime/providerRuntimeIdentity.js +29 -1
- package/dist/runtime/runtimeHealthPolicy.js +5 -5
- package/dist/runtime/runtimeObservation.js +28 -0
- package/dist/runtime/runtimeProjection.js +22 -32
- package/dist/runtime/sessionTokenMetrics.js +181 -0
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +4 -1
- package/dist/scheduler/activeRoleRunDelivery.js +34 -199
- package/dist/scheduler/activeTaskProgress.js +7 -6
- package/dist/scheduler/leaderWakeupProcessor.js +42 -138
- package/dist/scheduler/operatorEvent.js +34 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
- package/dist/scheduler/roleRunStall.js +53 -31
- package/dist/scheduler/taskExecutionProjection.js +97 -76
- package/dist/scheduler/taskObservabilityProjection.js +10 -11
- package/dist/storage/migration/productionRegistry.js +379 -0
- package/dist/storage/sqliteSchema.js +66 -23
- package/dist/storage/sqliteStore.js +46 -23
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +15 -30
- package/dist/storage/upgrade/homeClassification.js +52 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
- package/dist/storage/upgrade/recordVersions.js +3 -2
- package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
- package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
- package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
- package/dist/task/completionReadiness.js +10 -6
- package/dist/task/nextAction.js +82 -58
- package/dist/telemetry/sqliteTelemetryStore.js +1 -1
- package/dist/web/assets/client/components.js +13 -3
- package/dist/web/assets/client/i18n.js +6 -2
- package/dist/web/webSnapshot.js +7 -1
- package/i18n/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +60 -20
- package/skills/yui-operator/SKILL.md +13 -4
- package/skills/yui-reviewer/SKILL.md +35 -11
- package/dist/context/sessionContextBudget.js +0 -71
- package/dist/lifecycle/contextBudgetRollover.js +0 -81
- package/dist/lifecycle/taskRoleSessionReset.js +0 -126
- 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,
|
|
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
|
-
&&
|
|
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
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
-
?
|
|
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
|
-
//
|
|
469
|
-
// task-scope launch
|
|
470
|
-
//
|
|
471
|
-
//
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
|
508
|
+
const initialTurn = carriesInitialTurn
|
|
487
509
|
? {
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
:
|
|
645
|
-
?
|
|
646
|
-
|
|
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
|
|
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
|
|
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}:
|
|
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;
|