@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
|
@@ -62,7 +62,7 @@ export function classifyReviewRoundOutcome(round, evidence) {
|
|
|
62
62
|
reason: corroborationFailure
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
-
/** True only when a Round
|
|
65
|
+
/** True only when a Round contains semantic Reviewer evidence. */
|
|
66
66
|
export function isSemanticReviewRound(round, evidence) {
|
|
67
67
|
return classifyReviewRoundOutcome(round, evidence)?.kind === "semantic";
|
|
68
68
|
}
|
|
@@ -120,7 +120,7 @@ export function finishReviewRound(round, status, summary, now, result = {}) {
|
|
|
120
120
|
return terminal;
|
|
121
121
|
}
|
|
122
122
|
// Fail closed: a completed delta Round without an explicit, valid
|
|
123
|
-
// disposition
|
|
123
|
+
// disposition remains non-accepting for Leader routing. Uncertainty never accepts.
|
|
124
124
|
const disposition = result.deltaDisposition ?? "requires-full-review";
|
|
125
125
|
const reasoning = result.deltaReasoning ?? result.report ?? summary;
|
|
126
126
|
return validateReviewRound({
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { nextPendingBatch, mailboxHasWork } from "../coordination/workMailbox.js";
|
|
2
|
+
import { runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
3
|
+
/** One authoritative read-only projection for a Task Reviewer Role slot. */
|
|
4
|
+
export function projectReviewerAvailability(store, taskId, reviewerRoleName) {
|
|
5
|
+
const active = store.getActiveAgentRun(taskId, reviewerRoleName)
|
|
6
|
+
?? store.listAgentRuns(taskId).find((run) => (run.roleName === reviewerRoleName && run.status === "active"));
|
|
7
|
+
if (active !== null && active !== undefined) {
|
|
8
|
+
return {
|
|
9
|
+
kind: "busy",
|
|
10
|
+
reviewerRoleName,
|
|
11
|
+
phase: "active-run",
|
|
12
|
+
activeRunId: active.id,
|
|
13
|
+
...(active.reviewRoundId === undefined
|
|
14
|
+
? {}
|
|
15
|
+
: { activeReviewRoundId: active.reviewRoundId }),
|
|
16
|
+
startedAt: active.createdAt,
|
|
17
|
+
retryable: true,
|
|
18
|
+
retryAfterSeconds: 5
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const activeRound = store.listReviewRounds(taskId).find((round) => ((round.scope ?? "work-item") === "task"
|
|
22
|
+
&& (round.status === "pending" || round.status === "running")
|
|
23
|
+
&& (round.reviewerRoleName === reviewerRoleName
|
|
24
|
+
|| round.executionGroup?.lanes.some(({ roleName }) => roleName === reviewerRoleName) === true)));
|
|
25
|
+
if (activeRound !== undefined) {
|
|
26
|
+
return {
|
|
27
|
+
kind: "busy",
|
|
28
|
+
reviewerRoleName,
|
|
29
|
+
phase: "review-slot",
|
|
30
|
+
activeReviewRoundId: activeRound.id,
|
|
31
|
+
startedAt: activeRound.createdAt,
|
|
32
|
+
retryable: true,
|
|
33
|
+
retryAfterSeconds: 5
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const reviewerMailbox = store.getWorkMailbox({
|
|
37
|
+
kind: "role",
|
|
38
|
+
taskId,
|
|
39
|
+
roleName: reviewerRoleName
|
|
40
|
+
});
|
|
41
|
+
if (reviewerMailbox !== null && mailboxHasWork(reviewerMailbox)) {
|
|
42
|
+
return mailboxBusy(reviewerRoleName, "mailbox", reviewerMailbox);
|
|
43
|
+
}
|
|
44
|
+
const runtimeMailbox = store.getWorkMailbox(runtimeLifecycleTarget({
|
|
45
|
+
scope: "task",
|
|
46
|
+
taskId,
|
|
47
|
+
roleName: reviewerRoleName
|
|
48
|
+
}));
|
|
49
|
+
if (runtimeMailbox !== null && mailboxHasWork(runtimeMailbox)) {
|
|
50
|
+
return mailboxBusy(reviewerRoleName, "runtime-lifecycle", runtimeMailbox);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
kind: "available",
|
|
54
|
+
reviewerRoleName,
|
|
55
|
+
retryable: true
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function mailboxBusy(reviewerRoleName, phase, mailbox) {
|
|
59
|
+
const pending = nextPendingBatch(mailbox);
|
|
60
|
+
const startedAt = mailbox.processing?.startedAt ?? pending?.firstQueuedAt;
|
|
61
|
+
return {
|
|
62
|
+
kind: "busy",
|
|
63
|
+
reviewerRoleName,
|
|
64
|
+
phase,
|
|
65
|
+
...(startedAt === undefined ? {} : { startedAt }),
|
|
66
|
+
retryable: true,
|
|
67
|
+
retryAfterSeconds: 5
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
|
|
3
3
|
import { actionableExecutionLaneRecoveries } from "../execution/executionHealth.js";
|
|
4
|
+
import { runOwnsBlockingProviderContinuation } from "../runtime/runtimeContinuationProjection.js";
|
|
5
|
+
import { runHasActiveRuntimeOperations } from "../runtime/runtimeObservation.js";
|
|
4
6
|
export const RUN_RECOVERY_ACTIONS = [
|
|
5
7
|
"diagnose",
|
|
6
8
|
"retry",
|
|
7
|
-
"replace-session",
|
|
8
9
|
"terminate"
|
|
9
10
|
];
|
|
10
11
|
/**
|
|
@@ -17,13 +18,32 @@ export function readRunRecoveryFacts(store, taskId, runId) {
|
|
|
17
18
|
return null;
|
|
18
19
|
const task = store.getTask(taskId);
|
|
19
20
|
const sessionSet = store.getTaskRoleSessionSet(taskId, run.roleName);
|
|
21
|
+
const events = store.listEvents(taskId);
|
|
22
|
+
const roleMailbox = store.getWorkMailbox?.({
|
|
23
|
+
kind: "role",
|
|
24
|
+
taskId,
|
|
25
|
+
roleName: run.roleName
|
|
26
|
+
}) ?? null;
|
|
20
27
|
const progress = latestRunDurableProgressAt(store, taskId, run.roleName, runId);
|
|
21
28
|
return {
|
|
22
29
|
run,
|
|
23
30
|
task: task === null ? null : { id: task.id, status: task.status },
|
|
24
31
|
sessionSet,
|
|
32
|
+
inputDeliveryUnsettled: roleMailbox?.inputDelivery != null,
|
|
33
|
+
blockingProviderContinuation: runOwnsBlockingProviderContinuation(events, {
|
|
34
|
+
taskId,
|
|
35
|
+
roleName: run.roleName,
|
|
36
|
+
runId: run.id,
|
|
37
|
+
agentId: run.effective.agentId
|
|
38
|
+
}),
|
|
39
|
+
activeRuntimeOperation: runHasActiveRuntimeOperations(events, {
|
|
40
|
+
taskId,
|
|
41
|
+
roleName: run.roleName,
|
|
42
|
+
runId: run.id,
|
|
43
|
+
agentId: run.effective.agentId
|
|
44
|
+
}),
|
|
25
45
|
progress,
|
|
26
|
-
latestProviderObservation: latestRunProviderObservation(
|
|
46
|
+
latestProviderObservation: latestRunProviderObservation(events, runId)
|
|
27
47
|
};
|
|
28
48
|
}
|
|
29
49
|
/**
|
|
@@ -72,9 +92,12 @@ export function projectRunRecovery(facts) {
|
|
|
72
92
|
? ["accepted", "ambiguous"]
|
|
73
93
|
: ["rejected", "ambiguous"];
|
|
74
94
|
const blocked = recoveryBlocker(facts, session, canonicalProgressAt);
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
const sessionTerminal = session?.status === "stopped" || session?.status === "broken";
|
|
96
|
+
const providerTerminal = sessionSet?.providerBinding?.turn?.status === "failed"
|
|
97
|
+
|| sessionSet?.providerBinding?.turn?.status === "cancelled"
|
|
98
|
+
|| sessionSet?.providerBinding?.turn?.status === "rejected";
|
|
99
|
+
const supportedActions = RUN_RECOVERY_ACTIONS.filter((action) => ((action !== "retry" || !sessionTerminal)
|
|
100
|
+
&& (action !== "terminate" || sessionTerminal || providerTerminal)));
|
|
78
101
|
const actions = blocked === null
|
|
79
102
|
? supportedActions.map((action) => buildActionPlan(facts, action, session, canonicalProgressAt))
|
|
80
103
|
: [];
|
|
@@ -139,6 +162,23 @@ function recoveryBlocker(facts, session, canonicalProgressAt) {
|
|
|
139
162
|
return "progress-unavailable";
|
|
140
163
|
if (session === null)
|
|
141
164
|
return "session-missing";
|
|
165
|
+
if (facts.inputDeliveryUnsettled)
|
|
166
|
+
return "provider-input-delivery-unsettled";
|
|
167
|
+
if (facts.blockingProviderContinuation)
|
|
168
|
+
return "provider-continuation-writer-owned";
|
|
169
|
+
if (facts.activeRuntimeOperation)
|
|
170
|
+
return "provider-operation-active";
|
|
171
|
+
const binding = facts.sessionSet?.providerBinding;
|
|
172
|
+
if (run.deliveredAt !== undefined
|
|
173
|
+
&& (binding === null || binding?.turn === null))
|
|
174
|
+
return "provider-turn-state-missing";
|
|
175
|
+
if (binding !== null && binding !== undefined) {
|
|
176
|
+
if (["submitting", "accepted", "running", "delivery-unknown"].includes(binding.turn?.status ?? ""))
|
|
177
|
+
return "provider-turn-unsettled";
|
|
178
|
+
if (binding.authority.owner === "human" || binding.authority.owner === "unknown") {
|
|
179
|
+
return "provider-writer-authority-unavailable";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
142
182
|
return null;
|
|
143
183
|
}
|
|
144
184
|
function buildActionPlan(facts, action, session, canonicalProgressAt) {
|
|
@@ -208,6 +248,5 @@ function actionAcceptance(facts, action) {
|
|
|
208
248
|
const ACTION_REASONS = {
|
|
209
249
|
diagnose: "Collect bounded diagnostics before any state-changing recovery.",
|
|
210
250
|
retry: "Request another provider turn on the same native Session when the failure is transient.",
|
|
211
|
-
"replace-session": "Request a fresh native Session when the current one is unusable.",
|
|
212
251
|
terminate: "Fail the Run explicitly when recovery is not viable."
|
|
213
252
|
};
|
|
@@ -33,18 +33,24 @@ export function mapAgentDriverUsage(input, usage) {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
function observation(input, mapped) {
|
|
36
|
-
const
|
|
36
|
+
const requestOccurrenceId = mapped.kind === "activity.observed"
|
|
37
|
+
&& mapped.payload.usage?.semantics === "request-context"
|
|
38
|
+
? requireRequestOccurrenceId(mapped.payload.activityId)
|
|
39
|
+
: undefined;
|
|
40
|
+
const eventId = hookEventId(input, mapped, requestOccurrenceId);
|
|
37
41
|
const fence = { ...input.fence, ...mapped.fence };
|
|
38
42
|
return createRuntimeObservation({
|
|
39
43
|
schemaVersion: 2,
|
|
40
44
|
eventId,
|
|
41
|
-
semanticKey:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
semanticKey: requestOccurrenceId === undefined
|
|
46
|
+
? runtimeObservationSemanticKey({
|
|
47
|
+
eventId,
|
|
48
|
+
kind: mapped.kind,
|
|
49
|
+
fence,
|
|
50
|
+
...(input.sequence === undefined ? {} : { sequence: input.sequence }),
|
|
51
|
+
payload: mapped.payload
|
|
52
|
+
})
|
|
53
|
+
: `provider-request:${eventId}`,
|
|
48
54
|
kind: mapped.kind,
|
|
49
55
|
authority: "provider-structured",
|
|
50
56
|
receivedAt: input.receivedAt,
|
|
@@ -60,13 +66,21 @@ function requireMatchingDriver(input) {
|
|
|
60
66
|
throw new Error("Agent Driver Hook fence does not match the selected Driver.");
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
|
-
function hookEventId(input, mapped) {
|
|
69
|
+
function hookEventId(input, mapped, requestOccurrenceId) {
|
|
64
70
|
const digest = createHash("sha256").update(JSON.stringify([
|
|
65
71
|
input.driver.id,
|
|
66
72
|
input.hookEventName,
|
|
67
73
|
input.fence,
|
|
68
74
|
mapped.kind,
|
|
69
|
-
|
|
75
|
+
...(requestOccurrenceId === undefined
|
|
76
|
+
? [mapped.payload]
|
|
77
|
+
: [{ requestOccurrenceId }])
|
|
70
78
|
])).digest("hex");
|
|
71
79
|
return `runtime-observation-${digest}`;
|
|
72
80
|
}
|
|
81
|
+
function requireRequestOccurrenceId(value) {
|
|
82
|
+
if (value === undefined || value.trim().length === 0 || value.includes("\0")) {
|
|
83
|
+
throw new Error("Request-context usage requires a stable activity id.");
|
|
84
|
+
}
|
|
85
|
+
return value.trim();
|
|
86
|
+
}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { readdir, readFile, rename, unlink } from "node:fs/promises";
|
|
2
|
+
import { chmodSync, mkdirSync, rmSync } from "node:fs";
|
|
4
3
|
import { tmpdir } from "node:os";
|
|
5
4
|
import { dirname, join, resolve } from "node:path";
|
|
6
5
|
import { createConnection, createServer } from "node:net";
|
|
7
6
|
import { createInterface } from "node:readline";
|
|
8
|
-
import { callController, ControllerClientError } from "../core/controllerClient.js";
|
|
7
|
+
import { callController, controllerCallMayHaveApplied, ControllerClientError } from "../core/controllerClient.js";
|
|
9
8
|
import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
|
|
9
|
+
import { callFileTaskController } from "../controller/clientRuntime.js";
|
|
10
|
+
import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
|
|
10
11
|
import { publishStructuredProviderAccepted, publishStructuredProviderActivationTerminal, publishStructuredConversationRecoverability, publishStructuredProviderOpened, publishStructuredProviderTerminal } from "../controller/structuredProviderObservation.js";
|
|
11
12
|
import { validateAgentHostLaunchPayload } from "./launchBroker.js";
|
|
12
13
|
import { ProviderDeliveryUnknownError, ProviderConversationMissingError, ProviderTurnRejectedError, startStructuredProviderSession } from "./structuredProviderHost.js";
|
|
13
14
|
import { sameProviderAuthorityFence, validateProviderAuthorityFence } from "./providerAuthorityFence.js";
|
|
14
15
|
import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
|
|
15
|
-
import {
|
|
16
|
+
import { persistRuntimeProcessExitObservation, replayRuntimeProcessExitOutbox } from "./processExitOutbox.js";
|
|
17
|
+
import { readRuntimeStopReceipt, removeRuntimeStopReceipt, writeRuntimeStopReceipt } from "./runtimeStopReceipt.js";
|
|
16
18
|
import { AGENT_HOST_CONTROL_TIMEOUT_MS, AGENT_HOST_READY_TIMEOUT_MS } from "./runtimeDeadlines.js";
|
|
17
19
|
export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v2";
|
|
18
20
|
const HOST_CONTROL_MAX_BYTES = 32 * 1024;
|
|
@@ -26,6 +28,7 @@ export async function runAgentHost(input) {
|
|
|
26
28
|
let session;
|
|
27
29
|
let sessionPayload;
|
|
28
30
|
let activeTurnPayload;
|
|
31
|
+
const switchDetachedSessions = new WeakSet();
|
|
29
32
|
let activationId;
|
|
30
33
|
let conversationRecoverability = "unknown";
|
|
31
34
|
let authority;
|
|
@@ -116,19 +119,21 @@ export async function runAgentHost(input) {
|
|
|
116
119
|
const stopReceipt = readRuntimeStopReceipt(input.home, currentPayload.launchId);
|
|
117
120
|
const observedAt = new Date().toISOString();
|
|
118
121
|
const failures = [];
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
122
|
+
if (!switchDetachedSessions.has(providerSession)) {
|
|
123
|
+
try {
|
|
124
|
+
await publishStructuredProviderActivationTerminal({
|
|
125
|
+
home: input.home,
|
|
126
|
+
environment: currentPayload.environment,
|
|
127
|
+
conversationId: providerSession.conversationId,
|
|
128
|
+
nativeSessionId: providerSession.nativeSessionId,
|
|
129
|
+
activationId: currentActivationId,
|
|
130
|
+
status: stopReceipt !== null || hostStopRequested ? "ended" : "failed",
|
|
131
|
+
observedAt
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
failures.push(`activation terminal: ${errorText(error)}`);
|
|
136
|
+
}
|
|
132
137
|
}
|
|
133
138
|
let exitPersisted = false;
|
|
134
139
|
try {
|
|
@@ -163,7 +168,7 @@ export async function runAgentHost(input) {
|
|
|
163
168
|
if (stopReceipt !== null && exitPersisted) {
|
|
164
169
|
removeRuntimeStopReceipt(input.home, currentPayload.launchId);
|
|
165
170
|
}
|
|
166
|
-
if (hostStopRequested)
|
|
171
|
+
if (hostStopRequested || !ownsCurrentSession)
|
|
167
172
|
return;
|
|
168
173
|
updateSnapshot(hostSnapshot(failures.length === 0 ? "exited" : "failed", {
|
|
169
174
|
launchId: currentPayload.launchId,
|
|
@@ -196,9 +201,12 @@ export async function runAgentHost(input) {
|
|
|
196
201
|
throw new Error("Agent Host still owns an unsettled Provider Turn.");
|
|
197
202
|
}
|
|
198
203
|
const requestedAuthority = validateProviderAuthorityFence(providerControl.authority);
|
|
199
|
-
|
|
204
|
+
const replacesCurrentConversation = session !== undefined && providerControl.kind === "new";
|
|
205
|
+
if (!replacesCurrentConversation && authority === undefined)
|
|
200
206
|
authority = requestedAuthority;
|
|
201
|
-
else if (!
|
|
207
|
+
else if (!replacesCurrentConversation
|
|
208
|
+
&& authority !== undefined
|
|
209
|
+
&& !sameProviderAuthorityFence(authority, requestedAuthority)) {
|
|
202
210
|
throw new Error("Agent Host launch carries a stale Provider authority fence.");
|
|
203
211
|
}
|
|
204
212
|
updateSnapshot(hostSnapshot("starting", {
|
|
@@ -217,6 +225,43 @@ export async function runAgentHost(input) {
|
|
|
217
225
|
let providerAcceptedAttemptId;
|
|
218
226
|
let durableInitialTurn;
|
|
219
227
|
try {
|
|
228
|
+
if (replacesCurrentConversation) {
|
|
229
|
+
const previousSession = session;
|
|
230
|
+
const previousPayload = sessionPayload;
|
|
231
|
+
const previousActivationId = activationId ?? previousPayload?.launchId;
|
|
232
|
+
if (previousPayload === undefined || previousActivationId === undefined
|
|
233
|
+
|| authority?.owner !== "controller") {
|
|
234
|
+
throw new Error("Agent Host cannot detach an inexact Provider Activation.");
|
|
235
|
+
}
|
|
236
|
+
await detachDurableProviderForConversationSwitch(input.home, {
|
|
237
|
+
taskId: requiredEnvironment(next.environment.YUI_TASK_ID, "Task id"),
|
|
238
|
+
roleName: requiredEnvironment(next.environment.YUI_ROLE, "Role name"),
|
|
239
|
+
runId: requiredEnvironment(next.environment.YUI_RUN_ID, "Run id"),
|
|
240
|
+
agentId: requiredEnvironment(next.environment.YUI_AGENT_ID, "Agent id"),
|
|
241
|
+
launchId: next.launchId,
|
|
242
|
+
previousConversationId: previousSession.conversationId,
|
|
243
|
+
previousNativeSessionId: previousSession.nativeSessionId,
|
|
244
|
+
previousActivationId,
|
|
245
|
+
nextAuthorityEpoch: requestedAuthority.epoch,
|
|
246
|
+
nextAuthorityHolderId: requestedAuthority.holderId,
|
|
247
|
+
observedAt: new Date().toISOString()
|
|
248
|
+
});
|
|
249
|
+
switchDetachedSessions.add(previousSession);
|
|
250
|
+
// The persistent Provider child belongs to the Activation that first
|
|
251
|
+
// created it, not to the most recent resume Run payload. The exit
|
|
252
|
+
// observer carries that Activation launch id, so the stop receipt must
|
|
253
|
+
// use the same identity or the expected switch detach is misclassified
|
|
254
|
+
// as an abnormal child exit and generic cleanup can kill the Host.
|
|
255
|
+
writeRuntimeStopReceipt(input.home, previousActivationId, new Date());
|
|
256
|
+
authority = undefined;
|
|
257
|
+
await terminateProviderSessionForConversationSwitch(previousSession);
|
|
258
|
+
if (session === previousSession)
|
|
259
|
+
session = undefined;
|
|
260
|
+
sessionPayload = undefined;
|
|
261
|
+
activationId = undefined;
|
|
262
|
+
conversationRecoverability = "unknown";
|
|
263
|
+
authority = requestedAuthority;
|
|
264
|
+
}
|
|
220
265
|
if (session !== undefined) {
|
|
221
266
|
if (session.adapterId !== providerControl.adapterId
|
|
222
267
|
|| providerControl.mode !== "resume"
|
|
@@ -493,30 +538,12 @@ export async function runAgentHost(input) {
|
|
|
493
538
|
}
|
|
494
539
|
};
|
|
495
540
|
const durableTurn = hostTurnControlParams(currentPayload, currentSession.nativeSessionId, currentAuthority, attemptId);
|
|
496
|
-
|
|
497
|
-
await beginDurableProviderTurn(input.home, durableTurn);
|
|
498
|
-
}
|
|
499
|
-
catch (error) {
|
|
500
|
-
await callController(input.home, "runtime.provider-turn-submission-resolve", {
|
|
501
|
-
...durableTurn,
|
|
502
|
-
status: "rejected",
|
|
503
|
-
reason: `Human Turn intent acknowledgement failed before Provider write: ${errorText(error)}`,
|
|
504
|
-
observedAt: new Date().toISOString()
|
|
505
|
-
}).catch(() => { });
|
|
506
|
-
throw error;
|
|
507
|
-
}
|
|
541
|
+
await beginDurableProviderTurn(input.home, durableTurn);
|
|
508
542
|
try {
|
|
509
543
|
await submitTurn(turnControl);
|
|
510
544
|
}
|
|
511
545
|
catch (error) {
|
|
512
|
-
await
|
|
513
|
-
...durableTurn,
|
|
514
|
-
status: error instanceof ProviderDeliveryUnknownError
|
|
515
|
-
? "delivery-unknown"
|
|
516
|
-
: "rejected",
|
|
517
|
-
reason: errorText(error),
|
|
518
|
-
observedAt: new Date().toISOString()
|
|
519
|
-
}).catch(() => { });
|
|
546
|
+
await resolveProviderTurnSubmission(input.home, durableTurn, error);
|
|
520
547
|
throw error;
|
|
521
548
|
}
|
|
522
549
|
process.stdout.write("Provider accepted the human Turn; waiting for its terminal boundary.\n");
|
|
@@ -670,50 +697,19 @@ async function redeem(home, launchId, ticket) {
|
|
|
670
697
|
return validateAgentHostLaunchPayload(result);
|
|
671
698
|
}
|
|
672
699
|
async function persistAndSubmitExit(home, observation) {
|
|
673
|
-
const directory = resolve(join(home, "runtime", "agent-host-outbox"));
|
|
674
|
-
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
675
|
-
const path = join(directory, `${observation.hostInstanceId}.jsonl`);
|
|
676
|
-
const descriptor = openSync(path, "a", 0o600);
|
|
677
700
|
try {
|
|
678
|
-
|
|
679
|
-
fsyncSync(descriptor);
|
|
680
|
-
}
|
|
681
|
-
finally {
|
|
682
|
-
closeSync(descriptor);
|
|
683
|
-
}
|
|
684
|
-
chmodSync(path, 0o600);
|
|
685
|
-
await callController(home, "runtime.process-exit-observe", observation);
|
|
686
|
-
rmSync(path, { force: true });
|
|
687
|
-
}
|
|
688
|
-
async function replayExitOutbox(home) {
|
|
689
|
-
const directory = resolve(join(home, "runtime", "agent-host-outbox"));
|
|
690
|
-
let entries;
|
|
691
|
-
try {
|
|
692
|
-
entries = await readdir(directory);
|
|
701
|
+
await persistRuntimeProcessExitObservation(home, observation, (value) => callController(home, "runtime.process-exit-observe", value).then(() => undefined));
|
|
693
702
|
}
|
|
694
703
|
catch (error) {
|
|
695
|
-
|
|
704
|
+
// The durable outbox is the acknowledgement during a planned Controller
|
|
705
|
+
// gap. The replacement Controller drains it before accepting new work.
|
|
706
|
+
if (isForeignHandoverLockHeld(home))
|
|
696
707
|
return;
|
|
697
708
|
throw error;
|
|
698
709
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
try {
|
|
703
|
-
await rename(path, claimed);
|
|
704
|
-
}
|
|
705
|
-
catch (error) {
|
|
706
|
-
if (error.code === "ENOENT")
|
|
707
|
-
continue;
|
|
708
|
-
throw error;
|
|
709
|
-
}
|
|
710
|
-
const lines = (await readFile(claimed, "utf8")).split("\n").filter(Boolean);
|
|
711
|
-
for (const line of lines) {
|
|
712
|
-
const observation = validateRuntimeProcessExitObservation(JSON.parse(line));
|
|
713
|
-
await callController(home, "runtime.process-exit-observe", observation);
|
|
714
|
-
}
|
|
715
|
-
await unlink(claimed);
|
|
716
|
-
}
|
|
710
|
+
}
|
|
711
|
+
async function replayExitOutbox(home) {
|
|
712
|
+
await replayRuntimeProcessExitOutbox(home, (observation) => callController(home, "runtime.process-exit-observe", observation).then(() => undefined));
|
|
717
713
|
}
|
|
718
714
|
/** Internal socket boundary exported for transport-level verification. */
|
|
719
715
|
export async function openAgentHostControl(home, payload, snapshot, dispatch) {
|
|
@@ -906,26 +902,104 @@ function hostTurnControlParams(payload, nativeSessionId, authority, attemptId) {
|
|
|
906
902
|
}
|
|
907
903
|
async function beginDurableProviderTurn(home, durableTurn) {
|
|
908
904
|
try {
|
|
909
|
-
await
|
|
905
|
+
await callControllerIdempotently(home, "runtime.provider-turn-begin", durableTurn);
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
if (!(error instanceof ControllerAcknowledgementUnknownError))
|
|
909
|
+
throw error;
|
|
910
|
+
await resolveProviderTurnSubmission(home, durableTurn, new Error(`Provider Turn intent acknowledgement failed before Provider write: ${errorText(error)}`));
|
|
911
|
+
throw error;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
async function detachDurableProviderForConversationSwitch(home, request) {
|
|
915
|
+
await callControllerIdempotently(home, "runtime.conversation-switch-detach", request);
|
|
916
|
+
}
|
|
917
|
+
async function callControllerIdempotently(home, method, request) {
|
|
918
|
+
try {
|
|
919
|
+
await callAgentController(home, method, request);
|
|
910
920
|
}
|
|
911
921
|
catch (error) {
|
|
912
|
-
if (!(error instanceof ControllerClientError)
|
|
922
|
+
if (!(error instanceof ControllerClientError)
|
|
923
|
+
|| (error.code !== "INTERNAL_ERROR" && !controllerCallMayHaveApplied(error))) {
|
|
913
924
|
throw error;
|
|
914
925
|
}
|
|
915
|
-
|
|
916
|
-
//
|
|
917
|
-
|
|
926
|
+
const firstCallMayHaveApplied = controllerCallMayHaveApplied(error);
|
|
927
|
+
// These methods carry exact attempt, launch, and authority fences. A
|
|
928
|
+
// bounded replay confirms a commit whose acknowledgement may have been lost.
|
|
929
|
+
try {
|
|
930
|
+
await callAgentController(home, method, request);
|
|
931
|
+
}
|
|
932
|
+
catch (replayError) {
|
|
933
|
+
if (firstCallMayHaveApplied || controllerCallMayHaveApplied(replayError)) {
|
|
934
|
+
throw new ControllerAcknowledgementUnknownError(`${method} may have been committed, but its acknowledgement could not be confirmed: ${errorText(replayError)}`);
|
|
935
|
+
}
|
|
936
|
+
throw replayError;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
class ControllerAcknowledgementUnknownError extends Error {
|
|
941
|
+
name = "ControllerAcknowledgementUnknownError";
|
|
942
|
+
}
|
|
943
|
+
async function terminateProviderSessionForConversationSwitch(providerSession) {
|
|
944
|
+
providerSession.terminate("SIGTERM");
|
|
945
|
+
const forceKill = setTimeout(() => providerSession.terminate("SIGKILL"), 3_000);
|
|
946
|
+
forceKill.unref();
|
|
947
|
+
let hardTimeout;
|
|
948
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
949
|
+
hardTimeout = setTimeout(() => reject(new Error("Old Provider Activation did not exit after its switch authority was revoked.")), 8_000);
|
|
950
|
+
hardTimeout.unref();
|
|
951
|
+
});
|
|
952
|
+
try {
|
|
953
|
+
await Promise.race([providerSession.waitForExit(), timeout]);
|
|
954
|
+
}
|
|
955
|
+
finally {
|
|
956
|
+
clearTimeout(forceKill);
|
|
957
|
+
if (hardTimeout !== undefined)
|
|
958
|
+
clearTimeout(hardTimeout);
|
|
918
959
|
}
|
|
919
960
|
}
|
|
920
961
|
async function resolveProviderTurnSubmission(home, durableTurn, error) {
|
|
921
|
-
|
|
962
|
+
const attemptId = durableTurn.attemptId;
|
|
963
|
+
if (typeof attemptId !== "string") {
|
|
964
|
+
throw new Error("Provider Turn resolution has no attempt id.");
|
|
965
|
+
}
|
|
966
|
+
const request = {
|
|
922
967
|
...durableTurn,
|
|
923
968
|
status: error instanceof ProviderDeliveryUnknownError
|
|
924
969
|
? "delivery-unknown"
|
|
925
970
|
: "rejected",
|
|
926
971
|
reason: errorText(error),
|
|
927
972
|
observedAt: new Date().toISOString()
|
|
928
|
-
}
|
|
973
|
+
};
|
|
974
|
+
try {
|
|
975
|
+
await callControllerIdempotently(home, "runtime.provider-turn-submission-resolve", request);
|
|
976
|
+
}
|
|
977
|
+
catch (resolutionError) {
|
|
978
|
+
throw new ProviderDeliveryUnknownError(`Provider submission outcome could not be durably resolved: ${errorText(resolutionError)}. Original outcome: ${errorText(error)}`, attemptId);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Existing in-flight launches keep using the old Controller while it drains.
|
|
983
|
+
* If the socket is already gone under an explicit handover fence, wait for
|
|
984
|
+
* the replacement and retry the domain-idempotent Agent Host operation.
|
|
985
|
+
*/
|
|
986
|
+
async function callAgentController(home, method, params) {
|
|
987
|
+
try {
|
|
988
|
+
await callController(home, method, params);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
catch (error) {
|
|
992
|
+
if (!isControllerUnavailable(error) || !isForeignHandoverLockHeld(home)) {
|
|
993
|
+
throw error;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
await callFileTaskController(home, method, params);
|
|
997
|
+
}
|
|
998
|
+
function isControllerUnavailable(error) {
|
|
999
|
+
return error instanceof ControllerClientError
|
|
1000
|
+
&& (error.code === "CONTROLLER_NOT_RUNNING"
|
|
1001
|
+
|| error.code === "CONTROLLER_UNAVAILABLE"
|
|
1002
|
+
|| error.code === "CONTROLLER_DRAINING");
|
|
929
1003
|
}
|
|
930
1004
|
function requiredEnvironment(value, label) {
|
|
931
1005
|
if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
|
|
@@ -51,8 +51,8 @@ const STRUCTURED_CLI_CAPABILITIES = Object.freeze({
|
|
|
51
51
|
preInputReadiness: "unavailable",
|
|
52
52
|
promptAcceptance: "exact",
|
|
53
53
|
turnLifecycle: "exact",
|
|
54
|
-
//
|
|
55
|
-
//
|
|
54
|
+
// Transcript sources may emit an explicit activity identity separately
|
|
55
|
+
// from usage snapshots. Numeric token values never decide runtime health.
|
|
56
56
|
operations: Object.freeze(["tool", "subagent"]),
|
|
57
57
|
waiting: Object.freeze(["permission"]),
|
|
58
58
|
usage: "streaming-cumulative",
|
|
@@ -70,7 +70,8 @@ export const BUILTIN_AGENT_DRIVERS = Object.freeze([
|
|
|
70
70
|
observation: Object.freeze({
|
|
71
71
|
...STRUCTURED_CLI_CAPABILITIES.observation,
|
|
72
72
|
sessionBootstrap: "preallocated",
|
|
73
|
-
preInputReadiness: "exact"
|
|
73
|
+
preInputReadiness: "exact",
|
|
74
|
+
usage: "event-snapshot"
|
|
74
75
|
})
|
|
75
76
|
}),
|
|
76
77
|
runtime: Object.freeze({
|