@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
|
@@ -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";
|
|
@@ -36,6 +36,7 @@ import { createRuntimeResourceActivityTracker } from "./resourceInventory.js";
|
|
|
36
36
|
import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
|
|
37
37
|
import { launchBrokerForHome } from "../runtime/launchBroker.js";
|
|
38
38
|
import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
|
|
39
|
+
import { replayRuntimeProcessExitOutbox } from "../runtime/processExitOutbox.js";
|
|
39
40
|
import { appendGlobalProcessExitObservation } from "../runtime/globalProcessExitStore.js";
|
|
40
41
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
41
42
|
import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
@@ -385,6 +386,12 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
385
386
|
environment: options.environment
|
|
386
387
|
});
|
|
387
388
|
const lifecycleDispatcher = createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, options.dispatcher, signalRuntimeCleanup, launchCoordinator, planner);
|
|
389
|
+
// Process-exit observations are persisted by the Agent Host before socket
|
|
390
|
+
// delivery. Drain them while this Controller is still the only storage
|
|
391
|
+
// writer and before it begins accepting new work after a handover.
|
|
392
|
+
await replayRuntimeProcessExitOutbox(home, async (observation) => {
|
|
393
|
+
await lifecycleDispatcher("runtime.process-exit-observe", observation);
|
|
394
|
+
});
|
|
388
395
|
// f7/rr5: This same inbox feeds the supervisor's terminal channel and the
|
|
389
396
|
// runtime event processor. When a Job reaches a terminal state, the
|
|
390
397
|
// supervisor enqueues a durable-job-terminal event; the processor drains it
|
|
@@ -577,6 +584,11 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
577
584
|
}
|
|
578
585
|
return { recorded: true };
|
|
579
586
|
}
|
|
587
|
+
if (method === "runtime.conversation-switch-detach") {
|
|
588
|
+
const value = conversationSwitchDetachParams(params);
|
|
589
|
+
const outcome = schedulerStore.detachAgentHostProviderForConversationSwitch(value);
|
|
590
|
+
return { recorded: outcome === "detached", outcome };
|
|
591
|
+
}
|
|
580
592
|
if (method === "runtime.provider-turn-submission-resolve") {
|
|
581
593
|
const value = providerTurnControlParams(params);
|
|
582
594
|
const status = params.status;
|
|
@@ -671,6 +683,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
671
683
|
|| !Number.isSafeInteger(hostPid) || hostPid <= 0) {
|
|
672
684
|
throw applicationError("INVALID_PARAMS", "Launch redemption identity is invalid.");
|
|
673
685
|
}
|
|
686
|
+
// The launch payload is validated at reservation time and every member
|
|
687
|
+
// of its discriminated Provider-control union is JSON serializable.
|
|
674
688
|
return launchBrokerForHome(store.rootDirectory()).redeem(launchId, ticket);
|
|
675
689
|
}
|
|
676
690
|
if (method === "runtime.replace-agent-environment") {
|
|
@@ -735,6 +749,9 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
735
749
|
const activeRun = request.scope === "task"
|
|
736
750
|
? store.getActiveAgentRun(request.taskId, request.roleName)
|
|
737
751
|
: null;
|
|
752
|
+
if (request.scope === "task" && activeRun === null) {
|
|
753
|
+
throw applicationError("INVALID_PARAMS", "Task Role runtime attachment requires an admitted active Run; it cannot create an empty Provider Conversation.");
|
|
754
|
+
}
|
|
738
755
|
const managedWorkspace = request.scope === "task"
|
|
739
756
|
? activeRun?.workspace
|
|
740
757
|
?? currentDesiredManagedWorkspace(store, request.taskId, request.roleName)
|
|
@@ -754,8 +771,21 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
754
771
|
throw applicationError("INVALID_PARAMS", `Configured Agent does not match Role: ${effective.agentId}.`);
|
|
755
772
|
}
|
|
756
773
|
validateLifecycleEnvironment(request.environment, agent);
|
|
757
|
-
const mode = roleAgentSessionResumeMode(sessions, effective.agentId, effective, managedWorkspace);
|
|
758
774
|
const session = sessions?.sessions[effective.agentId];
|
|
775
|
+
const managedTurnDispatch = activeRun !== null && (activeRun.pushedAt === undefined
|
|
776
|
+
|| activeRun.providerRetry?.state === "dispatching"
|
|
777
|
+
|| activeRun.controlRequest?.state === "dispatching");
|
|
778
|
+
// An ensure call may reattach the already-admitted Run to its existing
|
|
779
|
+
// Conversation, but it may not manufacture a fresh Conversation without a
|
|
780
|
+
// pending managed Turn. Fresh replacement remains owned by Run dispatch.
|
|
781
|
+
const mode = activeRun === null
|
|
782
|
+
? roleAgentSessionResumeMode(sessions, effective.agentId, effective)
|
|
783
|
+
: managedTurnDispatch ? activeRun.mode : "resume";
|
|
784
|
+
if (request.scope === "task" && mode === "resume"
|
|
785
|
+
&& (session?.nativeSessionId === undefined
|
|
786
|
+
|| session.nativeSessionId.trim().length === 0)) {
|
|
787
|
+
throw applicationError("INVALID_PARAMS", "Task Role runtime attachment cannot resume because its Provider Conversation identity is missing.");
|
|
788
|
+
}
|
|
759
789
|
const owner = request.scope === "task"
|
|
760
790
|
? { scope: "task", taskId: request.taskId, roleName: request.roleName }
|
|
761
791
|
: { scope: "global", roleName: request.roleName };
|
|
@@ -878,7 +908,7 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
|
|
|
878
908
|
throw new Error(`Native session changed: ${request.owner.roleName}.`);
|
|
879
909
|
}
|
|
880
910
|
const sessionEffectiveCompatible = request.owner.scope === "task"
|
|
881
|
-
?
|
|
911
|
+
? effectiveLaunchSnapshotsCompatibleForTaskSession(session.effective, request.effective)
|
|
882
912
|
: effectiveLaunchSnapshotsCompatible(session.effective, request.effective);
|
|
883
913
|
if (!sessionEffectiveCompatible) {
|
|
884
914
|
throw new Error(`Native session effective launch changed: ${request.owner.roleName}.`);
|
|
@@ -1105,6 +1135,31 @@ function providerTurnControlParams(params) {
|
|
|
1105
1135
|
now: new Date(observedAt)
|
|
1106
1136
|
};
|
|
1107
1137
|
}
|
|
1138
|
+
function conversationSwitchDetachParams(params) {
|
|
1139
|
+
if (typeof params !== "object" || params === null || Array.isArray(params)) {
|
|
1140
|
+
throw applicationError("INVALID_PARAMS", "Provider Conversation detachment params are invalid.");
|
|
1141
|
+
}
|
|
1142
|
+
const value = params;
|
|
1143
|
+
const nextAuthorityEpoch = value.nextAuthorityEpoch;
|
|
1144
|
+
const observedAt = requiredParam(value.observedAt);
|
|
1145
|
+
if (!Number.isSafeInteger(nextAuthorityEpoch) || nextAuthorityEpoch < 1
|
|
1146
|
+
|| !Number.isFinite(Date.parse(observedAt))) {
|
|
1147
|
+
throw applicationError("INVALID_PARAMS", "Provider Conversation detachment fence is invalid.");
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
taskId: requiredParam(value.taskId),
|
|
1151
|
+
roleName: requiredParam(value.roleName),
|
|
1152
|
+
runId: requiredParam(value.runId),
|
|
1153
|
+
agentId: requiredParam(value.agentId),
|
|
1154
|
+
launchId: requiredParam(value.launchId),
|
|
1155
|
+
previousConversationId: requiredParam(value.previousConversationId),
|
|
1156
|
+
previousNativeSessionId: requiredParam(value.previousNativeSessionId),
|
|
1157
|
+
previousActivationId: requiredParam(value.previousActivationId),
|
|
1158
|
+
nextAuthorityEpoch: nextAuthorityEpoch,
|
|
1159
|
+
nextAuthorityHolderId: requiredParam(value.nextAuthorityHolderId),
|
|
1160
|
+
now: new Date(observedAt)
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1108
1163
|
function abortableDelay(milliseconds, signal) {
|
|
1109
1164
|
return new Promise((resolve, reject) => {
|
|
1110
1165
|
const timer = setTimeout(() => {
|
|
@@ -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 = {
|
|
@@ -4,6 +4,7 @@ import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.j
|
|
|
4
4
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
5
5
|
import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeEnvironment, exactControlPlaneDigest, parseExactControlPlaneDescriptor, refreshReusedTaskRuntimeDescriptorSource } from "../runtime/exactControlPlane.js";
|
|
6
6
|
import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
|
|
7
|
+
import { conversationReplacementBasis } from "../runtime/conversationSwitch.js";
|
|
7
8
|
/**
|
|
8
9
|
* Resolves turn identity from the current durable in-flight fence. The one
|
|
9
10
|
* exception is a Driver-declared startup Session Hook, which can arrive before
|
|
@@ -101,14 +102,27 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
|
|
|
101
102
|
const startupRun = startupRunId === undefined
|
|
102
103
|
? null
|
|
103
104
|
: store.getAgentRun(taskId, startupRunId);
|
|
105
|
+
const replacementStartup = options.startupSession !== undefined
|
|
106
|
+
&& session !== undefined
|
|
107
|
+
&& sessions !== null
|
|
108
|
+
&& startupReservation
|
|
109
|
+
&& startupRun?.mode === "new"
|
|
110
|
+
&& conversationReplacementBasis({
|
|
111
|
+
sessions,
|
|
112
|
+
events: store.listEvents(taskId),
|
|
113
|
+
mailbox: roleMailbox,
|
|
114
|
+
roleName,
|
|
115
|
+
runId: startupRun.id,
|
|
116
|
+
runMode: startupRun.mode
|
|
117
|
+
}) !== null;
|
|
104
118
|
const preallocatedStartup = options.startupSession === "preallocated"
|
|
105
119
|
&& expectedNativeSessionId !== undefined
|
|
106
|
-
&& session === undefined
|
|
120
|
+
&& (session === undefined || replacementStartup)
|
|
107
121
|
&& startupReservation
|
|
108
122
|
&& nativeSessionId === nativeSessionIdForLaunch(home, launchId, agentId, adapterId);
|
|
109
123
|
const discoveredStartup = options.startupSession === "discovered"
|
|
110
124
|
&& expectedNativeSessionId === undefined
|
|
111
|
-
&& session === undefined
|
|
125
|
+
&& (session === undefined || replacementStartup)
|
|
112
126
|
&& startupReservation;
|
|
113
127
|
const terminalRunId = options.terminal === true && acceptedBinding === null
|
|
114
128
|
? requireIdentity(environment.YUI_RUN_ID ?? runtime?.runId, "Run id")
|
|
@@ -154,6 +168,7 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
|
|
|
154
168
|
if (acceptedBinding === null
|
|
155
169
|
&& runtime !== undefined
|
|
156
170
|
&& session !== undefined
|
|
171
|
+
&& !replacementStartup
|
|
157
172
|
&& sessionLaunchId !== undefined
|
|
158
173
|
&& typeof runtimeSource === "string"
|
|
159
174
|
&& !runtimeSource.trimStart().startsWith("{")
|
|
@@ -187,7 +202,7 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
|
|
|
187
202
|
if (run.effective.workspace.root !== workspace) {
|
|
188
203
|
throw new Error("Runtime observation Hook workspace does not match the durable Run snapshot.");
|
|
189
204
|
}
|
|
190
|
-
if (session !== undefined && acceptedBinding === null) {
|
|
205
|
+
if (session !== undefined && acceptedBinding === null && !replacementStartup) {
|
|
191
206
|
if (session.adapterId !== adapterId
|
|
192
207
|
|| session.launchId !== effectiveLaunchId
|
|
193
208
|
|| session.nativeSessionId !== nativeSessionId
|
|
@@ -195,7 +210,7 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
|
|
|
195
210
|
throw new Error("Runtime observation Hook Session does not match its durable generation.");
|
|
196
211
|
}
|
|
197
212
|
}
|
|
198
|
-
else if (
|
|
213
|
+
else if (acceptedBinding === null && (session === undefined || replacementStartup)) {
|
|
199
214
|
if (!discoveredStartup && !preallocatedStartup) {
|
|
200
215
|
throw new Error("Runtime observation Hook launch is not durably reserved.");
|
|
201
216
|
}
|
|
@@ -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,6 +3,7 @@ import { callController } from "../core/controllerClient.js";
|
|
|
3
3
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
4
4
|
import { createRuntimeObservation, runtimeObservationSemanticKey } from "../runtime/runtimeObservation.js";
|
|
5
5
|
import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
|
|
6
|
+
import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
|
|
6
7
|
import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
|
|
7
8
|
import { resolveRuntimeHookRunFence } from "./runtimeHookRunFence.js";
|
|
8
9
|
let structuredSequence = 0;
|
|
@@ -256,10 +257,26 @@ async function persistAndApply(home, observations, taskId, roleName) {
|
|
|
256
257
|
const inbox = new FileRuntimeEventInbox(home);
|
|
257
258
|
for (const entry of observations)
|
|
258
259
|
inbox.enqueueObservation(entry);
|
|
260
|
+
// The immutable inbox is authoritative. During a release/update handover the
|
|
261
|
+
// old Controller is draining and the replacement is not ready yet; leave the
|
|
262
|
+
// entries for normal inbox replay instead of turning a healthy Provider Turn
|
|
263
|
+
// into a transport failure.
|
|
264
|
+
if (isForeignHandoverLockHeld(home))
|
|
265
|
+
return;
|
|
259
266
|
for (const entry of observations) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
267
|
+
let result;
|
|
268
|
+
try {
|
|
269
|
+
result = await callController(home, "runtime.observation-apply", entry, {
|
|
270
|
+
timeoutMs: 10_000
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
// Close the race where the handover begins after the first check but
|
|
275
|
+
// before this socket call. The durable entry remains pending for replay.
|
|
276
|
+
if (isForeignHandoverLockHeld(home))
|
|
277
|
+
return;
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
263
280
|
// A fast Provider can accept the initial Turn before the scheduler call
|
|
264
281
|
// that launched this Host has returned and committed `run.pushed`. The
|
|
265
282
|
// immutable inbox entry already makes that exact fenced fact durable;
|
|
@@ -14,6 +14,15 @@ export class ControllerClientError extends Error {
|
|
|
14
14
|
this.name = "ControllerClientError";
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
/** The Controller may have committed the request before this client lost its acknowledgement. */
|
|
18
|
+
export function controllerCallMayHaveApplied(error) {
|
|
19
|
+
return error instanceof ControllerClientError
|
|
20
|
+
&& [
|
|
21
|
+
"CONTROLLER_TIMEOUT",
|
|
22
|
+
"CONTROLLER_UNAVAILABLE",
|
|
23
|
+
"INVALID_RESPONSE"
|
|
24
|
+
].includes(error.code);
|
|
25
|
+
}
|
|
17
26
|
export async function readControllerDiscovery(home) {
|
|
18
27
|
const discoveryPath = join(home, CONTROLLER_DISCOVERY_PATH);
|
|
19
28
|
try {
|
|
@@ -210,6 +219,7 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
|
|
|
210
219
|
const socket = createConnection(socketPath);
|
|
211
220
|
let buffer = Buffer.alloc(0);
|
|
212
221
|
let settled = false;
|
|
222
|
+
let deliveryStarted = false;
|
|
213
223
|
const timer = setTimeout(() => {
|
|
214
224
|
fail(new ControllerClientError("CONTROLLER_TIMEOUT", "Controller request timed out."));
|
|
215
225
|
}, timeoutMs);
|
|
@@ -229,7 +239,13 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
|
|
|
229
239
|
socket.destroy();
|
|
230
240
|
reject(error);
|
|
231
241
|
};
|
|
232
|
-
socket.on("connect", () =>
|
|
242
|
+
socket.on("connect", () => {
|
|
243
|
+
// Once write begins, a missing response cannot prove that the Controller
|
|
244
|
+
// did not commit the request. Callers must use their domain identity to
|
|
245
|
+
// decide whether an explicit retry is safe.
|
|
246
|
+
deliveryStarted = true;
|
|
247
|
+
socket.write(requestLine);
|
|
248
|
+
});
|
|
233
249
|
socket.on("data", (chunk) => {
|
|
234
250
|
if (settled)
|
|
235
251
|
return;
|
|
@@ -260,7 +276,9 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
|
|
|
260
276
|
fail(invalidResponse());
|
|
261
277
|
});
|
|
262
278
|
socket.on("error", () => {
|
|
263
|
-
fail(
|
|
279
|
+
fail(deliveryStarted
|
|
280
|
+
? new ControllerClientError("CONTROLLER_DELIVERY_UNKNOWN", "Controller request delivery is unknown.")
|
|
281
|
+
: new ControllerClientError("CONTROLLER_UNAVAILABLE", "Controller is unavailable."));
|
|
264
282
|
});
|
|
265
283
|
});
|
|
266
284
|
}
|
|
@@ -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");
|