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