@zq-silk/yui 0.15.9 → 0.15.12
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 +8 -4
- package/ARCHITECTURE.zh-CN.md +5 -2
- package/README.md +13 -5
- package/dist/agent/launchEnvironment.js +7 -0
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/cli/commandCatalog.js +64 -16
- package/dist/cli/interactionPolicy.js +7 -3
- package/dist/cli/managedDiagnostics.js +1 -1
- 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 +381 -107
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +339 -4
- package/dist/commands/projectCommands.js +50 -22
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +586 -96
- package/dist/commands/taskIntegrationCommands.js +19 -39
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskOverviewCommand.js +4 -3
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +11 -2
- package/dist/commands/taskPublicationVerifyCommand.js +23 -39
- package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
- package/dist/commands/taskRoleRuntimeStatus.js +35 -0
- package/dist/context/runContextPack.js +3 -0
- package/dist/context/taskCatalog.js +187 -0
- package/dist/context/taskContext.js +55 -6
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +14 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
- package/dist/controller/globalInputDelivery.js +132 -0
- package/dist/controller/jobControl.js +6 -2
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/resourceInventory.js +14 -4
- package/dist/controller/resourceInventoryLinux.js +2 -6
- package/dist/controller/runtime.js +117 -7
- 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 +329 -386
- package/dist/integration/integrationAttempt.js +30 -4
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/integration/integrationSourceApplication.js +323 -0
- package/dist/lifecycle/exactRunTerminalization.js +4 -1
- package/dist/message/globalInterrupt.js +33 -0
- package/dist/message/globalProviderRetry.js +15 -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/executionAudit.js +19 -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 +430 -107
- package/dist/repository/projectMaintenanceLock.js +75 -18
- package/dist/repository/taskWorkspaceCoordinator.js +182 -101
- package/dist/repository/taskWorkspacePreparer.js +205 -72
- package/dist/repository/workItemCandidateSnapshot.js +34 -0
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/resources/resourceDiscovery.js +3 -2
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentHost.js +179 -82
- package/dist/runtime/agentHostCompatibility.js +127 -0
- package/dist/runtime/agentHostProtocol.js +53 -0
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/executionEnvironment.js +0 -19
- package/dist/runtime/launchBroker.js +6 -0
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +6 -2
- package/dist/runtime/taskRuntimeIsolation.js +30 -6
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/runtime/tmuxAdapters.js +5 -3
- package/dist/scheduler/activeRoleRunDelivery.js +12 -0
- package/dist/scheduler/leaderWakeupProcessor.js +5 -0
- package/dist/scheduler/operatorEvent.js +4 -0
- package/dist/scheduler/taskExecutionProjection.js +38 -6
- package/dist/scheduler/taskObservabilityProjection.js +6 -44
- 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 +167 -4
- package/dist/storage/sqliteStore.js +57 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +2 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +129 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/nextAction.js +44 -11
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/web/assets/client/app.js +147 -17
- package/dist/web/assets/client/components.js +56 -13
- package/dist/web/assets/client/i18n.js +78 -4
- package/dist/web/assets/client/taskSurface.js +108 -1
- package/dist/web/assets/client/view.js +39 -8
- package/dist/web/assets/shell.js +29 -0
- package/dist/web/assets/styles/layout.js +8 -1
- package/dist/web/assets/styles/widgets.js +12 -0
- package/dist/web/webServer.js +131 -4
- package/dist/web/webSnapshot.js +16 -6
- package/dist/web/webTaskSurface.js +222 -5
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +111 -35
- package/docs/agent-result-consumption.md +4 -0
- package/docs/agent-result-consumption.zh-CN.md +3 -0
- package/docs/agent-runtime-drivers.md +7 -0
- package/docs/agent-runtime-drivers.zh-CN.md +5 -0
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/README.zh-CN.md +3 -1
- package/docs/architecture/capabilities-and-resources.md +30 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
- package/docs/managed-turn-and-session-runtime.md +47 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/project-refresh.md +77 -0
- package/docs/project-refresh.zh-CN.md +59 -0
- package/docs/provider-retry.md +70 -0
- 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/task-delivery.md +133 -13
- package/docs/task-delivery.zh-CN.md +99 -10
- package/docs/task-discovery.md +102 -0
- package/docs/task-discovery.zh-CN.md +86 -0
- package/docs/testing/verification-levels.md +40 -0
- package/docs/testing/verification-levels.zh-CN.md +23 -0
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +154 -51
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-operator/SKILL.md +19 -3
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +42 -0
- package/skills/yui-runtime/references/publication.md +42 -0
- package/skills/yui-runtime/references/recovery.md +24 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { usageError } from "../errors/cliError.js";
|
|
2
|
+
import { contextContentDigest } from "./contextSnapshot.js";
|
|
3
|
+
import { materialize, resolveContextReader } from "./taskContext.js";
|
|
4
|
+
import { CATALOG_ATTENTION } from "../storage/taskCatalog.js";
|
|
5
|
+
const TASK_STATUSES = ["draft", "active", "completed", "cancelled", "archived"];
|
|
6
|
+
const PAGE_BYTES = 32 * 1024;
|
|
7
|
+
const SUMMARY_BYTES = 512;
|
|
8
|
+
const MAX_LIMIT = 100;
|
|
9
|
+
export function parseTaskCatalogOptions(args) {
|
|
10
|
+
const values = new Map();
|
|
11
|
+
let all = false;
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const key = args[i];
|
|
14
|
+
if (key === "--all" && !all) {
|
|
15
|
+
all = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (!["--view", "--limit", "--cursor", "--status", "--project", "--search", "--attention"].includes(key)
|
|
19
|
+
|| values.has(key) || args[i + 1] === undefined || args[i + 1].startsWith("--")) {
|
|
20
|
+
throw usageError("Compact Task list expects --view compact [--all] [--status <status>] [--project <id>] [--search <text>] [--attention <category>] [--limit <1..100>] [--cursor <cursor>].");
|
|
21
|
+
}
|
|
22
|
+
values.set(key, args[++i]);
|
|
23
|
+
}
|
|
24
|
+
if (values.get("--view") !== "compact")
|
|
25
|
+
throw usageError("Task list view must be compact.");
|
|
26
|
+
const status = values.get("--status");
|
|
27
|
+
if (status !== undefined && !TASK_STATUSES.includes(status)) {
|
|
28
|
+
throw usageError("Unknown Task catalog status.");
|
|
29
|
+
}
|
|
30
|
+
const attention = values.get("--attention");
|
|
31
|
+
if (attention !== undefined && !CATALOG_ATTENTION.includes(attention)) {
|
|
32
|
+
throw usageError(`Task catalog attention must be one of: ${CATALOG_ATTENTION.join(", ")}.`);
|
|
33
|
+
}
|
|
34
|
+
const limit = values.has("--limit") ? Number(values.get("--limit")) : 20;
|
|
35
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_LIMIT)
|
|
36
|
+
throw usageError("Task catalog limit must be between 1 and 100.");
|
|
37
|
+
const search = values.get("--search")?.trim();
|
|
38
|
+
const project = values.get("--project")?.trim();
|
|
39
|
+
if ((search?.length ?? 0) > 256 || (project?.length ?? 0) > 256 || project === "") {
|
|
40
|
+
throw usageError("Task catalog filters exceed their text limit.");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
all: all || status === "archived", limit,
|
|
44
|
+
...(status === undefined ? {} : { status: status }),
|
|
45
|
+
...(attention === undefined ? {} : { attention: attention }),
|
|
46
|
+
...(search ? { search } : {}),
|
|
47
|
+
...(project === undefined ? {} : { project }),
|
|
48
|
+
...(values.has("--cursor") ? { cursor: values.get("--cursor") } : {})
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Task-local Leaders discover only their Task. Assignment Roles use their
|
|
52
|
+
* existing Context pack: whole-Task counts would leak unrelated assignments. */
|
|
53
|
+
export function taskCatalogScope(store, environment) {
|
|
54
|
+
const caller = resolveContextReader(store, environment);
|
|
55
|
+
if (caller !== undefined && caller.roleName !== "leader") {
|
|
56
|
+
throw usageError("Task discovery is unavailable to an Assignment-scoped Role; use its authorized Run Context.");
|
|
57
|
+
}
|
|
58
|
+
return caller?.taskId;
|
|
59
|
+
}
|
|
60
|
+
export function readTaskCatalog(store, options, environment = {}) {
|
|
61
|
+
return store.transaction(reader => {
|
|
62
|
+
const taskId = taskCatalogScope(reader, environment);
|
|
63
|
+
const { cursor: rawCursor, ...filters } = options;
|
|
64
|
+
if (!Number.isSafeInteger(filters.limit) || filters.limit < 1 || filters.limit > MAX_LIMIT) {
|
|
65
|
+
throw usageError("Task catalog limit must be between 1 and 100.");
|
|
66
|
+
}
|
|
67
|
+
const scope = contextContentDigest({ taskId: taskId ?? null, ...filters });
|
|
68
|
+
const cursor = rawCursor === undefined ? undefined : decodeCursor(rawCursor, scope);
|
|
69
|
+
const facts = reader.queryTaskCatalog({
|
|
70
|
+
...filters, ...(taskId === undefined ? {} : { taskId }),
|
|
71
|
+
...(cursor === undefined ? {} : { after: cursor.after, through: cursor.through }),
|
|
72
|
+
limit: options.limit + 1
|
|
73
|
+
});
|
|
74
|
+
const refs = new Map();
|
|
75
|
+
const getRef = (id) => {
|
|
76
|
+
let ref = refs.get(id);
|
|
77
|
+
if (ref === undefined) {
|
|
78
|
+
const task = reader.getTask(id);
|
|
79
|
+
if (task === null)
|
|
80
|
+
throw usageError("Task catalog changed during the read; retry.");
|
|
81
|
+
ref = { taskId: id, ...materialize("task", id, task).ref };
|
|
82
|
+
refs.set(id, ref);
|
|
83
|
+
}
|
|
84
|
+
return ref;
|
|
85
|
+
};
|
|
86
|
+
const attention = Object.fromEntries(CATALOG_ATTENTION.map(kind => {
|
|
87
|
+
const source = facts.attention[kind];
|
|
88
|
+
const selected = [];
|
|
89
|
+
for (const id of source.taskIds) {
|
|
90
|
+
const ref = getRef(id);
|
|
91
|
+
if (bytes([...selected, ref]) <= 2048)
|
|
92
|
+
selected.push(ref);
|
|
93
|
+
}
|
|
94
|
+
return [kind, {
|
|
95
|
+
count: source.count,
|
|
96
|
+
unit: kind === "executionSignals" ? "signals" : "records",
|
|
97
|
+
refs: selected,
|
|
98
|
+
// Samples identify affected Tasks, not individual records. Count and
|
|
99
|
+
// sample size therefore must never be subtracted from one another.
|
|
100
|
+
taskCount: source.taskCount,
|
|
101
|
+
omittedTaskRefs: source.taskCount - selected.length,
|
|
102
|
+
query: { view: "compact", all: options.all, attention: kind }
|
|
103
|
+
}];
|
|
104
|
+
}));
|
|
105
|
+
const response = {
|
|
106
|
+
view: "compact",
|
|
107
|
+
scope: { taskId: taskId ?? null, archived: options.all ? "included" : "excluded",
|
|
108
|
+
attention: "authorized-catalog-before-filters", executionSignals: "raw-inspection-candidates-not-execution-status" },
|
|
109
|
+
filters, total: facts.total, counts: facts.counts, attention,
|
|
110
|
+
tasks: [],
|
|
111
|
+
nextCursor: null,
|
|
112
|
+
consistency: "current-per-page; refresh for membership changes; new creation keys after the upper bound excluded",
|
|
113
|
+
limits: { pageBytes: PAGE_BYTES, summaryBytes: SUMMARY_BYTES, maxTasks: options.limit, attentionTaskRefsPerCategory: 4 }
|
|
114
|
+
};
|
|
115
|
+
const through = cursor?.through ?? facts.through;
|
|
116
|
+
const continuation = (after) => through === null ? null
|
|
117
|
+
: Buffer.from(JSON.stringify({ version: 1, scope, after, through })).toString("base64url");
|
|
118
|
+
for (const row of facts.rows.slice(0, options.limit)) {
|
|
119
|
+
const ref = getRef(row.id);
|
|
120
|
+
const summary = row.summary === null ? null : truncateUtf8(row.summary, SUMMARY_BYTES);
|
|
121
|
+
const title = truncateUtf8(row.title, 256);
|
|
122
|
+
// SQL already limits these fields. Compare against the selected original
|
|
123
|
+
// only when necessary; length-at-limit is conservatively marked omitted.
|
|
124
|
+
const item = {
|
|
125
|
+
id: row.id, title, status: row.status, createdAt: row.createdAt, updatedAt: row.updatedAt,
|
|
126
|
+
summary, summaryStatus: row.summaryPresent ? "available" : "missing",
|
|
127
|
+
omitted: { title: row.title.length >= 256 || title !== row.title,
|
|
128
|
+
summary: row.summary !== null && (row.summary.length >= 512 || summary !== row.summary) },
|
|
129
|
+
counts: { workItems: row.workItems, activeRuns: row.activeRuns },
|
|
130
|
+
attention: Object.fromEntries(CATALOG_ATTENTION.map(kind => [kind, row[kind]])),
|
|
131
|
+
ref
|
|
132
|
+
};
|
|
133
|
+
const next = continuation({ id: row.id, createdAt: row.createdAt });
|
|
134
|
+
if (bytes({ ok: true, data: { ...response, tasks: [...response.tasks, item], nextCursor: next } }) + 1 > PAGE_BYTES)
|
|
135
|
+
break;
|
|
136
|
+
response.tasks.push(item);
|
|
137
|
+
}
|
|
138
|
+
if (response.tasks.length < facts.rows.length) {
|
|
139
|
+
const last = response.tasks.at(-1);
|
|
140
|
+
if (last === undefined)
|
|
141
|
+
throw usageError("Task catalog identity/attention metadata exceeds the page byte budget.");
|
|
142
|
+
response.nextCursor = continuation({ id: last.id, createdAt: last.createdAt });
|
|
143
|
+
}
|
|
144
|
+
if (bytes({ ok: true, data: response }) + 1 > PAGE_BYTES)
|
|
145
|
+
throw usageError("Task catalog metadata exceeds the page byte budget.");
|
|
146
|
+
return response;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
export function renderTaskCatalog(result) {
|
|
150
|
+
return [
|
|
151
|
+
`Tasks (compact): ${result.tasks.length} shown; ${result.total} matching`,
|
|
152
|
+
...result.tasks.map(task => `${task.id}\t${task.status}\t${task.title}\n ${task.summary ?? "(summary missing)"}`),
|
|
153
|
+
`Attention across catalog: ${CATALOG_ATTENTION.map(kind => `${kind}=${result.attention[kind].count}`).join(", ")}`,
|
|
154
|
+
"Expand: yui task context <task-id>; inspect the Task ref for original requirements.",
|
|
155
|
+
...(result.nextCursor === null ? [] : [`Next cursor: ${result.nextCursor}`])
|
|
156
|
+
].join("\n") + "\n";
|
|
157
|
+
}
|
|
158
|
+
function bytes(value) { return Buffer.byteLength(JSON.stringify(value)); }
|
|
159
|
+
function truncateUtf8(value, max) {
|
|
160
|
+
let result = "";
|
|
161
|
+
let size = 0;
|
|
162
|
+
for (const point of value) {
|
|
163
|
+
size += Buffer.byteLength(point);
|
|
164
|
+
if (size > max)
|
|
165
|
+
break;
|
|
166
|
+
result += point;
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
function decodeCursor(raw, scope) {
|
|
171
|
+
try {
|
|
172
|
+
if (raw.length > 4096 || !/^[A-Za-z0-9_-]+$/.test(raw))
|
|
173
|
+
throw new Error();
|
|
174
|
+
const parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
175
|
+
const validKey = (key) => key && typeof key.id === "string" && key.id.length > 0
|
|
176
|
+
&& typeof key.createdAt === "string" && Number.isFinite(Date.parse(key.createdAt));
|
|
177
|
+
if (parsed.version !== 1 || parsed.scope !== scope || !validKey(parsed.after) || !validKey(parsed.through)
|
|
178
|
+
|| parsed.after.createdAt > parsed.through.createdAt
|
|
179
|
+
|| (parsed.after.createdAt === parsed.through.createdAt
|
|
180
|
+
&& Buffer.compare(Buffer.from(parsed.after.id), Buffer.from(parsed.through.id)) > 0))
|
|
181
|
+
throw new Error();
|
|
182
|
+
return parsed;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
throw usageError("Invalid Task catalog cursor or cursor belongs to another scope/filter.");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -7,6 +7,8 @@ import { expandTaskMessageResult } from "../message/message.js";
|
|
|
7
7
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
8
8
|
import { runExecutionObservation } from "../agentRun/agentRun.js";
|
|
9
9
|
import { isGitArtifactRefString, parseGitArtifactRef } from "../artifacts/gitArtifactRef.js";
|
|
10
|
+
import { projectTaskRemoteDeliveryFromStore } from "../commands/taskRemoteDeliveryCommand.js";
|
|
11
|
+
import { providerRetryProjection } from "../runtime/providerRetry.js";
|
|
10
12
|
const MAX_RECORDS = 256;
|
|
11
13
|
const MAX_VALUE_BYTES = 4096;
|
|
12
14
|
const MAX_PAGE_BYTES = 128 * 1024;
|
|
@@ -33,6 +35,8 @@ export function readTaskContext(store, taskId, environment = {}) {
|
|
|
33
35
|
: { ...entry, omitted: false };
|
|
34
36
|
const record = { ...content, ...(entry.ref.store === "run" ? {
|
|
35
37
|
execution: runExecutionObservation(entry.value, reader.getTaskRoleSessionSet(taskId, entry.value.roleName)?.providerBinding, runtimeEvents)
|
|
38
|
+
} : {}), ...(entry.ref.store === "role" ? {
|
|
39
|
+
providerRetry: providerRetryProjection(reader.getTaskRoleSessionSet(taskId, entry.value.name)?.providerBinding)
|
|
36
40
|
} : {}) };
|
|
37
41
|
const size = Buffer.byteLength(JSON.stringify(record));
|
|
38
42
|
if (records.length >= MAX_RECORDS || bytes + size > MAX_PAGE_BYTES)
|
|
@@ -201,10 +205,9 @@ export async function withContextObservations(core, providers, timeoutMs = 250)
|
|
|
201
205
|
}));
|
|
202
206
|
return { ...core, observations };
|
|
203
207
|
}
|
|
204
|
-
|
|
208
|
+
/** Shared current/historical Session read identity; discovery may only narrow it. */
|
|
209
|
+
export function resolveContextReader(store, environment) {
|
|
205
210
|
const caller = resolveManagedTaskReader(store, environment);
|
|
206
|
-
if (caller !== undefined && caller.taskId !== taskId)
|
|
207
|
-
throw usageError("Context is outside the caller's Task.");
|
|
208
211
|
if (caller === undefined && environment.YUI_SESSION_SCOPE === "global" && environment.YUI_ROLE !== "operator") {
|
|
209
212
|
throw usageError("Only Operator may read Task context from a global Session.");
|
|
210
213
|
}
|
|
@@ -216,6 +219,12 @@ function authorizeContext(store, taskId, environment) {
|
|
|
216
219
|
if (environment.YUI_SESSION_SCOPE !== undefined && !["task", "global"].includes(environment.YUI_SESSION_SCOPE)) {
|
|
217
220
|
throw usageError("Incomplete managed Context caller identity.");
|
|
218
221
|
}
|
|
222
|
+
return caller;
|
|
223
|
+
}
|
|
224
|
+
function authorizeContext(store, taskId, environment) {
|
|
225
|
+
const caller = resolveContextReader(store, environment);
|
|
226
|
+
if (caller !== undefined && caller.taskId !== taskId)
|
|
227
|
+
throw usageError("Context is outside the caller's Task.");
|
|
219
228
|
const task = store.getTask(taskId);
|
|
220
229
|
if (task === null)
|
|
221
230
|
throw taskNotFound(taskId);
|
|
@@ -227,13 +236,22 @@ function authorizeContext(store, taskId, environment) {
|
|
|
227
236
|
allow = new Set(pack.authority.readableRefs.map((ref) => `${ref.store}:${ref.refId}`));
|
|
228
237
|
allow.add(`role:${caller.roleName}`);
|
|
229
238
|
allow.add(`turn:${caller.currentRunId}`);
|
|
239
|
+
// A steer targets exactly one Role's current native Turn and is never shared
|
|
240
|
+
// Task intent (decision-3 §9). A Leader steer in particular stores as an
|
|
241
|
+
// untargeted `user` Message (no recipient/WorkItem/Run scope, since a Leader
|
|
242
|
+
// holds no Assignment), so recognize it explicitly: the shared block below
|
|
243
|
+
// must never leak it, and the steer-delta block authorizes it only to its own
|
|
244
|
+
// recipient. `interruptThen.reusedInput` is the interrupt-then composition
|
|
245
|
+
// whose carried input can itself be a steer.
|
|
246
|
+
const isSteerMessage = (message) => message.inputControl?.action === "steer"
|
|
247
|
+
|| message.interruptThen?.reusedInput?.action === "steer";
|
|
230
248
|
// A frozen Assignment is not a cutoff for the Task's current user intent.
|
|
231
249
|
// Untargeted human/Operator messages are shared Task requirements; scoped
|
|
232
|
-
// messages
|
|
250
|
+
// messages, other Roles' results, and steers still require an explicit grant.
|
|
233
251
|
const sharedMessageIds = new Set(store.listMessages(taskId)
|
|
234
252
|
.filter(message => (message.kind === "user" || message.kind === "operator")
|
|
235
253
|
&& message.recipient === undefined && message.workItemId === undefined
|
|
236
|
-
&& message.runId === undefined)
|
|
254
|
+
&& message.runId === undefined && !isSteerMessage(message))
|
|
237
255
|
.map(message => message.id));
|
|
238
256
|
for (const id of sharedMessageIds)
|
|
239
257
|
allow.add(`task-message:${id}`);
|
|
@@ -242,6 +260,33 @@ function authorizeContext(store, taskId, environment) {
|
|
|
242
260
|
allow.add(`task-event:${event.id}`);
|
|
243
261
|
}
|
|
244
262
|
}
|
|
263
|
+
// A steer targets this Role's exact current native Turn (decision-3 §9,
|
|
264
|
+
// message-5 gap E). Unlike a queued or addressed Message — which reaches the
|
|
265
|
+
// Role through a new AgentRun's frozen Context pack — a steer is pushed into
|
|
266
|
+
// the *current* turn and is never captured by any frozen snapshot. So the
|
|
267
|
+
// steer header directs the recipient to reconcile the input "through your
|
|
268
|
+
// authorized Context read path"; that path must therefore resolve the exact
|
|
269
|
+
// steer Message it names. Authorize the steers addressed to this caller's
|
|
270
|
+
// exact current Assignment scope as a live delta — the same way untargeted
|
|
271
|
+
// user intent is layered on — WITHOUT expanding the frozen Assignment's
|
|
272
|
+
// readableRefs. Scope is the tightest signal a steer carries (it never gains
|
|
273
|
+
// a continuation runId), so a steer for a different WorkItem/Round is not
|
|
274
|
+
// authorized, and only genuine steers gain this read (an ordinary addressed
|
|
275
|
+
// Message still requires its own frozen delta).
|
|
276
|
+
const run = store.getRun(taskId, caller.currentRunId);
|
|
277
|
+
const steerMessageIds = new Set(store.listMessages(taskId)
|
|
278
|
+
.filter(message => isSteerMessage(message)
|
|
279
|
+
&& message.recipient?.roleName === caller.roleName
|
|
280
|
+
&& message.recipient.workItemId === run?.workItemId
|
|
281
|
+
&& message.recipient.reviewRoundId === run?.reviewRoundId)
|
|
282
|
+
.map(message => message.id));
|
|
283
|
+
for (const id of steerMessageIds)
|
|
284
|
+
allow.add(`task-message:${id}`);
|
|
285
|
+
for (const event of store.listEvents(taskId)) {
|
|
286
|
+
if (event.type.startsWith("message.") && steerMessageIds.has(event.payload.messageId)) {
|
|
287
|
+
allow.add(`task-event:${event.id}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
245
290
|
}
|
|
246
291
|
return { task, allow, caller };
|
|
247
292
|
}
|
|
@@ -263,6 +308,8 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
|
|
|
263
308
|
switch (family) {
|
|
264
309
|
case "task": return refId === taskId ? task : null;
|
|
265
310
|
case "task-brief": return refId === taskId ? store.getTaskBrief(taskId) : null;
|
|
311
|
+
case "remote-delivery": return refId === taskId && allow === undefined
|
|
312
|
+
? projectTaskRemoteDeliveryFromStore(store, task) : null;
|
|
266
313
|
case "role": return store.getRole(taskId, refId);
|
|
267
314
|
case "role-profile": {
|
|
268
315
|
const role = store.getRole(taskId, refId);
|
|
@@ -361,6 +408,8 @@ function authorizedEntries(store, taskId, environment) {
|
|
|
361
408
|
};
|
|
362
409
|
add("task", taskId, task);
|
|
363
410
|
add("task-brief", taskId, store.getTaskBrief(taskId));
|
|
411
|
+
if (allow === undefined)
|
|
412
|
+
add("remote-delivery", taskId, projectTaskRemoteDeliveryFromStore(store, task));
|
|
364
413
|
for (const role of store.listRoles(taskId)) {
|
|
365
414
|
add("role", role.name, role);
|
|
366
415
|
add("role-profile", role.name, roleProfile(role));
|
|
@@ -428,7 +477,7 @@ function authorizedEntries(store, taskId, environment) {
|
|
|
428
477
|
add("task-event", event.id, event);
|
|
429
478
|
return entries;
|
|
430
479
|
}
|
|
431
|
-
function materialize(store, refId, value) {
|
|
480
|
+
export function materialize(store, refId, value) {
|
|
432
481
|
const digest = contextContentDigest(value);
|
|
433
482
|
const record = value;
|
|
434
483
|
return { ref: { store, refId, revision: String(record.revision ?? record.updatedAt ?? record.createdAt ?? digest), digest }, value };
|
|
@@ -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
|
}
|
|
@@ -701,6 +701,8 @@ export class FileTaskController {
|
|
|
701
701
|
#onMaintenanceFenceDefer;
|
|
702
702
|
#leaderWakeFence;
|
|
703
703
|
#jobSupervisor;
|
|
704
|
+
#globalInputDelivery;
|
|
705
|
+
#providerRetry;
|
|
704
706
|
#continuationReconciler;
|
|
705
707
|
#current;
|
|
706
708
|
#operatorCurrent;
|
|
@@ -710,6 +712,7 @@ export class FileTaskController {
|
|
|
710
712
|
#operatorStartupRetryArmed = false;
|
|
711
713
|
#lastOperatorSignalIdentity;
|
|
712
714
|
#stopped = false;
|
|
715
|
+
#workspaceWaitAbort = new AbortController();
|
|
713
716
|
#lastRuntimeDrain;
|
|
714
717
|
#runtimeDrainPasses = 0;
|
|
715
718
|
#runtimeListedEvents = 0;
|
|
@@ -723,7 +726,11 @@ export class FileTaskController {
|
|
|
723
726
|
this.#now = options.now ?? (() => new Date());
|
|
724
727
|
this.#startedAt = this.#now();
|
|
725
728
|
this.#onError = options.onError ?? (() => { });
|
|
726
|
-
|
|
729
|
+
const workspacePreparer = options.workspacePreparer;
|
|
730
|
+
this.#workspacePreparer = workspacePreparer === undefined ? undefined : {
|
|
731
|
+
prepareTaskWorkspace: (taskId) => workspacePreparer.prepareTaskWorkspace(taskId, this.#workspaceWaitAbort.signal),
|
|
732
|
+
activateTaskWorkspace: (taskId, environment) => workspacePreparer.activateTaskWorkspace(taskId, environment, this.#workspaceWaitAbort.signal)
|
|
733
|
+
};
|
|
727
734
|
this.#deliveryRetryMs = positiveInteger(options.deliveryRetryMs, DEFAULT_DELIVERY_RETRY_MS, "Controller delivery retry delay");
|
|
728
735
|
this.#deliveryRetryLimit = positiveInteger(options.deliveryRetryLimit, DEFAULT_DELIVERY_RETRY_LIMIT, "Controller delivery retry limit");
|
|
729
736
|
this.#deliveryTimeoutMs = positiveInteger(options.deliveryTimeoutMs, DEFAULT_DELIVERY_TIMEOUT_MS, "Controller delivery timeout");
|
|
@@ -747,6 +754,8 @@ export class FileTaskController {
|
|
|
747
754
|
this.#leaderWakeFence = options.leaderWakeFence;
|
|
748
755
|
this.#jobSupervisor = options.jobSupervisor;
|
|
749
756
|
this.#continuationReconciler = options.continuationReconciler;
|
|
757
|
+
this.#globalInputDelivery = options.globalInputDelivery;
|
|
758
|
+
this.#providerRetry = options.providerRetry;
|
|
750
759
|
this.#signalScheduler = new MailboxScheduler(async (keys) => { await this.#requestPass({ kind: "dirty", keys }); }, {
|
|
751
760
|
windowMs: options.signalWindowMs ?? DEFAULT_SIGNAL_WINDOW_MS,
|
|
752
761
|
onError: this.#onError,
|
|
@@ -844,6 +853,7 @@ export class FileTaskController {
|
|
|
844
853
|
}
|
|
845
854
|
stop() {
|
|
846
855
|
this.#stopped = true;
|
|
856
|
+
this.#workspaceWaitAbort.abort(new Error("Controller stopped while waiting for Project maintenance."));
|
|
847
857
|
this.#signalScheduler.stop();
|
|
848
858
|
this.#operatorSignalScheduler.stop();
|
|
849
859
|
if (this.#deadlineTimer !== undefined) {
|
|
@@ -986,6 +996,8 @@ export class FileTaskController {
|
|
|
986
996
|
}
|
|
987
997
|
}
|
|
988
998
|
const firstRuntimeDrain = await this.#drainRuntimeEvents();
|
|
999
|
+
await this.#providerRetry?.reconcile();
|
|
1000
|
+
await this.#globalInputDelivery?.();
|
|
989
1001
|
for (const taskId of runtimeTaskFailureIds(firstRuntimeDrain)) {
|
|
990
1002
|
runtimeFailedTaskIds.add(taskId);
|
|
991
1003
|
}
|
|
@@ -1328,7 +1340,7 @@ export class FileTaskController {
|
|
|
1328
1340
|
at: Date.parse(request.policy.timeoutAt)
|
|
1329
1341
|
}]
|
|
1330
1342
|
: []);
|
|
1331
|
-
const nearest = nearestDeadlineBatch(deadlines);
|
|
1343
|
+
const nearest = nearestDeadlineBatch([...deadlines, ...(this.#providerRetry?.deadlines() ?? [])]);
|
|
1332
1344
|
if (nearest === null)
|
|
1333
1345
|
return;
|
|
1334
1346
|
const now = this.#now().getTime();
|