@zq-silk/yui 0.15.9 → 0.15.11
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/dist/agent/launchEnvironment.js +7 -0
- package/dist/cli/commandCatalog.js +26 -6
- package/dist/cli/interactionPolicy.js +3 -3
- package/dist/cli/updateOrchestrator.js +24 -1
- package/dist/cli/updatePorts.js +7 -3
- package/dist/cli/upgradeCommand.js +42 -2
- package/dist/cli.js +336 -93
- package/dist/commands/globalRoleCommands.js +314 -4
- package/dist/commands/projectCommands.js +6 -7
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +511 -83
- package/dist/commands/taskIntegrationCommands.js +16 -38
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
- package/dist/commands/taskRoleRuntimeStatus.js +35 -0
- package/dist/context/taskContext.js +38 -2
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +3 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
- package/dist/controller/globalInputDelivery.js +119 -0
- package/dist/controller/jobControl.js +6 -2
- package/dist/controller/resourceInventory.js +14 -4
- package/dist/controller/resourceInventoryLinux.js +2 -6
- package/dist/controller/runtime.js +81 -6
- package/dist/controller/runtimeEventInbox.js +32 -3
- package/dist/controller/runtimeEventProcessor.js +26 -6
- package/dist/controller/runtimeHookRunFence.js +75 -19
- package/dist/controller/structuredProviderObservation.js +133 -70
- package/dist/coordination/workMailboxQueue.js +5 -0
- package/dist/execution/workItemExecutionProjection.js +1 -1
- package/dist/executor/agentExecutor.js +64 -4
- package/dist/executor/executorRegistry.js +3 -0
- package/dist/executor/fileRoleLaunchPlanner.js +78 -118
- package/dist/integration/deliveryObligation.js +2 -1
- package/dist/integration/gitIntegrationService.js +312 -382
- package/dist/integration/integrationAttempt.js +30 -4
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/integration/integrationSourceApplication.js +323 -0
- package/dist/message/globalInterrupt.js +33 -0
- package/dist/message/inputControlResolution.js +106 -0
- package/dist/message/message.js +367 -0
- package/dist/message/messageContinuation.js +126 -3
- package/dist/message/taskInterrupt.js +34 -0
- package/dist/observability/orchestrationMetrics.js +1 -1
- package/dist/release/releaseHandover.js +22 -0
- package/dist/release/releaseWorkflowPorts.js +15 -7
- package/dist/repository/gitWorkspace.js +72 -15
- package/dist/repository/taskWorkspaceCoordinator.js +134 -0
- package/dist/repository/taskWorkspacePreparer.js +120 -49
- package/dist/repository/workItemCandidateSnapshot.js +34 -0
- package/dist/resources/resourceDiscovery.js +3 -2
- package/dist/runtime/agentHost.js +152 -72
- package/dist/runtime/agentHostCompatibility.js +127 -0
- package/dist/runtime/agentHostProtocol.js +53 -0
- package/dist/runtime/executionEnvironment.js +0 -19
- package/dist/runtime/launchBroker.js +6 -0
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/taskRuntimeIsolation.js +30 -6
- package/dist/runtime/tmuxAdapters.js +5 -3
- package/dist/scheduler/operatorEvent.js +4 -0
- package/dist/scheduler/taskExecutionProjection.js +12 -1
- package/dist/scheduler/wakeReason.js +7 -1
- package/dist/scheduler/wakeupQueue.js +2 -0
- package/dist/setup/setupCommand.js +26 -8
- package/dist/storage/homeLayout.js +130 -0
- package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
- package/dist/storage/migrations/integrationContinuation.js +104 -0
- package/dist/storage/migrations/unifyHomeLayout.js +925 -0
- package/dist/storage/sqliteSchema.js +136 -4
- package/dist/storage/sqliteStore.js +40 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +128 -0
- package/dist/task/nextAction.js +44 -11
- package/dist/web/assets/client/app.js +55 -0
- package/dist/web/assets/client/components.js +1 -0
- package/dist/web/assets/client/i18n.js +6 -0
- package/dist/web/assets/client/taskSurface.js +106 -0
- package/dist/web/assets/client/view.js +7 -4
- package/dist/web/assets/shell.js +23 -0
- package/dist/web/assets/styles/layout.js +1 -1
- package/dist/web/assets/styles/widgets.js +12 -0
- package/dist/web/webServer.js +117 -1
- package/dist/web/webSnapshot.js +4 -3
- package/dist/web/webTaskSurface.js +222 -5
- package/dist/workspace/workItemChangeSetManager.js +18 -2
- package/docs/release-workflow.md +39 -0
- package/docs/release-workflow.zh-CN.md +29 -0
- package/docs/sqlite-control-plane-design.md +223 -1
- package/docs/testing/verification-levels.md +24 -0
- package/docs/testing/verification-levels.zh-CN.md +11 -0
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +151 -49
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-operator/SKILL.md +6 -1
- package/skills/yui-runtime/SKILL.md +27 -0
- package/skills/yui-runtime/references/publication.md +20 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { sendAgentHostRunControl, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
|
|
2
|
+
import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
3
|
+
import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
4
|
+
import { markGlobalRoleMessageNotDelivered } from "../message/message.js";
|
|
5
|
+
/** Messages are the only queue. Host's atomic begin arbitrates native writers. */
|
|
6
|
+
export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
7
|
+
for (const role of store.listGlobalRoles()) {
|
|
8
|
+
try {
|
|
9
|
+
let pending = store.listGlobalRoleMessages(role.name).filter(message => message.delivery === undefined && message.notDelivered === undefined
|
|
10
|
+
&& (message.inputControl?.action === "queue" || message.interruptThen !== undefined));
|
|
11
|
+
if (pending.length === 0 || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
|
|
12
|
+
continue;
|
|
13
|
+
let sessions = store.getGlobalRoleSessionSet(role.name);
|
|
14
|
+
const current = sessions?.providerBinding?.run;
|
|
15
|
+
const obsolete = pending.filter(message => {
|
|
16
|
+
const claim = message.interruptThen;
|
|
17
|
+
return claim !== undefined && (sessions?.activeAgentId !== claim.targetAgentId
|
|
18
|
+
|| sessions.sessions[claim.targetAgentId]?.nativeSessionId !== claim.targetNativeSessionId
|
|
19
|
+
|| sessions.providerBinding?.authority.epoch !== claim.targetAuthorityEpoch
|
|
20
|
+
|| sessions.providerBinding.authority.holderId !== claim.targetAuthorityHolderId);
|
|
21
|
+
});
|
|
22
|
+
for (const message of obsolete)
|
|
23
|
+
store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(message, "native-session-or-authority-changed", new Date()));
|
|
24
|
+
pending = pending.filter(message => !obsolete.includes(message));
|
|
25
|
+
if (pending.length === 0)
|
|
26
|
+
continue;
|
|
27
|
+
if (current != null && ["submitting", "accepted", "delivery-unknown"].includes(current.status))
|
|
28
|
+
continue;
|
|
29
|
+
const claims = pending.filter(message => message.interruptThen !== undefined);
|
|
30
|
+
let next;
|
|
31
|
+
if (claims.length > 0) {
|
|
32
|
+
next = claims.find(message => {
|
|
33
|
+
const claim = message.interruptThen;
|
|
34
|
+
const binding = sessions?.providerBinding;
|
|
35
|
+
return current?.attemptId === claim.targetAttemptId
|
|
36
|
+
&& ["completed", "failed", "cancelled"].includes(current.status)
|
|
37
|
+
&& sessions?.activeAgentId === claim.targetAgentId
|
|
38
|
+
&& sessions.sessions[claim.targetAgentId]?.nativeSessionId === claim.targetNativeSessionId
|
|
39
|
+
&& binding?.authority.owner === "controller"
|
|
40
|
+
&& binding.authority.epoch === claim.targetAuthorityEpoch
|
|
41
|
+
&& binding.authority.holderId === claim.targetAuthorityHolderId;
|
|
42
|
+
});
|
|
43
|
+
if (next === undefined)
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
next = pending[0];
|
|
48
|
+
const attemptId = `global-input:${role.name}/${next.id}`;
|
|
49
|
+
if (current?.attemptId === attemptId)
|
|
50
|
+
continue;
|
|
51
|
+
if (next.control !== undefined && next.control.outcome !== "rejected")
|
|
52
|
+
continue;
|
|
53
|
+
const matchesTarget = () => next.deliveryTarget === undefined
|
|
54
|
+
|| sessions?.activeAgentId === next.deliveryTarget.agentId
|
|
55
|
+
&& sessions.sessions[next.deliveryTarget.agentId]?.nativeSessionId === next.deliveryTarget.nativeSessionId;
|
|
56
|
+
if (!matchesTarget()) {
|
|
57
|
+
store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(next, "native-session-changed", new Date()));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
await ensure(role.name);
|
|
61
|
+
sessions = store.getGlobalRoleSessionSet(role.name);
|
|
62
|
+
const binding = sessions?.providerBinding;
|
|
63
|
+
if (binding == null || binding.authority.owner !== "controller")
|
|
64
|
+
continue;
|
|
65
|
+
const nativeSessionId = currentProviderConversation(binding).conversationId;
|
|
66
|
+
const persisted = store.listGlobalRoleMessages(role.name).find(message => message.id === next.id);
|
|
67
|
+
if (persisted === undefined || JSON.stringify(persisted) !== JSON.stringify(next))
|
|
68
|
+
continue;
|
|
69
|
+
const claim = next.interruptThen;
|
|
70
|
+
if (!matchesTarget() || claim !== undefined
|
|
71
|
+
&& (sessions?.activeAgentId !== claim.targetAgentId
|
|
72
|
+
|| nativeSessionId !== claim.targetNativeSessionId
|
|
73
|
+
|| binding.authority.epoch !== claim.targetAuthorityEpoch
|
|
74
|
+
|| binding.authority.holderId !== claim.targetAuthorityHolderId
|
|
75
|
+
|| binding.run?.attemptId !== claim.targetAttemptId
|
|
76
|
+
|| !["completed", "failed", "cancelled"].includes(binding.run.status))) {
|
|
77
|
+
store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(next, "native-session-changed", new Date()));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (next.deliveryTarget === undefined) {
|
|
81
|
+
next = { ...next, deliveryTarget: { agentId: sessions.activeAgentId, nativeSessionId } };
|
|
82
|
+
store.updateGlobalRoleMessage(next);
|
|
83
|
+
}
|
|
84
|
+
const result = await sendAgentHostRunControl({
|
|
85
|
+
home, scope: "global", roleName: role.name,
|
|
86
|
+
control: {
|
|
87
|
+
protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "submit-turn", nativeSessionId,
|
|
88
|
+
authority: { epoch: binding.authority.epoch, owner: "controller",
|
|
89
|
+
holderId: binding.authority.holderId },
|
|
90
|
+
run: { attemptId, boundedText: [
|
|
91
|
+
`Yui Global Message: role=${role.name} message=${next.id}.`,
|
|
92
|
+
"Read your Session Context and the referenced Message in full, then act on its durable input."
|
|
93
|
+
].join("\n") }
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
if (result.outcome === "rejected") {
|
|
97
|
+
store.transaction(tx => {
|
|
98
|
+
const latest = tx.listGlobalRoleMessages(role.name).find(message => message.id === next.id);
|
|
99
|
+
if (latest === undefined || latest.delivery !== undefined || latest.notDelivered !== undefined)
|
|
100
|
+
return;
|
|
101
|
+
const turn = tx.getGlobalRoleSessionSet(role.name)?.providerBinding?.run;
|
|
102
|
+
// A later canonical acceptance wins over transport diagnostics.
|
|
103
|
+
if (turn?.attemptId === attemptId && ["accepted", "completed", "failed", "cancelled"].includes(turn.status))
|
|
104
|
+
return;
|
|
105
|
+
const unknown = result.failure?.inputDisposition === "unknown" || result.snapshot.state === "delivery-unknown";
|
|
106
|
+
const updated = { ...latest, control: {
|
|
107
|
+
requestId: latest.inputControl?.requestId ?? latest.interruptThen.requestId,
|
|
108
|
+
receiptId: attemptId, outcome: unknown ? "delivery-unknown" : "rejected",
|
|
109
|
+
observedAt: new Date().toISOString()
|
|
110
|
+
} };
|
|
111
|
+
tx.updateGlobalRoleMessage(unknown ? updated : markGlobalRoleMessageNotDelivered(updated, result.failure?.detail ?? result.snapshot.detail ?? "provider-input-rejected", new Date()));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
onError(error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -253,10 +253,14 @@ function validateJobTarget(store, params) {
|
|
|
253
253
|
if (attempt === null) {
|
|
254
254
|
throw jobDomainError(`Integration Attempt not found: ${params.taskId}/${params.owner.integrationAttemptId}.`);
|
|
255
255
|
}
|
|
256
|
-
if (attempt.status
|
|
257
|
-
throw jobDomainError(`DurableJob owner Integration Attempt
|
|
256
|
+
if (attempt.status !== "running") {
|
|
257
|
+
throw jobDomainError(`DurableJob owner Integration Attempt must be running: `
|
|
258
258
|
+ `${params.taskId}/${params.owner.integrationAttemptId} is ${attempt.status}.`);
|
|
259
259
|
}
|
|
260
|
+
if (attempt.candidateCommit !== params.head || attempt.checkInputDigest === undefined
|
|
261
|
+
|| attempt.checkInputDigest !== durableJobIdempotencyKey(params)) {
|
|
262
|
+
throw jobDomainError("Integration Job has no matching admitted candidate/check specification.");
|
|
263
|
+
}
|
|
260
264
|
}
|
|
261
265
|
const project = store.getProject(params.projectId);
|
|
262
266
|
if (project === null) {
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
+
/** Archiving may retain execution; a Task lifecycle label is not exit proof. */
|
|
3
|
+
export function runtimeRoleProtectsDomain(role) {
|
|
4
|
+
return (role.ownerKind === "task-role"
|
|
5
|
+
&& (role.taskStatus === "active" || role.taskStatus === "archived"))
|
|
6
|
+
// Global Roles have no Task lifecycle; require a current native Session.
|
|
7
|
+
|| (role.ownerKind === "global-role" && role.nativeSessionId !== undefined);
|
|
8
|
+
}
|
|
2
9
|
export function createRuntimeResourceActivityTracker() {
|
|
3
10
|
const previous = new Map();
|
|
4
11
|
return (identity, resource) => {
|
|
@@ -112,8 +119,7 @@ export function buildControllerResourceInventory(facts) {
|
|
|
112
119
|
for (const process of paneProcesses)
|
|
113
120
|
claimed.add(process.pid);
|
|
114
121
|
const role = findRole(homeFact.roles, pane);
|
|
115
|
-
const terminalIsolation =
|
|
116
|
-
|| role?.taskStatus === "archived";
|
|
122
|
+
const terminalIsolation = role?.taskStatus === "cancelled" && role.taskRetirementIsolated === true;
|
|
117
123
|
resources.push(processResource({
|
|
118
124
|
kind: "agent-session",
|
|
119
125
|
state: pane.dead ? "dead" : role === undefined ? "orphaned" : "running",
|
|
@@ -122,7 +128,8 @@ export function buildControllerResourceInventory(facts) {
|
|
|
122
128
|
: terminalIsolation ? "safe" : "protected",
|
|
123
129
|
reasonCode: role === undefined
|
|
124
130
|
? pane.dead ? "dead-orphan-pane" : "orphan-pane"
|
|
125
|
-
: terminalIsolation ?
|
|
131
|
+
: terminalIsolation ? "retired-task-pane"
|
|
132
|
+
: role.taskStatus === "archived" ? "archived-task-pane" : "owned-role-pane",
|
|
126
133
|
yuiHome,
|
|
127
134
|
owner: role === undefined ? { kind: "none" } : roleOwner(role),
|
|
128
135
|
processes: paneProcesses,
|
|
@@ -358,7 +365,10 @@ function artifactResource(artifact, yuiHome, domain, homeId) {
|
|
|
358
365
|
};
|
|
359
366
|
}
|
|
360
367
|
function domainDisposition(base, reason, domain, target) {
|
|
361
|
-
|
|
368
|
+
// Even an expired test domain cannot authorize deleting retained execution.
|
|
369
|
+
if (reason === "archived-task-pane")
|
|
370
|
+
return "protected";
|
|
371
|
+
if (reason === "retired-task-pane") {
|
|
362
372
|
return "safe";
|
|
363
373
|
}
|
|
364
374
|
if (domain === undefined)
|
|
@@ -11,7 +11,7 @@ import { readControllerDiscovery } from "../core/controllerClient.js";
|
|
|
11
11
|
import { CONTROLLER_DISCOVERY_PATH } from "../core/protocol.js";
|
|
12
12
|
import { controllerSocketPath } from "../core/controllerEndpoint.js";
|
|
13
13
|
import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
|
|
14
|
-
import { buildControllerResourceInventory } from "./resourceInventory.js";
|
|
14
|
+
import { buildControllerResourceInventory, runtimeRoleProtectsDomain } from "./resourceInventory.js";
|
|
15
15
|
import { CONTROLLER_DOMAIN_PATH, EPHEMERAL_DOMAIN_GRACE_MS, ephemeralDomainFingerprint, readEphemeralDomainIdentity, readLinuxProcessStartIdentity } from "./domainIdentity.js";
|
|
16
16
|
const LINUX_PROCESS_SCAN_BATCH_SIZE = 64;
|
|
17
17
|
export const INVENTORY_EVENT_LOOP_RUN_BUDGET_MS = 25;
|
|
@@ -518,11 +518,7 @@ function inspectRuntimeDomain(home, processes, roles, storageStatus, now) {
|
|
|
518
518
|
graceMs: EPHEMERAL_DOMAIN_GRACE_MS
|
|
519
519
|
};
|
|
520
520
|
}
|
|
521
|
-
const activeRole = roles.some(
|
|
522
|
-
// Global Roles have no Task status. Only a live native Session is a
|
|
523
|
-
// durable liveness fact for that scope; stopped/broken history must not
|
|
524
|
-
// protect an expired disposable domain merely because it has an ID.
|
|
525
|
-
|| (role.ownerKind === "global-role" && role.nativeSessionId !== undefined)));
|
|
521
|
+
const activeRole = roles.some(runtimeRoleProtectsDomain);
|
|
526
522
|
if (!storageSafe) {
|
|
527
523
|
return {
|
|
528
524
|
kind: "ephemeral-test",
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveRuntimeHealth, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
2
|
import { isDeepStrictEqual } from "node:util";
|
|
4
3
|
import { controllerSocketPath } from "../core/controllerEndpoint.js";
|
|
5
4
|
import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
|
|
@@ -11,6 +10,7 @@ import { AgentConfigurationCatalogService, validateAgentLaunchConfiguration } fr
|
|
|
11
10
|
import { isTaskOwnedWorkspace, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
12
11
|
import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
|
|
13
12
|
import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
|
|
13
|
+
import { managedRuntimeRoot } from "../storage/homeLayout.js";
|
|
14
14
|
import { SqliteTaskStore } from "../storage/sqliteStore.js";
|
|
15
15
|
import { AsyncTaskStoreClient, resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
|
|
16
16
|
import { FileTaskWorkspacePreparer } from "../repository/taskWorkspacePreparer.js";
|
|
@@ -41,9 +41,12 @@ import { createResourceAutoGc } from "../resources/autoResourceGc.js";
|
|
|
41
41
|
import { createRuntimeResourceActivityTracker } from "./resourceInventory.js";
|
|
42
42
|
import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
|
|
43
43
|
import { launchBrokerForHome } from "../runtime/launchBroker.js";
|
|
44
|
+
import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
|
|
44
45
|
import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
|
|
45
46
|
import { replayRuntimeProcessExitOutbox } from "../runtime/processExitOutbox.js";
|
|
46
47
|
import { appendGlobalProcessExitObservation } from "../runtime/globalProcessExitStore.js";
|
|
48
|
+
import { deliverGlobalInputs } from "./globalInputDelivery.js";
|
|
49
|
+
import { createGlobalRoleMessage } from "../message/message.js";
|
|
47
50
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
48
51
|
import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
49
52
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
@@ -174,11 +177,14 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
174
177
|
?? new AgentHostPromptPushAdapter(home);
|
|
175
178
|
const runtimeIsolation = options.runtimeIsolation
|
|
176
179
|
?? new FileTaskRuntimeIsolation({
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
+
// Provider data/cache/tmp live in a dedicated Home partition. The
|
|
181
|
+
// control boundary permits runtime roots to overlap only this exact
|
|
182
|
+
// subtree, keeping them out of the database, managed Git workspaces and
|
|
183
|
+
// every other part of the control plane.
|
|
184
|
+
runtimeRoot: managedRuntimeRoot(home),
|
|
180
185
|
controlPlane: {
|
|
181
186
|
yuiHome: home,
|
|
187
|
+
managedRuntimeRoot: managedRuntimeRoot(home),
|
|
182
188
|
controllerSocketPath: controllerSocketPath(homeId),
|
|
183
189
|
tmuxNamespace: yuiTmuxServerName(home),
|
|
184
190
|
globalInstallPaths: [process.execPath]
|
|
@@ -239,6 +245,31 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
239
245
|
sessionHost,
|
|
240
246
|
promptPush,
|
|
241
247
|
launchCoordinator,
|
|
248
|
+
notifyOperatorInputOnce: async (input) => {
|
|
249
|
+
// The original attention mailbox remains pending until this one
|
|
250
|
+
// Global Message has an actual Provider receipt. Never inject keys
|
|
251
|
+
// into a busy Host console and mistake that for accepted attention.
|
|
252
|
+
const message = store.transaction(tx => {
|
|
253
|
+
const requestId = `operator-notice:${encodeURIComponent(input.receiptId)}`;
|
|
254
|
+
const previous = tx.listGlobalRoleMessages("operator").find(entry => (entry.inputControl ?? entry.interruptThen?.reusedInput)?.requestId === requestId);
|
|
255
|
+
if (previous !== undefined)
|
|
256
|
+
return previous;
|
|
257
|
+
const sessions = tx.getGlobalRoleSessionSet("operator");
|
|
258
|
+
const session = sessions?.sessions[sessions.activeAgentId];
|
|
259
|
+
if (session?.status !== "active" || sessions?.providerBinding == null)
|
|
260
|
+
return null;
|
|
261
|
+
const created = { ...createGlobalRoleMessage(tx.nextGlobalRoleMessageId(), "operator", input.text, "system", { type: "system" }, new Date(), { inputControl: { action: "queue", requestId } }),
|
|
262
|
+
deliveryTarget: { agentId: session.agentId, nativeSessionId: session.nativeSessionId } };
|
|
263
|
+
tx.saveGlobalRoleMessage(created);
|
|
264
|
+
return created;
|
|
265
|
+
});
|
|
266
|
+
if (message === null)
|
|
267
|
+
return "unavailable";
|
|
268
|
+
if (message.delivery?.via === "provider")
|
|
269
|
+
return "already-sent";
|
|
270
|
+
runningRuntime?.signal("global-role:operator");
|
|
271
|
+
return "not-ready";
|
|
272
|
+
},
|
|
242
273
|
roleResourceInventory: async (panes, inputs) => {
|
|
243
274
|
const inventory = await scanInventory(panes);
|
|
244
275
|
return inventory.resources.flatMap((resource) => {
|
|
@@ -440,6 +471,9 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
440
471
|
onError: options.onError,
|
|
441
472
|
lifecycleHost,
|
|
442
473
|
jobSupervisor,
|
|
474
|
+
globalInputDelivery: () => deliverGlobalInputs(home, store, async (roleName) => {
|
|
475
|
+
await lifecycleDispatcher("runtime.ensure-role-session", { scope: "global", roleName });
|
|
476
|
+
}, options.onError ?? (() => undefined)),
|
|
443
477
|
jobControl,
|
|
444
478
|
capabilityDispatcher: createCapabilityDispatcher(kernel.capabilities),
|
|
445
479
|
...(continuationReconciler === undefined ? {} : { continuationReconciler }),
|
|
@@ -546,6 +580,20 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
546
580
|
});
|
|
547
581
|
const lifecycleTails = new Map();
|
|
548
582
|
return async (method, params) => {
|
|
583
|
+
if (method === "runtime.host-observation-apply") {
|
|
584
|
+
const id = params.eventId;
|
|
585
|
+
if (typeof id !== "string")
|
|
586
|
+
throw applicationError("INVALID_PARAMS", "Host event id is required.");
|
|
587
|
+
// Eager delivery is only a hint about the exact immutable Inbox file.
|
|
588
|
+
// Missing means the normal drainer has already committed and ACKed it.
|
|
589
|
+
const event = new FileRuntimeEventInbox(store.rootDirectory()).read(id);
|
|
590
|
+
if (event === null)
|
|
591
|
+
return { outcome: "applied" };
|
|
592
|
+
if (event.type !== "runtime-observation" || event.host === undefined) {
|
|
593
|
+
throw applicationError("INVALID_PARAMS", "Expected an Agent Host observation.");
|
|
594
|
+
}
|
|
595
|
+
return { outcome: schedulerStore.observeAgentHostObservation(event, new Date()) };
|
|
596
|
+
}
|
|
549
597
|
if (method === "runtime.observation-apply") {
|
|
550
598
|
try {
|
|
551
599
|
return {
|
|
@@ -676,7 +724,34 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
676
724
|
}
|
|
677
725
|
// The launch payload is validated at reservation time and every member
|
|
678
726
|
// of its discriminated Provider-control union is JSON serializable.
|
|
679
|
-
|
|
727
|
+
const payload = launchBrokerForHome(store.rootDirectory()).redeem(ticket);
|
|
728
|
+
if (payload.executionEnvironment !== undefined) {
|
|
729
|
+
if (payload.environment.YUI_SESSION_SCOPE !== "task"
|
|
730
|
+
|| payload.environment.YUI_TASK_ID !== payload.executionEnvironment.taskId
|
|
731
|
+
|| payload.cwd !== payload.executionEnvironment.directory.path) {
|
|
732
|
+
throw applicationError("INVALID_PARAMS", "Launch does not match its adopted execution environment.");
|
|
733
|
+
}
|
|
734
|
+
assertExecutionEnvironmentCurrent(store, payload.executionEnvironment.taskId, payload.executionEnvironment);
|
|
735
|
+
}
|
|
736
|
+
return payload;
|
|
737
|
+
}
|
|
738
|
+
if (method === "runtime.execution-environment-check") {
|
|
739
|
+
const value = params;
|
|
740
|
+
const { taskId, roleName, agentId, nativeSessionId, workspace } = value;
|
|
741
|
+
if ([taskId, roleName, agentId, nativeSessionId, workspace].some(v => typeof v !== "string")) {
|
|
742
|
+
throw applicationError("INVALID_PARAMS", "Exact Host execution identity is required.");
|
|
743
|
+
}
|
|
744
|
+
const sessions = store.getTaskRoleSessionSet(taskId, roleName);
|
|
745
|
+
const session = sessions?.sessions[agentId];
|
|
746
|
+
if (sessions?.activeAgentId !== agentId || session?.status !== "active"
|
|
747
|
+
|| session.nativeSessionId !== nativeSessionId
|
|
748
|
+
|| (session.effective.executionEnvironment?.directory.path ?? session.effective.workspace.root) !== workspace) {
|
|
749
|
+
throw applicationError("INVALID_PARAMS", "Host no longer owns the current execution environment.");
|
|
750
|
+
}
|
|
751
|
+
if (session.effective.executionEnvironment !== undefined) {
|
|
752
|
+
assertExecutionEnvironmentCurrent(store, taskId, session.effective.executionEnvironment);
|
|
753
|
+
}
|
|
754
|
+
return { current: true };
|
|
680
755
|
}
|
|
681
756
|
if (method === "runtime.replace-agent-environment") {
|
|
682
757
|
if (environmentRefresher === undefined) {
|
|
@@ -1123,7 +1198,7 @@ function providerTurnControlParams(params) {
|
|
|
1123
1198
|
throw applicationError("INVALID_PARAMS", "Provider Turn control fence is invalid.");
|
|
1124
1199
|
}
|
|
1125
1200
|
return {
|
|
1126
|
-
taskId: requiredParam(value.taskId),
|
|
1201
|
+
...(value.taskId === undefined ? {} : { taskId: requiredParam(value.taskId) }),
|
|
1127
1202
|
roleName: requiredParam(value.roleName),
|
|
1128
1203
|
...(value.runId === undefined ? {} : { runId: requiredParam(value.runId) }),
|
|
1129
1204
|
agentId: requiredParam(value.agentId),
|
|
@@ -4,6 +4,7 @@ import { closeSync, constants, existsSync, fchmodSync, fsyncSync, linkSync, lsta
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { createRuntimeObservation, isRuntimeTokenEvidence } from "../runtime/runtimeObservation.js";
|
|
6
6
|
import { boundedRunFailureDiagnostic, transportAgentResult } from "../domain/agentResultTransport.js";
|
|
7
|
+
import { readAgentHostObservationSource } from "../runtime/agentHostProtocol.js";
|
|
7
8
|
export const MAX_RUNTIME_EVENT_FILE_BYTES = 16 * 1024 * 1024;
|
|
8
9
|
const RUNTIME_EVENT_DIRECTORY = join("runtime", "inbox");
|
|
9
10
|
const INVALID_RUNTIME_EVENT_DIRECTORY = join("runtime", "inbox-invalid");
|
|
@@ -24,7 +25,7 @@ export class FileRuntimeEventInbox {
|
|
|
24
25
|
this.hooks = hooks;
|
|
25
26
|
this.directory = join(home, RUNTIME_EVENT_DIRECTORY);
|
|
26
27
|
}
|
|
27
|
-
enqueueObservation(input) {
|
|
28
|
+
enqueueObservation(input, host) {
|
|
28
29
|
const observation = createRuntimeObservation(input);
|
|
29
30
|
const scope = observation.fence.taskId === undefined ? "global" : "task";
|
|
30
31
|
const event = Object.freeze({
|
|
@@ -36,7 +37,8 @@ export class FileRuntimeEventInbox {
|
|
|
36
37
|
...(observation.fence.taskId === undefined
|
|
37
38
|
? {}
|
|
38
39
|
: { taskId: observation.fence.taskId }),
|
|
39
|
-
observation
|
|
40
|
+
observation,
|
|
41
|
+
...(host === undefined ? {} : { host: readAgentHostObservationSource(host) })
|
|
40
42
|
});
|
|
41
43
|
return this.publish(event);
|
|
42
44
|
}
|
|
@@ -117,6 +119,20 @@ export class FileRuntimeEventInbox {
|
|
|
117
119
|
}
|
|
118
120
|
return event;
|
|
119
121
|
}
|
|
122
|
+
/** Producer-local delivery diagnostics need only existence, never a read of
|
|
123
|
+
* another version's payload or permission to quarantine another producer. */
|
|
124
|
+
has(id) {
|
|
125
|
+
assertEventId(id);
|
|
126
|
+
try {
|
|
127
|
+
lstatSync(this.eventPath(id));
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (isNodeError(error, "ENOENT"))
|
|
132
|
+
return false;
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
120
136
|
acknowledge(id) {
|
|
121
137
|
assertEventId(id);
|
|
122
138
|
try {
|
|
@@ -408,6 +424,7 @@ function parseRuntimeEvent(value) {
|
|
|
408
424
|
function parseRuntimeObservationEvent(value) {
|
|
409
425
|
const expected = [
|
|
410
426
|
"schemaVersion", "id", "type", "receivedAt", "scope", "observation",
|
|
427
|
+
...(value.host === undefined ? [] : ["host"]),
|
|
411
428
|
...(value.taskId === undefined ? [] : ["taskId"])
|
|
412
429
|
];
|
|
413
430
|
if (value.schemaVersion !== 1 || !hasExactKeys(value, expected))
|
|
@@ -426,9 +443,18 @@ function parseRuntimeObservationEvent(value) {
|
|
|
426
443
|
receivedAt: observation.receivedAt,
|
|
427
444
|
scope,
|
|
428
445
|
...(scope === "task" ? { taskId: observation.fence.taskId } : {}),
|
|
429
|
-
observation
|
|
446
|
+
observation,
|
|
447
|
+
...(value.host === undefined ? {} : { host: parseHostSource(value.host) })
|
|
430
448
|
});
|
|
431
449
|
}
|
|
450
|
+
function parseHostSource(value) {
|
|
451
|
+
try {
|
|
452
|
+
return readAgentHostObservationSource(value);
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
throw invalidEvent(error instanceof Error ? error.message : String(error));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
432
458
|
function parseDurableJobTerminalEvent(value) {
|
|
433
459
|
const expected = [
|
|
434
460
|
"schemaVersion", "id", "type", "receivedAt", "scope", "taskId",
|
|
@@ -520,6 +546,9 @@ function assertEventId(id) {
|
|
|
520
546
|
throw invalidEvent("Runtime event id is invalid.");
|
|
521
547
|
}
|
|
522
548
|
function hasSameIdentity(left, right) {
|
|
549
|
+
if (left.type === "runtime-observation" && right.type === "runtime-observation"
|
|
550
|
+
&& !isDeepStrictEqual(left.host, right.host))
|
|
551
|
+
return false;
|
|
523
552
|
if (left.type === "runtime-observation" && right.type === "runtime-observation"
|
|
524
553
|
&& ["turn.completed", "turn.failed", "turn.cancelled"].includes(left.observation.kind)) {
|
|
525
554
|
return left.id === right.id
|
|
@@ -143,11 +143,17 @@ export class FileRuntimeEventProcessor {
|
|
|
143
143
|
return { candidate, outcome };
|
|
144
144
|
}
|
|
145
145
|
applyRuntimeObservation(event, now) {
|
|
146
|
+
if (event.host !== undefined) {
|
|
147
|
+
return this.observer.observeAgentHostObservation?.(event, now) ?? "deferred";
|
|
148
|
+
}
|
|
146
149
|
const taskId = event.observation.fence.taskId;
|
|
147
150
|
if (taskId !== undefined) {
|
|
148
151
|
const task = this.observer.getTask(taskId);
|
|
152
|
+
if (task?.status === "archived") {
|
|
153
|
+
this.recordObsolete(event, "task-archived", now);
|
|
154
|
+
return "obsolete";
|
|
155
|
+
}
|
|
149
156
|
if (task === null
|
|
150
|
-
|| task.status === "archived"
|
|
151
157
|
|| (!["turn.completed", "turn.failed", "turn.cancelled"].includes(event.observation.kind)
|
|
152
158
|
&& (task.status !== "active" || task.executionGate.state !== "enabled")))
|
|
153
159
|
return "obsolete";
|
|
@@ -247,7 +253,8 @@ export class FileRuntimeEventProcessor {
|
|
|
247
253
|
agentId: fence.agentId,
|
|
248
254
|
...(fence.runId === undefined ? {} : { runId: fence.runId }),
|
|
249
255
|
nativeSessionId: fence.nativeSessionId,
|
|
250
|
-
reason
|
|
256
|
+
reason,
|
|
257
|
+
...(reason === "task-archived" ? { originalEvent: event } : {})
|
|
251
258
|
}, now);
|
|
252
259
|
return;
|
|
253
260
|
}
|
|
@@ -263,7 +270,8 @@ export class FileRuntimeEventProcessor {
|
|
|
263
270
|
agentId: event.agentId,
|
|
264
271
|
...(event.runId === undefined ? {} : { runId: event.runId }),
|
|
265
272
|
nativeSessionId: event.nativeSessionId,
|
|
266
|
-
reason
|
|
273
|
+
reason,
|
|
274
|
+
...(reason === "task-archived" ? { originalEvent: event } : {})
|
|
267
275
|
}, now);
|
|
268
276
|
}
|
|
269
277
|
acknowledge(ids, acknowledged) {
|
|
@@ -394,6 +402,9 @@ function progressStreamKey(event) {
|
|
|
394
402
|
fence.driverId,
|
|
395
403
|
fence.nativeSessionId ?? null,
|
|
396
404
|
fence.runId ?? null,
|
|
405
|
+
fence.receiptId ?? null,
|
|
406
|
+
fence.nativeTurnId ?? null,
|
|
407
|
+
event.host ?? null,
|
|
397
408
|
payload.activity,
|
|
398
409
|
isRuntimeTokenEvidence(event.observation) ? "usage" : "signal"
|
|
399
410
|
]);
|
|
@@ -510,6 +521,7 @@ export function createAsyncRuntimeObserver(invoke) {
|
|
|
510
521
|
const withNow = (method, input, now) => now === undefined ? call(method, [input]) : call(method, [input, now]);
|
|
511
522
|
return {
|
|
512
523
|
getTask: (taskId) => call("getTask", [taskId]),
|
|
524
|
+
observeAgentHostObservation: (input, now) => withNow("observeAgentHostObservation", input, now),
|
|
513
525
|
observeRuntimeObservation: (input, now) => withNow("observeRuntimeObservation", input, now),
|
|
514
526
|
observeRuntimeRunTerminal: (input, now) => withNow("observeRuntimeRunTerminal", input, now),
|
|
515
527
|
observeGlobalRuntimeRunTerminal: (input, now) => withNow("observeGlobalRuntimeRunTerminal", input, now),
|
|
@@ -605,11 +617,17 @@ export class AsyncRuntimeEventProcessor {
|
|
|
605
617
|
};
|
|
606
618
|
}
|
|
607
619
|
async applyRuntimeObservation(event, now) {
|
|
620
|
+
if (event.host !== undefined) {
|
|
621
|
+
return this.observer.observeAgentHostObservation?.(event, now) ?? "deferred";
|
|
622
|
+
}
|
|
608
623
|
const taskId = event.observation.fence.taskId;
|
|
609
624
|
if (taskId !== undefined) {
|
|
610
625
|
const task = await this.observer.getTask(taskId);
|
|
626
|
+
if (task?.status === "archived") {
|
|
627
|
+
await this.recordObsolete(event, "task-archived", now);
|
|
628
|
+
return "obsolete";
|
|
629
|
+
}
|
|
611
630
|
if (task === null
|
|
612
|
-
|| task.status === "archived"
|
|
613
631
|
|| (!["turn.completed", "turn.failed", "turn.cancelled"].includes(event.observation.kind)
|
|
614
632
|
&& (task.status !== "active" || task.executionGate.state !== "enabled")))
|
|
615
633
|
return "obsolete";
|
|
@@ -685,7 +703,8 @@ export class AsyncRuntimeEventProcessor {
|
|
|
685
703
|
agentId: fence.agentId,
|
|
686
704
|
...(fence.runId === undefined ? {} : { runId: fence.runId }),
|
|
687
705
|
nativeSessionId: fence.nativeSessionId,
|
|
688
|
-
reason
|
|
706
|
+
reason,
|
|
707
|
+
...(reason === "task-archived" ? { originalEvent: event } : {})
|
|
689
708
|
}, now);
|
|
690
709
|
return;
|
|
691
710
|
}
|
|
@@ -697,7 +716,8 @@ export class AsyncRuntimeEventProcessor {
|
|
|
697
716
|
agentId: event.agentId,
|
|
698
717
|
...(event.runId === undefined ? {} : { runId: event.runId }),
|
|
699
718
|
nativeSessionId: event.nativeSessionId,
|
|
700
|
-
reason
|
|
719
|
+
reason,
|
|
720
|
+
...(reason === "task-archived" ? { originalEvent: event } : {})
|
|
701
721
|
}, now);
|
|
702
722
|
}
|
|
703
723
|
acknowledge(ids, acknowledged) {
|