@zq-silk/yui 0.15.8 → 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/ARCHITECTURE.md +2 -0
- package/ARCHITECTURE.zh-CN.md +151 -0
- package/README.md +211 -14
- package/dist/agent/launchEnvironment.js +7 -0
- package/dist/artifacts/artifactCapability.js +74 -0
- package/dist/artifacts/artifactCommitLock.js +249 -0
- package/dist/artifacts/artifactPaths.js +151 -0
- package/dist/artifacts/gitArtifactRef.js +146 -0
- package/dist/artifacts/managedGit.js +332 -0
- package/dist/artifacts/taskArtifactRepository.js +277 -0
- package/dist/cli/commandCatalog.js +40 -16
- 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 +403 -93
- package/dist/commands/globalRoleCommands.js +314 -4
- package/dist/commands/operatorCommands.js +33 -2
- package/dist/commands/projectCommands.js +6 -7
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActivationCommands.js +22 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +846 -155
- 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/runContextPack.js +28 -16
- package/dist/context/taskContext.js +64 -5
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +11 -2
- 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/kernel/builtinCapabilities.js +32 -24
- package/dist/message/globalInterrupt.js +33 -0
- package/dist/message/inputControlResolution.js +106 -0
- package/dist/message/message.js +423 -0
- package/dist/message/messageContinuation.js +126 -3
- package/dist/message/taskInterrupt.js +34 -0
- package/dist/observability/orchestrationMetrics.js +1 -1
- package/dist/plugins/pluginService.js +11 -3
- 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/projectResource.js +0 -48
- package/dist/resources/projectResourceService.js +3 -81
- 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 +29 -16
- package/dist/storage/homeLayout.js +130 -0
- package/dist/storage/migrations/artifactsToGit.js +338 -0
- package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
- package/dist/storage/migrations/integrationContinuation.js +104 -0
- package/dist/storage/migrations/submitIntent.js +126 -0
- package/dist/storage/migrations/unifyHomeLayout.js +925 -0
- package/dist/storage/sqliteSchema.js +173 -7
- package/dist/storage/sqliteStore.js +41 -22
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -1
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +128 -0
- package/dist/task/nextAction.js +44 -11
- package/dist/task/taskActivation.js +26 -0
- package/dist/task/taskActivationService.js +85 -69
- package/dist/task/taskSubmission.js +236 -0
- package/dist/web/assets/client/app.js +58 -2
- 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 +202 -7
- 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 +135 -4
- package/dist/web/webSnapshot.js +4 -3
- package/dist/web/webTaskSurface.js +225 -8
- package/dist/workItem/workItem.js +14 -10
- package/dist/workspace/workItemChangeSetManager.js +18 -2
- package/docs/agent-result-consumption.md +2 -0
- package/docs/agent-result-consumption.zh-CN.md +81 -0
- package/docs/agent-runtime-drivers.md +2 -0
- package/docs/agent-runtime-drivers.zh-CN.md +77 -0
- package/docs/architecture/README.md +44 -32
- package/docs/architecture/README.zh-CN.md +43 -0
- package/docs/architecture/capabilities-and-resources.md +118 -79
- package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
- package/docs/managed-turn-and-session-runtime.md +2 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
- package/docs/observability/README.md +2 -0
- package/docs/observability/README.zh-CN.md +71 -0
- package/docs/plugin-sdk.md +320 -217
- package/docs/plugin-sdk.zh-CN.md +293 -0
- package/docs/provider-runtime.md +2 -0
- package/docs/provider-runtime.zh-CN.md +132 -0
- package/docs/release-workflow.md +41 -0
- package/docs/release-workflow.zh-CN.md +266 -0
- package/docs/roles-and-configuration.md +2 -0
- package/docs/roles-and-configuration.zh-CN.md +96 -0
- package/docs/sqlite-control-plane-design.md +225 -1
- package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
- package/docs/task-dag-semantics.md +80 -57
- package/docs/task-dag-semantics.zh-CN.md +59 -0
- package/docs/task-delivery.md +2 -0
- package/docs/task-delivery.zh-CN.md +82 -0
- package/docs/task-local-identity.md +2 -0
- package/docs/task-local-identity.zh-CN.md +58 -0
- package/docs/testing/verification-levels.md +26 -0
- package/docs/testing/verification-levels.zh-CN.md +80 -0
- package/i18n/README.zh-CN.md +199 -10
- package/package.json +2 -1
- package/skills/yui-leader/SKILL.md +88 -331
- package/skills/yui-leader/references/execution.md +405 -0
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-leader/references/planning.md +109 -0
- package/skills/yui-leader/references/task-plugins.md +8 -4
- package/skills/yui-operator/SKILL.md +22 -4
- package/skills/yui-runtime/SKILL.md +27 -0
- package/skills/yui-runtime/references/publication.md +20 -0
|
@@ -3,7 +3,7 @@ import { assertProjectActive, resolveProject } from "../repository/project.js";
|
|
|
3
3
|
import { workspaceProjectEntry } from "../worktree/managedWorkspace.js";
|
|
4
4
|
import { usageError } from "../errors/cliError.js";
|
|
5
5
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
6
|
-
import { createIntegrationAttempt, recordResolutionDecision, supersedeIntegration
|
|
6
|
+
import { createIntegrationAttempt, recordResolutionDecision, supersedeIntegration } from "../integration/integrationAttempt.js";
|
|
7
7
|
import { GitIntegrationService } from "../integration/gitIntegrationService.js";
|
|
8
8
|
import { FileTaskWorkspacePreparer } from "../repository/taskWorkspacePreparer.js";
|
|
9
9
|
import { runTaskIntegrationQueueCommand } from "./taskIntegrationQueueCommands.js";
|
|
@@ -171,6 +171,7 @@ async function continueIntegration(args, store, home, now, options) {
|
|
|
171
171
|
taskLocalActor(store, options.environment, integration.taskId);
|
|
172
172
|
if (integration.status !== "validating"
|
|
173
173
|
&& integration.status !== "running"
|
|
174
|
+
&& integration.status !== "conflicted"
|
|
174
175
|
&& (integration.status !== "blocked"
|
|
175
176
|
|| integration.resolution?.action !== "manual-resolution")) {
|
|
176
177
|
throw usageError(`Integration is not ready to continue: ${integration.id}/${integration.status}.`);
|
|
@@ -181,11 +182,13 @@ async function runIntegration(store, home, integration, now, options) {
|
|
|
181
182
|
const result = await new GitIntegrationService(home, store, undefined, now, options.environment, undefined, options.jobPort).integrate(integration.taskId, integration.id);
|
|
182
183
|
const output = result.status === "committed"
|
|
183
184
|
? `Integrated ${integrationSourceLabel(result.attempt)} into ${result.attempt.targetRef} with CAS (${result.attempt.id})\n`
|
|
184
|
-
: result.status === "
|
|
185
|
-
? `Integration ${result.attempt.id}
|
|
186
|
-
: result.status === "
|
|
187
|
-
? `Integration ${result.attempt.id}
|
|
188
|
-
:
|
|
185
|
+
: result.status === "conflicted"
|
|
186
|
+
? `Integration ${result.attempt.id} is conflicted in ${result.workspace.path}; resolve files, then run 'yui task integration continue ${result.attempt.taskId}/${result.attempt.id}' (no prior resolve required)\n`
|
|
187
|
+
: result.status === "blocked"
|
|
188
|
+
? `Integration ${result.attempt.id} needs Leader attention in ${result.workspace.path}: ${result.attempt.summary ?? result.attempt.conflict?.summary ?? "Inspect the current attempt"}\n`
|
|
189
|
+
: result.status === "checks-running"
|
|
190
|
+
? `Integration ${result.attempt.id} checks are running as DurableJob ${result.job.id}; run 'yui task integration continue ${result.attempt.taskId}/${result.attempt.id}' when the job finishes\n`
|
|
191
|
+
: `Integration ${result.attempt.id} failed; inspect its target and preserved evidence\n`;
|
|
189
192
|
return { output, data: result };
|
|
190
193
|
}
|
|
191
194
|
function resolveDecision(args, store, now, environment, home) {
|
|
@@ -223,41 +226,16 @@ async function abortIntegration(args, store, now, options, home) {
|
|
|
223
226
|
const integration = requireIntegration(store, parsed.positionals[0], options.environment);
|
|
224
227
|
requireActiveIntegrationTask(store, integration);
|
|
225
228
|
taskLocalActor(store, options.environment, integration.taskId);
|
|
226
|
-
if (integration.status !== "running" && integration.status !== "blocked") {
|
|
227
|
-
throw usageError(`Integration cannot be aborted from ${integration.status}: ${integration.id}.`);
|
|
228
|
-
}
|
|
229
229
|
const reason = parsed.one.get("--reason");
|
|
230
230
|
if (reason === undefined)
|
|
231
231
|
throw usageError(usage);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// target advanced while the Attempt is failed.
|
|
240
|
-
const current = tx.getIntegrationAttempt(integration.taskId, integration.id);
|
|
241
|
-
if (current === null) {
|
|
242
|
-
throw usageError(`Integration Attempt not found: ${integration.taskId}/${integration.id}.`);
|
|
243
|
-
}
|
|
244
|
-
if (current.status !== "running" && current.status !== "blocked") {
|
|
245
|
-
throw usageError(`Integration cannot be aborted from ${current.status}: ${current.id}.`);
|
|
246
|
-
}
|
|
247
|
-
taskLocalActor(tx, options.environment, current.taskId);
|
|
248
|
-
const aborted = updateIntegrationAttempt(current, {
|
|
249
|
-
status: "failed",
|
|
250
|
-
checks: [
|
|
251
|
-
...(current.checks ?? []),
|
|
252
|
-
{ name: "aborted", outcome: "failed", details: reason }
|
|
253
|
-
]
|
|
254
|
-
}, now);
|
|
255
|
-
tx.saveIntegrationAttempt(aborted.taskId, aborted);
|
|
256
|
-
return {
|
|
257
|
-
output: `Aborted Integration ${aborted.id}; start a new Integration Attempt to retry\n`,
|
|
258
|
-
data: { integration: aborted }
|
|
259
|
-
};
|
|
260
|
-
});
|
|
232
|
+
const settled = await new GitIntegrationService(home, store, undefined, () => now, options.environment, undefined, options.jobPort).abort(integration.taskId, integration.id, reason, (tx, taskId) => taskLocalActor(tx, options.environment, taskId));
|
|
233
|
+
return {
|
|
234
|
+
output: settled.status === "committed"
|
|
235
|
+
? `Integration ${settled.id} had already advanced its target; recorded committed, not aborted.\n`
|
|
236
|
+
: `Aborted Integration ${settled.id}; candidate, workspace and history are preserved. This is not Git abort or proof of Job quiescence. Choose an authorized replacement delivery path.\n`,
|
|
237
|
+
data: { integration: settled }
|
|
238
|
+
};
|
|
261
239
|
}
|
|
262
240
|
function supersedeIntegrationCommand(args, store, now, environment, home) {
|
|
263
241
|
const usage = "Task Integration supersede usage: yui task integration supersede <task>/<integration> --reason <text>.";
|
|
@@ -138,7 +138,7 @@ async function process(args, store, home, options) {
|
|
|
138
138
|
if (result.status === "committed") {
|
|
139
139
|
return `${entry.id} committed ${entry.changeSetId} -> ${entry.targetAfter ?? "-"}`;
|
|
140
140
|
}
|
|
141
|
-
if (result.status === "blocked") {
|
|
141
|
+
if (result.status === "blocked" || result.status === "conflicted") {
|
|
142
142
|
return `${entry.id} conflicted ${entry.changeSetId}: ${entry.conflictSummary ?? "blocked"}`;
|
|
143
143
|
}
|
|
144
144
|
return `${entry.id} conflicted ${entry.changeSetId}: ${entry.conflictSummary ?? "gate failure"}`;
|
|
@@ -50,7 +50,7 @@ export function createTaskRemoteDeliveryProof(store, task, currentCandidate = nu
|
|
|
50
50
|
delivery
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
-
export function assertTaskRemoteDeliveryProof(store, task, proof
|
|
53
|
+
export function assertTaskRemoteDeliveryProof(store, task, proof) {
|
|
54
54
|
if (proof === undefined || proof.schemaVersion !== 1) {
|
|
55
55
|
throw usageError(`Task remote-delivery preflight proof is required: ${task.id}.`);
|
|
56
56
|
}
|
|
@@ -60,16 +60,16 @@ export function assertTaskRemoteDeliveryProof(store, task, proof, options = {})
|
|
|
60
60
|
if (!isDeepStrictEqual(proof.publications, store.listPublicationReferences(task.id))) {
|
|
61
61
|
throw usageError(`Task Publication evidence changed after remote-delivery preflight: ${task.id}.`);
|
|
62
62
|
}
|
|
63
|
-
assertTaskRemoteDeliveryIntegrated(proof.delivery
|
|
63
|
+
assertTaskRemoteDeliveryIntegrated(proof.delivery);
|
|
64
64
|
return proof.delivery;
|
|
65
65
|
}
|
|
66
|
-
export function assertTaskRemoteDeliveryIntegrated(delivery
|
|
66
|
+
export function assertTaskRemoteDeliveryIntegrated(delivery) {
|
|
67
67
|
if (!delivery.allMerged) {
|
|
68
68
|
const headUnavailable = delivery.projects.filter((project) => (project.codeDelivery !== "none"
|
|
69
69
|
&& project.coverage === "head-unavailable"));
|
|
70
70
|
if (headUnavailable.length > 0 && delivery.source === "task-completed") {
|
|
71
71
|
throw usageError(`Task ${delivery.taskId} has no frozen completion heads for: ${headUnavailable.map(({ projectId }) => projectId).join(", ")}. Reopen and complete it again to record exact heads before archiving; `
|
|
72
|
-
+ "--force
|
|
72
|
+
+ "An explicitly authorized --force archive retains this evidence gap.");
|
|
73
73
|
}
|
|
74
74
|
const uncovered = delivery.projects
|
|
75
75
|
.filter((project) => project.codeDelivery !== "none" && !project.merged)
|
|
@@ -79,7 +79,7 @@ export function assertTaskRemoteDeliveryIntegrated(delivery, options = {}) {
|
|
|
79
79
|
throw usageError(`Task ${delivery.taskId} is not fully merged into remote delivery: ${uncovered || "expected Project heads are unavailable"}. Record confirmed current-head evidence with task publication upsert, `
|
|
80
80
|
+ "or use task archive --abandon for an intentional non-merge.");
|
|
81
81
|
}
|
|
82
|
-
if (delivery.allVerified
|
|
82
|
+
if (delivery.allVerified)
|
|
83
83
|
return;
|
|
84
84
|
const unverified = delivery.projects
|
|
85
85
|
.filter((project) => (project.codeDelivery !== "none"
|
|
@@ -91,7 +91,7 @@ export function assertTaskRemoteDeliveryIntegrated(delivery, options = {}) {
|
|
|
91
91
|
throw usageError(`Task ${delivery.taskId} has merged Publication evidence that is not verified: `
|
|
92
92
|
+ `${unverified || "verification evidence is unavailable"}. Run task publication verify `
|
|
93
93
|
+ "for each Publication, or repeat task archive --integrated --force to explicitly "
|
|
94
|
-
+ "
|
|
94
|
+
+ "archive despite evidence gaps, retaining the original delivery facts.");
|
|
95
95
|
}
|
|
96
96
|
export function renderTaskRemoteDelivery(delivery, indent = "") {
|
|
97
97
|
const source = `${delivery.source}${delivery.provisional ? " (provisional)" : ""}`;
|
|
@@ -11,6 +11,40 @@ import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
|
11
11
|
import { formatRunReceiptId } from "../task/taskRecordReference.js";
|
|
12
12
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
13
13
|
import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runtime/sessionTokenMetrics.js";
|
|
14
|
+
export function taskRoleHostDiagnostic(host) {
|
|
15
|
+
const snapshot = host.snapshot;
|
|
16
|
+
if (snapshot === undefined)
|
|
17
|
+
return `unavailable: ${host.detail ?? "Host could not be read"}`;
|
|
18
|
+
const delivery = snapshot.eventDelivery;
|
|
19
|
+
return [
|
|
20
|
+
`Provider Host=${snapshot.state}`,
|
|
21
|
+
delivery === undefined ? "event delivery=unreported (legacy Host)" :
|
|
22
|
+
`pending facts=${delivery.pending}; native terminals awaiting acknowledgement=${delivery.pendingTerminals}`,
|
|
23
|
+
delivery?.failure === undefined ? undefined :
|
|
24
|
+
`reporting failure (${delivery.failure.stage}, ${delivery.failure.observedAt}): ${delivery.failure.detail}`,
|
|
25
|
+
snapshot.detail
|
|
26
|
+
].filter(Boolean).join("; ");
|
|
27
|
+
}
|
|
28
|
+
/** Overlay live reporting evidence without rewriting Run/Provider/business truth. */
|
|
29
|
+
export function withTaskRoleHostObservation(status, host) {
|
|
30
|
+
if (host === undefined)
|
|
31
|
+
return status;
|
|
32
|
+
const snapshot = host.snapshot;
|
|
33
|
+
if (snapshot !== undefined && (snapshot.nativeSessionId !== status.nativeSession?.nativeSessionId
|
|
34
|
+
|| snapshot.adapterId !== status.nativeSession?.adapterId)) {
|
|
35
|
+
return { ...status, host: { detail: "Host does not match the recorded native Session." } };
|
|
36
|
+
}
|
|
37
|
+
const reportingNeedsAttention = snapshot !== undefined && (snapshot.state === "failed" || snapshot.state === "exited"
|
|
38
|
+
|| snapshot.eventDelivery?.failure !== undefined
|
|
39
|
+
|| (snapshot.eventDelivery?.pendingTerminals ?? 0) > 0);
|
|
40
|
+
return {
|
|
41
|
+
...status, host,
|
|
42
|
+
...(reportingNeedsAttention ? {
|
|
43
|
+
health: "needs-attention",
|
|
44
|
+
healthReason: `${taskRoleHostDiagnostic(host)}. Formal Run status and business acceptance are unchanged.`
|
|
45
|
+
} : {})
|
|
46
|
+
};
|
|
47
|
+
}
|
|
14
48
|
export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes, now = new Date()) {
|
|
15
49
|
const taskOpenInputRequestCount = store.listInputRequests(taskId)
|
|
16
50
|
.filter((request) => request.status === "open").length;
|
|
@@ -99,6 +133,7 @@ export function renderTaskRoleRuntimeStatus(status) {
|
|
|
99
133
|
: "none"}`,
|
|
100
134
|
` Native session ${nativeSession}`,
|
|
101
135
|
` AgentRuntime ${runtime}`,
|
|
136
|
+
...(status.host === undefined ? [] : [` Host reporting ${taskRoleHostDiagnostic(status.host)}`]),
|
|
102
137
|
` Session tokens ${sessionTokens}`,
|
|
103
138
|
` Runtime cleanup ${status.runtimeCleanupPending ? "pending" : "none"}`,
|
|
104
139
|
` tmux pane ${tmux}`,
|
|
@@ -3,7 +3,7 @@ import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publica
|
|
|
3
3
|
import { RUN_INPUT_MAX_DELTAS } from "./runInputContract.js";
|
|
4
4
|
import { assertWorkItemDependenciesCompleted } from "../workItem/dependencyGate.js";
|
|
5
5
|
import { governingWorkItemCandidate, workItemExecutionGroupById } from "../workItem/workItem.js";
|
|
6
|
-
import {
|
|
6
|
+
import { formatGitArtifactRef, validateGitArtifactRef } from "../artifacts/gitArtifactRef.js";
|
|
7
7
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
8
8
|
import { contextContentDigest, contextSnapshotRef, createContextSnapshot, validateContextSnapshot } from "./contextSnapshot.js";
|
|
9
9
|
import { MAX_SYNTHESIS_SOURCE_RUNS, sourceRunContextValue } from "./sourceRunContext.js";
|
|
@@ -99,14 +99,14 @@ export function freezeWorkItemExecutionAssignmentContextSnapshot(store, input, n
|
|
|
99
99
|
materialize("L2", "task", task.id, task),
|
|
100
100
|
materialize("L3", "work-item", workItem.id, workItem)
|
|
101
101
|
];
|
|
102
|
-
materialized.push(...candidateArtifacts(
|
|
102
|
+
materialized.push(...candidateArtifacts(task.id, workItem.candidates));
|
|
103
103
|
assertWorkItemDependenciesCompleted(store, workItem);
|
|
104
104
|
for (const dependencyId of workItem.dependsOn) {
|
|
105
105
|
const dependency = store.getWorkItem(task.id, dependencyId);
|
|
106
106
|
if (dependency === null)
|
|
107
107
|
throw new Error(`WorkItem dependency disappeared: ${dependencyId}.`);
|
|
108
108
|
materialized.push(materialize("L3", "accepted-work-item", dependency.id, dependency));
|
|
109
|
-
materialized.push(...candidateArtifacts(
|
|
109
|
+
materialized.push(...candidateArtifacts(task.id, dependency.candidates));
|
|
110
110
|
}
|
|
111
111
|
for (const binding of task.projectBindings) {
|
|
112
112
|
const project = store.getProject(binding.projectId);
|
|
@@ -371,7 +371,7 @@ function collectAuthorizedContext(store, run) {
|
|
|
371
371
|
if (item === null)
|
|
372
372
|
throw new Error(`AgentRun WorkItem not found: ${run.workItemId}.`);
|
|
373
373
|
result.push(materialize("L3", "work-item", item.id, item));
|
|
374
|
-
result.push(...candidateArtifacts(
|
|
374
|
+
result.push(...candidateArtifacts(task.id, item.candidates));
|
|
375
375
|
if (view === "worker") {
|
|
376
376
|
assertWorkItemDependenciesCompleted(store, item);
|
|
377
377
|
for (const dependencyId of item.dependsOn) {
|
|
@@ -379,7 +379,7 @@ function collectAuthorizedContext(store, run) {
|
|
|
379
379
|
if (dependency === null)
|
|
380
380
|
throw new Error(`WorkItem dependency disappeared: ${dependencyId}.`);
|
|
381
381
|
result.push(materialize("L3", "accepted-work-item", dependency.id, dependency));
|
|
382
|
-
result.push(...candidateArtifacts(
|
|
382
|
+
result.push(...candidateArtifacts(task.id, dependency.candidates));
|
|
383
383
|
}
|
|
384
384
|
}
|
|
385
385
|
}
|
|
@@ -394,7 +394,7 @@ function collectAuthorizedContext(store, run) {
|
|
|
394
394
|
if (candidate === undefined)
|
|
395
395
|
continue;
|
|
396
396
|
result.push(materialize("L3", "candidate", `${item.id}/${candidate.id}`, candidate));
|
|
397
|
-
result.push(...candidateArtifacts(
|
|
397
|
+
result.push(...candidateArtifacts(task.id, [candidate]));
|
|
398
398
|
}
|
|
399
399
|
}
|
|
400
400
|
if (round.deltaRecheck !== undefined) {
|
|
@@ -425,9 +425,12 @@ function collectAuthorizedContext(store, run) {
|
|
|
425
425
|
}
|
|
426
426
|
}
|
|
427
427
|
if (view === "leader") {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
428
|
+
// File artifacts are NOT enumerated into the synchronous context pack: the
|
|
429
|
+
// core cursor does not cover file edits, and an ambient directory listing
|
|
430
|
+
// here would be exactly the forbidden update-time mirror (§3.7). The Leader
|
|
431
|
+
// reads current artifacts on demand via the async `artifact.list` /
|
|
432
|
+
// `artifact.read` capabilities; frozen Candidate artifacts still enter the
|
|
433
|
+
// pack as commit-pinned pointers through candidateArtifacts().
|
|
431
434
|
for (const preparation of store.listEnvironmentPreparations(task.id)) {
|
|
432
435
|
result.push(materialize("L3", "environment-preparation", preparation.id, preparation));
|
|
433
436
|
}
|
|
@@ -476,16 +479,25 @@ function collectAuthorizedContext(store, run) {
|
|
|
476
479
|
return [...unique.values()].sort((left, right) => (contextRefIdentity(left.ref).localeCompare(contextRefIdentity(right.ref))));
|
|
477
480
|
}
|
|
478
481
|
/** Only artifacts attached to already-authorized candidates enter a bounded
|
|
479
|
-
* Worker/Reviewer snapshot.
|
|
482
|
+
* Worker/Reviewer snapshot. Each is a commit-pinned pointer: the commit
|
|
483
|
+
* self-certifies the frozen bytes, so the pointer is carried as pure data with
|
|
484
|
+
* NO build-time Git read. The bytes are resolved lazily on the async expand
|
|
485
|
+
* path (`artifact.read` at the pinned commit); the pointer survives cleanup of
|
|
480
486
|
* the producing workspace and Session. */
|
|
481
|
-
function candidateArtifacts(
|
|
487
|
+
function candidateArtifacts(taskId, candidates) {
|
|
482
488
|
return candidates.flatMap((candidate) => (candidate.artifactRefs ?? []).map((ref) => {
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
throw new Error(`Candidate Artifact is unavailable or drifted: ${ref.artifactId}.`);
|
|
489
|
+
const pinned = validateGitArtifactRef(ref);
|
|
490
|
+
if (pinned.taskId !== taskId) {
|
|
491
|
+
throw new Error(`Candidate Artifact belongs to another Task: ${pinned.taskId}.`);
|
|
487
492
|
}
|
|
488
|
-
|
|
493
|
+
// The refId is the self-certifying string form; the value is the pure
|
|
494
|
+
// pointer, never the bytes. inspectValue reconstructs the same pointer.
|
|
495
|
+
return materialize("L3", "artifact", formatGitArtifactRef(pinned), {
|
|
496
|
+
taskId: pinned.taskId,
|
|
497
|
+
commit: pinned.commit,
|
|
498
|
+
relativePath: pinned.relativePath,
|
|
499
|
+
...(pinned.digest === undefined ? {} : { digest: pinned.digest })
|
|
500
|
+
});
|
|
489
501
|
}));
|
|
490
502
|
}
|
|
491
503
|
/** Lane-specific context that may be layered over an immutable stage base. */
|
|
@@ -6,6 +6,7 @@ import { sourceRunContextValue } from "./sourceRunContext.js";
|
|
|
6
6
|
import { expandTaskMessageResult } from "../message/message.js";
|
|
7
7
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
8
8
|
import { runExecutionObservation } from "../agentRun/agentRun.js";
|
|
9
|
+
import { isGitArtifactRefString, parseGitArtifactRef } from "../artifacts/gitArtifactRef.js";
|
|
9
10
|
const MAX_RECORDS = 256;
|
|
10
11
|
const MAX_VALUE_BYTES = 4096;
|
|
11
12
|
const MAX_PAGE_BYTES = 128 * 1024;
|
|
@@ -226,13 +227,22 @@ function authorizeContext(store, taskId, environment) {
|
|
|
226
227
|
allow = new Set(pack.authority.readableRefs.map((ref) => `${ref.store}:${ref.refId}`));
|
|
227
228
|
allow.add(`role:${caller.roleName}`);
|
|
228
229
|
allow.add(`turn:${caller.currentRunId}`);
|
|
230
|
+
// A steer targets exactly one Role's current native Turn and is never shared
|
|
231
|
+
// Task intent (decision-3 §9). A Leader steer in particular stores as an
|
|
232
|
+
// untargeted `user` Message (no recipient/WorkItem/Run scope, since a Leader
|
|
233
|
+
// holds no Assignment), so recognize it explicitly: the shared block below
|
|
234
|
+
// must never leak it, and the steer-delta block authorizes it only to its own
|
|
235
|
+
// recipient. `interruptThen.reusedInput` is the interrupt-then composition
|
|
236
|
+
// whose carried input can itself be a steer.
|
|
237
|
+
const isSteerMessage = (message) => message.inputControl?.action === "steer"
|
|
238
|
+
|| message.interruptThen?.reusedInput?.action === "steer";
|
|
229
239
|
// A frozen Assignment is not a cutoff for the Task's current user intent.
|
|
230
240
|
// Untargeted human/Operator messages are shared Task requirements; scoped
|
|
231
|
-
// messages
|
|
241
|
+
// messages, other Roles' results, and steers still require an explicit grant.
|
|
232
242
|
const sharedMessageIds = new Set(store.listMessages(taskId)
|
|
233
243
|
.filter(message => (message.kind === "user" || message.kind === "operator")
|
|
234
244
|
&& message.recipient === undefined && message.workItemId === undefined
|
|
235
|
-
&& message.runId === undefined)
|
|
245
|
+
&& message.runId === undefined && !isSteerMessage(message))
|
|
236
246
|
.map(message => message.id));
|
|
237
247
|
for (const id of sharedMessageIds)
|
|
238
248
|
allow.add(`task-message:${id}`);
|
|
@@ -241,6 +251,33 @@ function authorizeContext(store, taskId, environment) {
|
|
|
241
251
|
allow.add(`task-event:${event.id}`);
|
|
242
252
|
}
|
|
243
253
|
}
|
|
254
|
+
// A steer targets this Role's exact current native Turn (decision-3 §9,
|
|
255
|
+
// message-5 gap E). Unlike a queued or addressed Message — which reaches the
|
|
256
|
+
// Role through a new AgentRun's frozen Context pack — a steer is pushed into
|
|
257
|
+
// the *current* turn and is never captured by any frozen snapshot. So the
|
|
258
|
+
// steer header directs the recipient to reconcile the input "through your
|
|
259
|
+
// authorized Context read path"; that path must therefore resolve the exact
|
|
260
|
+
// steer Message it names. Authorize the steers addressed to this caller's
|
|
261
|
+
// exact current Assignment scope as a live delta — the same way untargeted
|
|
262
|
+
// user intent is layered on — WITHOUT expanding the frozen Assignment's
|
|
263
|
+
// readableRefs. Scope is the tightest signal a steer carries (it never gains
|
|
264
|
+
// a continuation runId), so a steer for a different WorkItem/Round is not
|
|
265
|
+
// authorized, and only genuine steers gain this read (an ordinary addressed
|
|
266
|
+
// Message still requires its own frozen delta).
|
|
267
|
+
const run = store.getRun(taskId, caller.currentRunId);
|
|
268
|
+
const steerMessageIds = new Set(store.listMessages(taskId)
|
|
269
|
+
.filter(message => isSteerMessage(message)
|
|
270
|
+
&& message.recipient?.roleName === caller.roleName
|
|
271
|
+
&& message.recipient.workItemId === run?.workItemId
|
|
272
|
+
&& message.recipient.reviewRoundId === run?.reviewRoundId)
|
|
273
|
+
.map(message => message.id));
|
|
274
|
+
for (const id of steerMessageIds)
|
|
275
|
+
allow.add(`task-message:${id}`);
|
|
276
|
+
for (const event of store.listEvents(taskId)) {
|
|
277
|
+
if (event.type.startsWith("message.") && steerMessageIds.has(event.payload.messageId)) {
|
|
278
|
+
allow.add(`task-event:${event.id}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
244
281
|
}
|
|
245
282
|
return { task, allow, caller };
|
|
246
283
|
}
|
|
@@ -302,7 +339,26 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
|
|
|
302
339
|
return run === null ? null : sourceRunContextValue(run);
|
|
303
340
|
}
|
|
304
341
|
case "review-round": return store.getReviewRound(taskId, refId);
|
|
305
|
-
case "artifact":
|
|
342
|
+
case "artifact": {
|
|
343
|
+
// A file artifact is addressed by its self-certifying commit-pinned refId
|
|
344
|
+
// (git:<commit>:<relativePath>). Resolve it to the pure pointer only; the
|
|
345
|
+
// bytes are read on the async `artifact.read` path, never synchronously in
|
|
346
|
+
// this transaction.
|
|
347
|
+
if (!isGitArtifactRefString(refId))
|
|
348
|
+
return null;
|
|
349
|
+
try {
|
|
350
|
+
const pinned = parseGitArtifactRef(refId, taskId);
|
|
351
|
+
return {
|
|
352
|
+
taskId: pinned.taskId,
|
|
353
|
+
commit: pinned.commit,
|
|
354
|
+
relativePath: pinned.relativePath,
|
|
355
|
+
...(pinned.digest === undefined ? {} : { digest: pinned.digest })
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
306
362
|
case "environment-preparation": return store.getEnvironmentPreparation(taskId, refId);
|
|
307
363
|
case "change-set": return store.getChangeSet(taskId, refId);
|
|
308
364
|
case "managed-workspace": {
|
|
@@ -365,8 +421,11 @@ function authorizedEntries(store, taskId, environment) {
|
|
|
365
421
|
for (const candidate of [...item.candidates].reverse())
|
|
366
422
|
add("candidate", `${item.id}/${candidate.id}`, candidate);
|
|
367
423
|
}
|
|
368
|
-
|
|
369
|
-
|
|
424
|
+
// File artifacts are not enumerated into the synchronous Context listing: the
|
|
425
|
+
// core cursor does not cover file edits and an ambient directory index here
|
|
426
|
+
// would be the forbidden update-time mirror (§3.7). Current artifacts are
|
|
427
|
+
// listed on demand via the async `artifact.list` capability; frozen Candidate
|
|
428
|
+
// artifacts remain reachable as commit-pinned pointers under their Candidate.
|
|
370
429
|
for (const preparation of store.listEnvironmentPreparations(taskId))
|
|
371
430
|
add("environment-preparation", preparation.id, preparation);
|
|
372
431
|
for (const workspace of store.listManagedWorkspaces(taskId))
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
2
|
+
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
3
|
+
import { createRuntimeObservation } from "../runtime/runtimeObservation.js";
|
|
4
|
+
import { readAgentHostObservationSource } from "../runtime/agentHostProtocol.js";
|
|
5
|
+
import { resolveRuntimeHookRunFence, RuntimeHookRunFenceError } from "./runtimeHookRunFence.js";
|
|
6
|
+
export class AgentHostObservationDeferred extends Error {
|
|
7
|
+
}
|
|
8
|
+
/** Resolve wire facts only here, using the Controller's current authoritative
|
|
9
|
+
* store. The producer's launch Run is never a substitute for input identity. */
|
|
10
|
+
export function resolveAgentHostObservation(store, event) {
|
|
11
|
+
const host = readAgentHostObservationSource(event.host);
|
|
12
|
+
const input = createRuntimeObservation(event.observation);
|
|
13
|
+
const { fence, kind } = input;
|
|
14
|
+
const hostKinds = [
|
|
15
|
+
"host.observed", "session.started", "session.ready", "session.ended", "session.failed",
|
|
16
|
+
"conversation.observed", "goal.updated", "goal.cleared",
|
|
17
|
+
"turn.accepted", "turn.completed", "turn.failed", "turn.cancelled",
|
|
18
|
+
"input.accepted", "input.rejected", "input.delivery-unknown",
|
|
19
|
+
"operation.started", "operation.completed", "operation.failed", "activity.observed"
|
|
20
|
+
];
|
|
21
|
+
if (!hostKinds.includes(kind)
|
|
22
|
+
|| (kind === "host.observed" ? input.authority !== "host"
|
|
23
|
+
: input.authority !== "provider-structured" && !(kind === "turn.accepted" && input.authority === "transport"))) {
|
|
24
|
+
throw new RuntimeHookRunFenceError("Agent Host fact exceeds its event authority.");
|
|
25
|
+
}
|
|
26
|
+
let driver;
|
|
27
|
+
try {
|
|
28
|
+
driver = builtinAgentDriverRegistry().requireByAdapterId(host.adapterId);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new RuntimeHookRunFenceError("Agent Host adapter is unsupported.");
|
|
32
|
+
}
|
|
33
|
+
if (event.taskId !== fence.taskId
|
|
34
|
+
|| event.scope !== (fence.taskId === undefined ? "global" : "task") || fence.runId !== undefined
|
|
35
|
+
|| fence.nativeSessionId === undefined
|
|
36
|
+
|| driver.id !== fence.driverId
|
|
37
|
+
|| (host.connection !== undefined && kind !== "host.observed")) {
|
|
38
|
+
throw new RuntimeHookRunFenceError("Agent Host fact source does not match its observation.");
|
|
39
|
+
}
|
|
40
|
+
const sessionStartup = ["session.started", "session.ready", "host.observed"].includes(kind)
|
|
41
|
+
|| (kind === "conversation.observed" && host.startupRunId !== undefined);
|
|
42
|
+
const sessionFact = sessionStartup
|
|
43
|
+
|| ["conversation.observed", "goal.updated", "goal.cleared"].includes(kind);
|
|
44
|
+
const directStart = kind === "turn.accepted" && fence.receiptId === `direct:${fence.nativeTurnId}`;
|
|
45
|
+
const additionalInput = kind.startsWith("input.") && (fence.receiptId?.startsWith("native-input:") || fence.receiptId?.startsWith("turn-input:")
|
|
46
|
+
|| fence.receiptId?.startsWith("steer:"));
|
|
47
|
+
const terminal = ["turn.completed", "turn.failed", "turn.cancelled", "session.ended", "session.failed"].includes(kind);
|
|
48
|
+
if (fence.taskId === undefined) {
|
|
49
|
+
if (host.startupRunId !== undefined)
|
|
50
|
+
throw new RuntimeHookRunFenceError("Global Host cannot carry a startup Run.");
|
|
51
|
+
const sessions = store.getGlobalRoleSessionSet(fence.roleName);
|
|
52
|
+
const session = sessions?.sessions[fence.agentId];
|
|
53
|
+
if (sessionStartup && (session === undefined || session.status === "ended")) {
|
|
54
|
+
const role = store.getGlobalRole(fence.roleName);
|
|
55
|
+
if (role?.activeAgentId === fence.agentId && role.workspace === host.workspace) {
|
|
56
|
+
throw new AgentHostObservationDeferred("Awaiting exact Global Session adoption.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const resolved = resolveRuntimeHookRunFence({
|
|
60
|
+
YUI_SESSION_SCOPE: "global", YUI_ROLE: fence.roleName, YUI_AGENT_ID: fence.agentId,
|
|
61
|
+
YUI_ADAPTER_ID: host.adapterId, YUI_WORKSPACE: host.workspace
|
|
62
|
+
}, host.adapterId, fence.nativeSessionId, {
|
|
63
|
+
...(sessionFact || directStart ? { sessionOnly: true } : {
|
|
64
|
+
exactInput: true,
|
|
65
|
+
...(fence.nativeTurnId === undefined ? {} : { nativeTurnId: fence.nativeTurnId }),
|
|
66
|
+
...(fence.receiptId === undefined || additionalInput ? {} : { attemptId: fence.receiptId })
|
|
67
|
+
})
|
|
68
|
+
}, store);
|
|
69
|
+
return createRuntimeObservation({
|
|
70
|
+
...input, fence: { ...fence,
|
|
71
|
+
...(fence.receiptId === undefined && resolved.receiptId !== undefined
|
|
72
|
+
? { receiptId: resolved.receiptId } : {}) }
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const sessions = store.getTaskRoleSessionSet(fence.taskId, fence.roleName);
|
|
76
|
+
const session = sessions?.sessions[fence.agentId];
|
|
77
|
+
if (kind === "host.observed" && host.startupRunId === undefined
|
|
78
|
+
&& (session === undefined || session.status === "ended")) {
|
|
79
|
+
const role = store.getRole(fence.taskId, fence.roleName);
|
|
80
|
+
const task = store.getTask(fence.taskId);
|
|
81
|
+
if (role?.activeAgentId === fence.agentId && role.workspace === host.workspace
|
|
82
|
+
&& task !== null && ["active", "draft"].includes(task.status)) {
|
|
83
|
+
// A runless Session launch is adopted by the existing launch coordinator
|
|
84
|
+
// after its Host returns the native id. Retain custody until that commit;
|
|
85
|
+
// it must not itself authorize or invent the Session.
|
|
86
|
+
throw new AgentHostObservationDeferred("Awaiting exact Session adoption.");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const knownStartup = sessionStartup && store.listEvents(fence.taskId).some(e => e.type === "runtime.observation" && e.payload.eventId === input.eventId);
|
|
90
|
+
const startup = sessionStartup && host.startupRunId !== undefined && !knownStartup;
|
|
91
|
+
const options = {
|
|
92
|
+
...(fence.nativeTurnId === undefined || sessionFact || directStart ? {} : { nativeTurnId: fence.nativeTurnId }),
|
|
93
|
+
...(fence.receiptId === undefined || additionalInput || sessionFact || directStart
|
|
94
|
+
? {} : { attemptId: fence.receiptId }),
|
|
95
|
+
...(terminal || additionalInput ? { terminal: true } : {}),
|
|
96
|
+
...((sessionFact && !startup) || directStart || (terminal && fence.receiptId === undefined) ? { sessionOnly: true } : {}),
|
|
97
|
+
...(!sessionFact && !directStart ? { exactInput: true } : {}),
|
|
98
|
+
...(startup ? {
|
|
99
|
+
startupRunId: host.startupRunId,
|
|
100
|
+
startupSession: driver.capabilities.observation.sessionBootstrap
|
|
101
|
+
} : {})
|
|
102
|
+
};
|
|
103
|
+
// Idempotent replay of an already applied startup must not be rejected just
|
|
104
|
+
// because its Run has since ended. All raw fields still match the saved fact.
|
|
105
|
+
const resolved = resolveRuntimeHookRunFence({
|
|
106
|
+
YUI_SESSION_SCOPE: "task", YUI_TASK_ID: fence.taskId,
|
|
107
|
+
YUI_ROLE: fence.roleName, YUI_AGENT_ID: fence.agentId,
|
|
108
|
+
YUI_ADAPTER_ID: host.adapterId, YUI_WORKSPACE: host.workspace
|
|
109
|
+
}, host.adapterId, fence.nativeSessionId, options, store);
|
|
110
|
+
return createRuntimeObservation({
|
|
111
|
+
...input,
|
|
112
|
+
fence: {
|
|
113
|
+
...fence,
|
|
114
|
+
...(resolved.runId === undefined ? {} : { runId: resolved.runId }),
|
|
115
|
+
...(fence.receiptId === undefined && resolved.receiptId !== undefined
|
|
116
|
+
? { receiptId: resolved.receiptId } : {})
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/** Custody and account evidence share the same exact Session validation and
|
|
121
|
+
* commit/ACK boundary as activity. They contain locations, never credentials. */
|
|
122
|
+
export function recordAgentHostConnection(store, event, input, now) {
|
|
123
|
+
const host = readAgentHostObservationSource(event.host);
|
|
124
|
+
const connection = host.connection;
|
|
125
|
+
if (connection === undefined)
|
|
126
|
+
throw new RuntimeHookRunFenceError("Host connection evidence is missing.");
|
|
127
|
+
const fence = input.fence;
|
|
128
|
+
const owner = connection.processOwner;
|
|
129
|
+
if (owner !== undefined) {
|
|
130
|
+
if (owner.owner.scope !== (fence.taskId === undefined ? "global" : "task")
|
|
131
|
+
|| (owner.owner.scope === "task" && owner.owner.taskId !== fence.taskId)
|
|
132
|
+
|| owner.owner.roleName !== fence.roleName || owner.agentId !== fence.agentId
|
|
133
|
+
|| owner.adapterId !== host.adapterId || owner.nativeSessionId !== fence.nativeSessionId
|
|
134
|
+
|| owner.providerRoot.attribution !== "owned-child") {
|
|
135
|
+
throw new RuntimeHookRunFenceError("Host process custody does not match its exact Session.");
|
|
136
|
+
}
|
|
137
|
+
store.saveSessionOwner(owner);
|
|
138
|
+
}
|
|
139
|
+
if (connection.account !== undefined) {
|
|
140
|
+
if (host.adapterId !== "codex")
|
|
141
|
+
throw new RuntimeHookRunFenceError("Native account evidence requires Codex.");
|
|
142
|
+
// Global Roles have no Task event stream. Process custody above is still
|
|
143
|
+
// recorded under the exact Global owner; Task account history stays Task-scoped.
|
|
144
|
+
if (fence.taskId === undefined)
|
|
145
|
+
return;
|
|
146
|
+
const exists = store.listEvents(fence.taskId).some(e => e.type === "runtime.native-connection-bound"
|
|
147
|
+
&& e.payload.roleName === fence.roleName && e.payload.agentId === fence.agentId
|
|
148
|
+
&& e.payload.nativeSessionId === fence.nativeSessionId);
|
|
149
|
+
if (!exists)
|
|
150
|
+
store.saveEvent(fence.taskId, createTaskEvent(store.nextEventId(fence.taskId), fence.taskId, "runtime.native-connection-bound", {
|
|
151
|
+
roleName: fence.roleName, agentId: fence.agentId, nativeSessionId: fence.nativeSessionId,
|
|
152
|
+
...connection.account
|
|
153
|
+
}, now));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { findLiveControllerProcessForHome } from "../core/controllerProcessIdentity.js";
|
|
3
|
+
import { findLiveControllerProcessForHome, inspectLiveControllerProcess } from "../core/controllerProcessIdentity.js";
|
|
4
4
|
import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
|
|
5
5
|
import { callController, ControllerClientError, readControllerDiscovery, stopOrphanedFileTaskController } from "../core/controllerClient.js";
|
|
6
6
|
import { FILE_TASK_CONTROLLER_PROTOCOL_VERSION } from "../core/protocol.js";
|
|
@@ -185,6 +185,9 @@ export async function stopFileTaskController(home, options = {}) {
|
|
|
185
185
|
...(expectedPid === undefined ? {} : { expectedPid }) };
|
|
186
186
|
try {
|
|
187
187
|
const result = await stopControllerGracefully(home, bounded);
|
|
188
|
+
if (result.stopped && physical !== undefined) {
|
|
189
|
+
await waitForControllerProcessExit(home, physical, options);
|
|
190
|
+
}
|
|
188
191
|
if (result.stopped || options.call !== undefined)
|
|
189
192
|
return result;
|
|
190
193
|
}
|
|
@@ -196,6 +199,18 @@ export async function stopFileTaskController(home, options = {}) {
|
|
|
196
199
|
...(physical === undefined ? {} : { expectedProcessStartIdentity: physical.processStartIdentity }) });
|
|
197
200
|
return stopped === undefined ? { stopped: false, alreadyStopped: true } : { stopped: true, pid: stopped.pid };
|
|
198
201
|
}
|
|
202
|
+
async function waitForControllerProcessExit(home, controller, options) {
|
|
203
|
+
const timeoutMs = positive(options.shutdownTimeoutMs, CONTROLLER_SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
|
|
204
|
+
const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
|
|
205
|
+
const homeFilesystemId = readHomeFilesystemId(home);
|
|
206
|
+
const deadline = Date.now() + timeoutMs;
|
|
207
|
+
while (inspectLiveControllerProcess(controller.pid, homeFilesystemId, controller.processStartIdentity) !== undefined) {
|
|
208
|
+
if (Date.now() >= deadline) {
|
|
209
|
+
throw new ControllerClientError("CONTROLLER_TIMEOUT", `Controller process ${controller.pid} did not exit within ${timeoutMs} ms.`);
|
|
210
|
+
}
|
|
211
|
+
await delay(pollMs);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
199
214
|
async function stopControllerGracefully(home, options) {
|
|
200
215
|
const call = options.call ?? ((h, method, params) => callController(h, method, params, {
|
|
201
216
|
timeoutMs: options.requestTimeoutMs
|
|
@@ -558,7 +573,7 @@ export class FileTaskWorkflowRuntime {
|
|
|
558
573
|
&& entry.archiveBlocked));
|
|
559
574
|
if (blockers.length === 0)
|
|
560
575
|
return;
|
|
561
|
-
throw new WorkspaceCleanupBlockedError("physical-resource-live", `task:${taskId}`, true, `Task
|
|
576
|
+
throw new WorkspaceCleanupBlockedError("physical-resource-live", `task:${taskId}`, true, `Task workspace cleanup blocked: ${blockers.length} owned physical resource(s) live or unverified: `
|
|
562
577
|
+ blockers.map((entry) => (`${entry.owner.roleName}/${entry.nativeSessionId ?? "unknown-session"}`
|
|
563
578
|
+ ` (pid ${entry.physical?.alive === true ? entry.physical.pid : "?"})`)).join("; "));
|
|
564
579
|
}
|