@zq-silk/yui 0.15.11 → 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/agentRun/agentRun.js +3 -0
- package/dist/cli/commandCatalog.js +38 -10
- package/dist/cli/interactionPolicy.js +4 -0
- package/dist/cli/managedDiagnostics.js +1 -1
- package/dist/cli.js +46 -15
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +27 -2
- package/dist/commands/projectCommands.js +44 -15
- package/dist/commands/taskCommands.js +95 -33
- package/dist/commands/taskIntegrationCommands.js +3 -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 +18 -7
- package/dist/context/runContextPack.js +3 -0
- package/dist/context/taskCatalog.js +187 -0
- package/dist/context/taskContext.js +17 -4
- package/dist/controller/controller.js +11 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +80 -19
- package/dist/controller/globalInputDelivery.js +13 -0
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/runtime.js +36 -1
- package/dist/integration/gitIntegrationService.js +19 -6
- package/dist/lifecycle/exactRunTerminalization.js +4 -1
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/repository/gitWorkspace.js +371 -105
- package/dist/repository/projectMaintenanceLock.js +75 -18
- package/dist/repository/taskWorkspaceCoordinator.js +71 -124
- package/dist/repository/taskWorkspacePreparer.js +86 -24
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentHost.js +28 -11
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +6 -2
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/scheduler/activeRoleRunDelivery.js +12 -0
- package/dist/scheduler/leaderWakeupProcessor.js +5 -0
- package/dist/scheduler/taskExecutionProjection.js +26 -5
- package/dist/scheduler/taskObservabilityProjection.js +6 -44
- package/dist/storage/sqliteSchema.js +31 -0
- package/dist/storage/sqliteStore.js +17 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +2 -0
- package/dist/task/archiveDiagnostics.js +1 -0
- package/dist/task/archivePreflight.js +124 -0
- 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 +92 -17
- package/dist/web/assets/client/components.js +55 -13
- package/dist/web/assets/client/i18n.js +72 -4
- package/dist/web/assets/client/taskSurface.js +2 -1
- package/dist/web/assets/client/view.js +35 -7
- package/dist/web/assets/shell.js +6 -0
- package/dist/web/assets/styles/layout.js +7 -0
- package/dist/web/webServer.js +14 -3
- package/dist/web/webSnapshot.js +12 -3
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +110 -50
- 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/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 +16 -0
- package/docs/testing/verification-levels.zh-CN.md +13 -1
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +3 -2
- package/skills/yui-operator/SKILL.md +13 -2
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +17 -2
- package/skills/yui-runtime/references/publication.md +26 -4
- package/skills/yui-runtime/references/recovery.md +24 -0
|
@@ -30,6 +30,7 @@ export function projectTaskRemoteDeliveryFromStore(store, task, currentCandidate
|
|
|
30
30
|
publications: store.listPublicationReferences(task.id),
|
|
31
31
|
managedWorkspaces: store.listManagedWorkspaces(task.id),
|
|
32
32
|
runs: store.listRuns(task.id),
|
|
33
|
+
integrations: store.listIntegrationAttempts(task.id),
|
|
33
34
|
currentCandidate
|
|
34
35
|
});
|
|
35
36
|
}
|
|
@@ -41,6 +42,7 @@ export function createTaskRemoteDeliveryProof(store, task, currentCandidate = nu
|
|
|
41
42
|
publications,
|
|
42
43
|
managedWorkspaces: store.listManagedWorkspaces(task.id),
|
|
43
44
|
runs: store.listRuns(task.id),
|
|
45
|
+
integrations: store.listIntegrationAttempts(task.id),
|
|
44
46
|
currentCandidate
|
|
45
47
|
});
|
|
46
48
|
return {
|
|
@@ -60,6 +62,15 @@ export function assertTaskRemoteDeliveryProof(store, task, proof) {
|
|
|
60
62
|
if (!isDeepStrictEqual(proof.publications, store.listPublicationReferences(task.id))) {
|
|
61
63
|
throw usageError(`Task Publication evidence changed after remote-delivery preflight: ${task.id}.`);
|
|
62
64
|
}
|
|
65
|
+
// Recheck semantic adoption/Integration facts as well as Publication versions.
|
|
66
|
+
// For provisional cancelled Tasks retain the frozen candidate used at preflight.
|
|
67
|
+
const current = projectTaskRemoteDeliveryFromStore(store, task, {
|
|
68
|
+
projects: proof.delivery.projects.flatMap(p => p.expectedLocalCommit === null
|
|
69
|
+
? [] : [{ projectId: p.projectId, commit: p.expectedLocalCommit }])
|
|
70
|
+
});
|
|
71
|
+
if (!isDeepStrictEqual(current, proof.delivery)) {
|
|
72
|
+
throw usageError(`Task delivery evidence changed after remote-delivery preflight: ${task.id}.`);
|
|
73
|
+
}
|
|
63
74
|
assertTaskRemoteDeliveryIntegrated(proof.delivery);
|
|
64
75
|
return proof.delivery;
|
|
65
76
|
}
|
|
@@ -68,16 +79,15 @@ export function assertTaskRemoteDeliveryIntegrated(delivery) {
|
|
|
68
79
|
const headUnavailable = delivery.projects.filter((project) => (project.codeDelivery !== "none"
|
|
69
80
|
&& project.coverage === "head-unavailable"));
|
|
70
81
|
if (headUnavailable.length > 0 && delivery.source === "task-completed") {
|
|
71
|
-
throw usageError(`Task ${delivery.taskId} has no frozen completion heads for: ${headUnavailable.map(({ projectId }) => projectId).join(", ")}.
|
|
72
|
-
+ "An explicitly authorized --force archive retains this evidence gap.");
|
|
82
|
+
throw usageError(`Task ${delivery.taskId} has no frozen completion heads for: ${headUnavailable.map(({ projectId }) => projectId).join(", ")}. Historical acceptance cannot be inferred from a merge or archive.`);
|
|
73
83
|
}
|
|
74
84
|
const uncovered = delivery.projects
|
|
75
85
|
.filter((project) => project.codeDelivery !== "none" && !project.merged)
|
|
76
86
|
.map((project) => (`${project.projectId}/${project.coverage}`
|
|
77
87
|
+ (project.publication === null ? "" : `/${project.publication.id}`)))
|
|
78
88
|
.join(", ");
|
|
79
|
-
throw usageError(`Task ${delivery.taskId} is not fully merged into remote delivery: ${uncovered || "expected Project heads are unavailable"}.
|
|
80
|
-
+ "
|
|
89
|
+
throw usageError(`Task ${delivery.taskId} is not fully merged into remote delivery: ${uncovered || "expected Project heads are unavailable"}. Inspect each Project reason with task remote-delivery. Record confirmed Publication facts; `
|
|
90
|
+
+ "for a changed post-completion candidate, review publication diff and explicitly adopt its acceptance.");
|
|
81
91
|
}
|
|
82
92
|
if (delivery.allVerified)
|
|
83
93
|
return;
|
|
@@ -90,8 +100,7 @@ export function assertTaskRemoteDeliveryIntegrated(delivery) {
|
|
|
90
100
|
.join(", ");
|
|
91
101
|
throw usageError(`Task ${delivery.taskId} has merged Publication evidence that is not verified: `
|
|
92
102
|
+ `${unverified || "verification evidence is unavailable"}. Run task publication verify `
|
|
93
|
-
+ "for each Publication
|
|
94
|
-
+ "archive despite evidence gaps, retaining the original delivery facts.");
|
|
103
|
+
+ "for each Publication only when the provider read is authorized.");
|
|
95
104
|
}
|
|
96
105
|
export function renderTaskRemoteDelivery(delivery, indent = "") {
|
|
97
106
|
const source = `${delivery.source}${delivery.provisional ? " (provisional)" : ""}`;
|
|
@@ -108,12 +117,14 @@ export function renderTaskRemoteDelivery(delivery, indent = "") {
|
|
|
108
117
|
`${indent}Projects:${delivery.projects.length === 0 ? " none" : ""}`,
|
|
109
118
|
...delivery.projects.map((project) => (`${indent}- ${project.directory} (${project.projectId}): `
|
|
110
119
|
+ `expected=${shortCommit(project.expectedLocalCommit)}; `
|
|
120
|
+
+ `candidate=${shortCommit(project.deliveryLocalCommit)}; `
|
|
121
|
+
+ `adoption=${project.adoption?.id ?? "none"}; `
|
|
111
122
|
+ `base=${shortCommit(project.baseCommit)}; `
|
|
112
123
|
+ `publication=${project.publication?.id ?? "none"}; `
|
|
113
124
|
+ `state=${project.state ?? "none"}; `
|
|
114
125
|
+ `verification=${project.verification ?? "none"}; `
|
|
115
126
|
+ `remote=${shortCommit(project.remoteCommit)}; `
|
|
116
|
-
+ `coverage=${project.coverage}`))
|
|
127
|
+
+ `coverage=${project.coverage}\n${indent} ${project.reason}`))
|
|
117
128
|
];
|
|
118
129
|
return `${lines.join("\n")}\n`;
|
|
119
130
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { providerRetryProjection } from "../runtime/providerRetry.js";
|
|
1
2
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
2
3
|
import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
|
|
3
4
|
import { RUN_INPUT_MAX_DELTAS } from "./runInputContract.js";
|
|
@@ -295,6 +296,8 @@ function readLiveTaskState(store, taskId) {
|
|
|
295
296
|
status: round.status
|
|
296
297
|
}));
|
|
297
298
|
return Object.freeze({
|
|
299
|
+
providerRetries: Object.freeze(store.listRoleSessionSets(taskId).flatMap(set => set.providerBinding?.retry === undefined
|
|
300
|
+
? [] : [{ roleName: set.owner.roleName, retry: providerRetryProjection(set.providerBinding) }])),
|
|
298
301
|
activeRuns: Object.freeze(activeRuns),
|
|
299
302
|
activeTaskReviews: Object.freeze(activeTaskReviews)
|
|
300
303
|
});
|
|
@@ -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);
|
|
@@ -299,6 +308,8 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
|
|
|
299
308
|
switch (family) {
|
|
300
309
|
case "task": return refId === taskId ? task : null;
|
|
301
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;
|
|
302
313
|
case "role": return store.getRole(taskId, refId);
|
|
303
314
|
case "role-profile": {
|
|
304
315
|
const role = store.getRole(taskId, refId);
|
|
@@ -397,6 +408,8 @@ function authorizedEntries(store, taskId, environment) {
|
|
|
397
408
|
};
|
|
398
409
|
add("task", taskId, task);
|
|
399
410
|
add("task-brief", taskId, store.getTaskBrief(taskId));
|
|
411
|
+
if (allow === undefined)
|
|
412
|
+
add("remote-delivery", taskId, projectTaskRemoteDeliveryFromStore(store, task));
|
|
400
413
|
for (const role of store.listRoles(taskId)) {
|
|
401
414
|
add("role", role.name, role);
|
|
402
415
|
add("role-profile", role.name, roleProfile(role));
|
|
@@ -464,7 +477,7 @@ function authorizedEntries(store, taskId, environment) {
|
|
|
464
477
|
add("task-event", event.id, event);
|
|
465
478
|
return entries;
|
|
466
479
|
}
|
|
467
|
-
function materialize(store, refId, value) {
|
|
480
|
+
export function materialize(store, refId, value) {
|
|
468
481
|
const digest = contextContentDigest(value);
|
|
469
482
|
const record = value;
|
|
470
483
|
return { ref: { store, refId, revision: String(record.revision ?? record.updatedAt ?? record.createdAt ?? digest), digest }, value };
|
|
@@ -702,6 +702,7 @@ export class FileTaskController {
|
|
|
702
702
|
#leaderWakeFence;
|
|
703
703
|
#jobSupervisor;
|
|
704
704
|
#globalInputDelivery;
|
|
705
|
+
#providerRetry;
|
|
705
706
|
#continuationReconciler;
|
|
706
707
|
#current;
|
|
707
708
|
#operatorCurrent;
|
|
@@ -711,6 +712,7 @@ export class FileTaskController {
|
|
|
711
712
|
#operatorStartupRetryArmed = false;
|
|
712
713
|
#lastOperatorSignalIdentity;
|
|
713
714
|
#stopped = false;
|
|
715
|
+
#workspaceWaitAbort = new AbortController();
|
|
714
716
|
#lastRuntimeDrain;
|
|
715
717
|
#runtimeDrainPasses = 0;
|
|
716
718
|
#runtimeListedEvents = 0;
|
|
@@ -724,7 +726,11 @@ export class FileTaskController {
|
|
|
724
726
|
this.#now = options.now ?? (() => new Date());
|
|
725
727
|
this.#startedAt = this.#now();
|
|
726
728
|
this.#onError = options.onError ?? (() => { });
|
|
727
|
-
|
|
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
|
+
};
|
|
728
734
|
this.#deliveryRetryMs = positiveInteger(options.deliveryRetryMs, DEFAULT_DELIVERY_RETRY_MS, "Controller delivery retry delay");
|
|
729
735
|
this.#deliveryRetryLimit = positiveInteger(options.deliveryRetryLimit, DEFAULT_DELIVERY_RETRY_LIMIT, "Controller delivery retry limit");
|
|
730
736
|
this.#deliveryTimeoutMs = positiveInteger(options.deliveryTimeoutMs, DEFAULT_DELIVERY_TIMEOUT_MS, "Controller delivery timeout");
|
|
@@ -749,6 +755,7 @@ export class FileTaskController {
|
|
|
749
755
|
this.#jobSupervisor = options.jobSupervisor;
|
|
750
756
|
this.#continuationReconciler = options.continuationReconciler;
|
|
751
757
|
this.#globalInputDelivery = options.globalInputDelivery;
|
|
758
|
+
this.#providerRetry = options.providerRetry;
|
|
752
759
|
this.#signalScheduler = new MailboxScheduler(async (keys) => { await this.#requestPass({ kind: "dirty", keys }); }, {
|
|
753
760
|
windowMs: options.signalWindowMs ?? DEFAULT_SIGNAL_WINDOW_MS,
|
|
754
761
|
onError: this.#onError,
|
|
@@ -846,6 +853,7 @@ export class FileTaskController {
|
|
|
846
853
|
}
|
|
847
854
|
stop() {
|
|
848
855
|
this.#stopped = true;
|
|
856
|
+
this.#workspaceWaitAbort.abort(new Error("Controller stopped while waiting for Project maintenance."));
|
|
849
857
|
this.#signalScheduler.stop();
|
|
850
858
|
this.#operatorSignalScheduler.stop();
|
|
851
859
|
if (this.#deadlineTimer !== undefined) {
|
|
@@ -988,6 +996,7 @@ export class FileTaskController {
|
|
|
988
996
|
}
|
|
989
997
|
}
|
|
990
998
|
const firstRuntimeDrain = await this.#drainRuntimeEvents();
|
|
999
|
+
await this.#providerRetry?.reconcile();
|
|
991
1000
|
await this.#globalInputDelivery?.();
|
|
992
1001
|
for (const taskId of runtimeTaskFailureIds(firstRuntimeDrain)) {
|
|
993
1002
|
runtimeFailedTaskIds.add(taskId);
|
|
@@ -1331,7 +1340,7 @@ export class FileTaskController {
|
|
|
1331
1340
|
at: Date.parse(request.policy.timeoutAt)
|
|
1332
1341
|
}]
|
|
1333
1342
|
: []);
|
|
1334
|
-
const nearest = nearestDeadlineBatch(deadlines);
|
|
1343
|
+
const nearest = nearestDeadlineBatch([...deadlines, ...(this.#providerRetry?.deadlines() ?? [])]);
|
|
1335
1344
|
if (nearest === null)
|
|
1336
1345
|
return;
|
|
1337
1346
|
const now = this.#now().getTime();
|
|
@@ -6,6 +6,9 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
6
6
|
import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
|
|
7
7
|
import { resolveAgentHostObservation, recordAgentHostConnection, AgentHostObservationDeferred } from "./agentHostObservation.js";
|
|
8
8
|
import { RuntimeHookRunFenceError } from "./runtimeHookRunFence.js";
|
|
9
|
+
import { admitOwnedProviderInput, ownedProviderInput } from "./providerRetryAdmission.js";
|
|
10
|
+
import { recordProviderFailure, providerRetryPending, deferProviderRetry } from "../runtime/providerRetry.js";
|
|
11
|
+
import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
9
12
|
import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindGlobalRoleProviderRuntime, createRoleSessionSet, recordRoleAgentSession, replaceTaskRoleAgentSession, recordTaskRoleNativeTurnBoundary, rememberRoleAgentCompletedTurn, detachRoleAgentSessionHost, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime, updateGlobalRoleProviderRuntime, selectNewTaskRoleSession, taskRoleControlTarget } from "../executor/agentExecutor.js";
|
|
10
13
|
import { acceptProviderTurn, cancelQuiescentProviderInput, beginProviderTurn, createProviderRuntimeBinding, currentProviderConversation, managedProviderTurnId, clearProviderGoal, settleProviderTurnSubmission, settleProviderTurn, transferProviderAuthority, supersedeProviderConversation, updateProviderConversationRecoverability, updateProviderGoal } from "../runtime/providerRuntimeIdentity.js";
|
|
11
14
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
@@ -404,7 +407,9 @@ export class FileSchedulerStoreAdapter {
|
|
|
404
407
|
}
|
|
405
408
|
else if (binding.run?.status !== "accepted")
|
|
406
409
|
return "applied";
|
|
407
|
-
const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`
|
|
410
|
+
const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`
|
|
411
|
+
|| binding?.retry?.currentAttemptId === receipt && binding.retry.input.kind === "message"
|
|
412
|
+
&& binding.retry.input.messageId === entry.id);
|
|
408
413
|
if (message !== undefined)
|
|
409
414
|
store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at)));
|
|
410
415
|
}
|
|
@@ -417,7 +422,9 @@ export class FileSchedulerStoreAdapter {
|
|
|
417
422
|
if (input.kind === "turn.completed" && binding.run != null
|
|
418
423
|
&& ["submitting", "delivery-unknown"].includes(binding.run.status)) {
|
|
419
424
|
binding = acceptProviderTurn(binding, { attemptId: receipt, nativeTurnId: fence.nativeTurnId, acceptedAt: at });
|
|
420
|
-
const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`
|
|
425
|
+
const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`
|
|
426
|
+
|| binding?.retry?.currentAttemptId === receipt && binding.retry.input.kind === "message"
|
|
427
|
+
&& binding.retry.input.messageId === entry.id);
|
|
421
428
|
if (message !== undefined)
|
|
422
429
|
store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at)));
|
|
423
430
|
}
|
|
@@ -449,7 +456,13 @@ export class FileSchedulerStoreAdapter {
|
|
|
449
456
|
else if (input.kind === "conversation.observed" && input.payload.recoverability !== undefined) {
|
|
450
457
|
binding = updateProviderConversationRecoverability(binding, input.payload.recoverability);
|
|
451
458
|
}
|
|
459
|
+
if (input.kind === "turn.failed" && matches && input.payload.failure !== undefined) {
|
|
460
|
+
binding = recordProviderFailure(binding, {
|
|
461
|
+
error: input.payload.failure.error, failureRef: input.eventId, at: now.getTime()
|
|
462
|
+
});
|
|
463
|
+
}
|
|
452
464
|
store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, binding, now));
|
|
465
|
+
settleGlobalRetryInput(store, fence.roleName, binding, now);
|
|
453
466
|
return "applied";
|
|
454
467
|
});
|
|
455
468
|
}
|
|
@@ -751,7 +764,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
751
764
|
...(failure.lastOutput === undefined ? {} : { lastOutput: failure.lastOutput })
|
|
752
765
|
}, now);
|
|
753
766
|
store.saveEvent(taskId, errorEvent);
|
|
754
|
-
|
|
767
|
+
if (!providerRetryPending(store.getTaskRoleSessionSet(taskId, input.fence.roleName)?.providerBinding))
|
|
768
|
+
routeRoleEvent(store, errorEvent, input.fence.roleName, wakeReason("agent-error", errorEvent.id), now);
|
|
755
769
|
}
|
|
756
770
|
if ((input.kind === "operation.completed" || input.kind === "operation.failed")
|
|
757
771
|
&& input.payload.operation === "subagent") {
|
|
@@ -1092,7 +1106,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1092
1106
|
throw new AgentHostProviderTurnFenceError("Global Session admission is stopped.");
|
|
1093
1107
|
}
|
|
1094
1108
|
const sessions = store.getGlobalRoleSessionSet(input.roleName);
|
|
1095
|
-
|
|
1109
|
+
let binding = sessions?.providerBinding;
|
|
1096
1110
|
const session = sessions?.sessions[input.agentId];
|
|
1097
1111
|
if (sessions === null || binding == null || session === undefined
|
|
1098
1112
|
|| sessions.activeAgentId !== input.agentId || session.status !== "active"
|
|
@@ -1104,8 +1118,10 @@ export class FileSchedulerStoreAdapter {
|
|
|
1104
1118
|
throw new AgentHostProviderTurnFenceError("Global input carries a stale Session or writer fence.");
|
|
1105
1119
|
}
|
|
1106
1120
|
const turn = binding.run;
|
|
1107
|
-
if (turn?.attemptId === input.attemptId && turn.status === "submitting")
|
|
1121
|
+
if (turn?.attemptId === input.attemptId && turn.status === "submitting") {
|
|
1122
|
+
admitOwnedProviderInput(store, binding, input);
|
|
1108
1123
|
return;
|
|
1124
|
+
}
|
|
1109
1125
|
if (turn != null && ["submitting", "accepted", "delivery-unknown"].includes(turn.status)) {
|
|
1110
1126
|
throw new AgentHostProviderSessionBusyError("Global Conversation has an unsettled input.");
|
|
1111
1127
|
}
|
|
@@ -1139,7 +1155,10 @@ export class FileSchedulerStoreAdapter {
|
|
|
1139
1155
|
receiptId: input.attemptId, outcome: "pending", observedAt: input.now.toISOString()
|
|
1140
1156
|
} });
|
|
1141
1157
|
}
|
|
1158
|
+
binding = admitOwnedProviderInput(store, binding, input);
|
|
1142
1159
|
store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, beginProviderTurn(binding, { attemptId: input.attemptId,
|
|
1160
|
+
input: binding.retry?.currentAttemptId === input.attemptId ? binding.retry.input : ownedProviderInput(input),
|
|
1161
|
+
retrySupported: input.retrySupported,
|
|
1143
1162
|
authorityEpoch: input.authorityEpoch, submittedAt: input.now.toISOString() }), input.now));
|
|
1144
1163
|
});
|
|
1145
1164
|
return;
|
|
@@ -1157,7 +1176,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1157
1176
|
}
|
|
1158
1177
|
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
1159
1178
|
const session = sessions?.sessions[input.agentId];
|
|
1160
|
-
|
|
1179
|
+
let binding = sessions?.providerBinding;
|
|
1161
1180
|
const active = store.getActiveRun(input.taskId, input.roleName);
|
|
1162
1181
|
if (sessions === null || sessions === undefined || binding === null || binding === undefined || (input.runId !== undefined && (active?.id !== input.runId || active.effective.agentId !== input.agentId)) || session === undefined || session.nativeSessionId !== input.nativeSessionId || currentProviderConversation(binding).conversationId !== input.nativeSessionId || binding.authority.owner !== input.authorityOwner || binding.authority.epoch !== input.authorityEpoch || binding.authority.holderId !== input.holderId) {
|
|
1163
1182
|
throw new AgentHostProviderTurnFenceError("Agent Host Provider Turn carries a stale durable writer fence. Release and reacquire Provider authority before retrying input.");
|
|
@@ -1214,11 +1233,14 @@ export class FileSchedulerStoreAdapter {
|
|
|
1214
1233
|
}
|
|
1215
1234
|
}
|
|
1216
1235
|
}
|
|
1236
|
+
binding = admitOwnedProviderInput(store, binding, input);
|
|
1217
1237
|
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, beginProviderTurn(binding, {
|
|
1218
1238
|
...(input.runId === undefined ? {} : { runId: input.runId }),
|
|
1219
1239
|
attemptId: input.attemptId,
|
|
1220
1240
|
authorityEpoch: input.authorityEpoch,
|
|
1221
|
-
submittedAt: input.now.toISOString()
|
|
1241
|
+
submittedAt: input.now.toISOString(),
|
|
1242
|
+
input: binding.retry?.currentAttemptId === input.attemptId ? binding.retry.input : ownedProviderInput(input),
|
|
1243
|
+
retrySupported: input.retrySupported
|
|
1222
1244
|
}), input.now));
|
|
1223
1245
|
});
|
|
1224
1246
|
}
|
|
@@ -1230,8 +1252,11 @@ export class FileSchedulerStoreAdapter {
|
|
|
1230
1252
|
if (input.runId !== undefined || sessions == null || binding?.run?.attemptId !== input.attemptId) {
|
|
1231
1253
|
throw new Error("Global input submission is no longer current.");
|
|
1232
1254
|
}
|
|
1233
|
-
|
|
1234
|
-
reason: input.reason, resolvedAt: input.now.toISOString() })
|
|
1255
|
+
let resolved = settleProviderTurnSubmission(binding, { attemptId: input.attemptId, status: input.status,
|
|
1256
|
+
reason: input.reason, resolvedAt: input.now.toISOString() });
|
|
1257
|
+
resolved = this.retrySubmissionResolution(resolved, input, sessions.sessions[sessions.activeAgentId]?.adapterId);
|
|
1258
|
+
store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, resolved, input.now));
|
|
1259
|
+
settleGlobalRetryInput(store, input.roleName, resolved, input.now);
|
|
1235
1260
|
const message = store.listGlobalRoleMessages(input.roleName).find(entry => input.attemptId === `global-input:${input.roleName}/${entry.id}`);
|
|
1236
1261
|
if (message !== undefined && message.delivery?.via !== "provider") {
|
|
1237
1262
|
const outcome = input.status === "delivery-unknown" ? "delivery-unknown" : "rejected";
|
|
@@ -1239,7 +1264,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1239
1264
|
requestId: message.inputControl?.requestId ?? message.interruptThen.requestId,
|
|
1240
1265
|
receiptId: input.attemptId, outcome, observedAt: input.now.toISOString()
|
|
1241
1266
|
} };
|
|
1242
|
-
store.updateGlobalRoleMessage(input.status === "rejected" && updated.delivery === undefined
|
|
1267
|
+
store.updateGlobalRoleMessage(input.status === "rejected" && updated.delivery === undefined && !providerRetryPending(resolved)
|
|
1243
1268
|
? markGlobalRoleMessageNotDelivered(updated, input.reason, input.now) : updated);
|
|
1244
1269
|
}
|
|
1245
1270
|
});
|
|
@@ -1257,20 +1282,18 @@ export class FileSchedulerStoreAdapter {
|
|
|
1257
1282
|
|| binding.run?.attemptId !== input.attemptId) {
|
|
1258
1283
|
throw new Error("Agent Host Provider Turn submission is no longer current.");
|
|
1259
1284
|
}
|
|
1260
|
-
|
|
1285
|
+
let updated = settleProviderTurnSubmission(binding, {
|
|
1261
1286
|
attemptId: input.attemptId,
|
|
1262
1287
|
status: input.status,
|
|
1263
1288
|
reason: input.reason,
|
|
1264
1289
|
resolvedAt: input.now.toISOString()
|
|
1265
1290
|
});
|
|
1291
|
+
updated = this.retrySubmissionResolution(updated, input, sessions.sessions[sessions.activeAgentId]?.adapterId);
|
|
1266
1292
|
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updated, input.now));
|
|
1267
|
-
|
|
1268
|
-
return;
|
|
1269
|
-
const run = store.getRun(input.taskId, input.runId);
|
|
1293
|
+
const run = input.runId === undefined ? null : store.getRun(input.taskId, input.runId);
|
|
1270
1294
|
const session = sessions.sessions[sessions.activeAgentId];
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1273
|
-
: this.drivers.findByAdapterId(run.effective.adapterId);
|
|
1295
|
+
const adapterId = run?.effective.adapterId ?? session?.adapterId;
|
|
1296
|
+
const driver = adapterId === undefined ? null : this.drivers.findByAdapterId(adapterId);
|
|
1274
1297
|
const error = standardAgentError({
|
|
1275
1298
|
source: "driver",
|
|
1276
1299
|
phase: "turn-submit",
|
|
@@ -1284,7 +1307,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1284
1307
|
});
|
|
1285
1308
|
const errorEvent = createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.agent-error", {
|
|
1286
1309
|
sourceEventId: input.attemptId,
|
|
1287
|
-
runId: input.runId,
|
|
1310
|
+
runId: input.runId ?? "",
|
|
1288
1311
|
roleName: input.roleName,
|
|
1289
1312
|
agentId: run?.effective.agentId ?? sessions.activeAgentId,
|
|
1290
1313
|
adapterId: run?.effective.adapterId ?? session?.adapterId ?? "unknown",
|
|
@@ -1306,7 +1329,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1306
1329
|
sessionDisposition: error.sessionDisposition
|
|
1307
1330
|
}, input.now);
|
|
1308
1331
|
store.saveEvent(input.taskId, errorEvent);
|
|
1309
|
-
if (input.status === "deferred")
|
|
1332
|
+
if (input.status === "deferred" || providerRetryPending(updated))
|
|
1310
1333
|
return;
|
|
1311
1334
|
const route = input.roleName === "leader"
|
|
1312
1335
|
? { kind: "operator" }
|
|
@@ -1319,6 +1342,30 @@ export class FileSchedulerStoreAdapter {
|
|
|
1319
1342
|
});
|
|
1320
1343
|
});
|
|
1321
1344
|
}
|
|
1345
|
+
retrySubmissionResolution(binding, input, adapterId) {
|
|
1346
|
+
if (input.status === "deferred")
|
|
1347
|
+
return deferProviderRetry(binding, input.now.getTime());
|
|
1348
|
+
const driver = adapterId === undefined ? null : this.drivers.findByAdapterId(adapterId);
|
|
1349
|
+
const classification = driver?.runtime.mapError({ message: input.reason, raw: input.raw });
|
|
1350
|
+
const error = standardAgentError({
|
|
1351
|
+
source: "driver", phase: "turn-submit", classification,
|
|
1352
|
+
message: input.reason, raw: input.raw,
|
|
1353
|
+
inputDisposition: input.status === "delivery-unknown" ? "unknown" : "not-accepted"
|
|
1354
|
+
});
|
|
1355
|
+
// Native preflight issued no mutation. Transient metadata failures may
|
|
1356
|
+
// cool down within the same deadline; unavailable proof needs an Agent.
|
|
1357
|
+
const noProviderWrite = input.status === "rejected" && input.noProviderWrite === true;
|
|
1358
|
+
if (noProviderWrite && classification?.retryable !== true) {
|
|
1359
|
+
const recorded = binding.run === null ? binding : {
|
|
1360
|
+
...binding, run: { ...binding.run, failure: { error, ref: input.attemptId } }
|
|
1361
|
+
};
|
|
1362
|
+
return deferProviderRetry(recorded, input.now.getTime(), input.reason);
|
|
1363
|
+
}
|
|
1364
|
+
return recordProviderFailure(binding, {
|
|
1365
|
+
error,
|
|
1366
|
+
failureRef: input.attemptId, at: input.now.getTime(), noProviderWrite
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1322
1369
|
getProviderAuthorityFence(input) {
|
|
1323
1370
|
const sessions = this.store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
1324
1371
|
const session = sessions?.sessions[input.agentId];
|
|
@@ -2140,6 +2187,12 @@ export class FileSchedulerStoreAdapter {
|
|
|
2140
2187
|
&& sessions.providerBinding !== null) {
|
|
2141
2188
|
sessions = updateTaskRoleProviderRuntime(sessions, updateProviderConversationRecoverability(sessions.providerBinding, "unrecoverable"), now);
|
|
2142
2189
|
}
|
|
2190
|
+
if (input.observation?.kind === "turn.failed" && input.observation.payload.failure !== undefined
|
|
2191
|
+
&& sessions.providerBinding !== null) {
|
|
2192
|
+
sessions = updateTaskRoleProviderRuntime(sessions, recordProviderFailure(sessions.providerBinding, {
|
|
2193
|
+
error: input.observation.payload.failure.error, failureRef: input.observation.eventId, at: now.getTime()
|
|
2194
|
+
}), now);
|
|
2195
|
+
}
|
|
2143
2196
|
store.saveTaskRoleSessionSet(sessions);
|
|
2144
2197
|
let terminalRun;
|
|
2145
2198
|
if (recordedProviderTurn && observedRun?.status === "active") {
|
|
@@ -2482,6 +2535,14 @@ export class FileSchedulerStoreAdapter {
|
|
|
2482
2535
|
return "obsolete";
|
|
2483
2536
|
}
|
|
2484
2537
|
store.saveTaskRoleSessionSet(recordStructuredProviderAcceptance(sessions, input, now));
|
|
2538
|
+
const retry = binding.retry;
|
|
2539
|
+
if (retry?.currentAttemptId === ordinaryAttemptId && retry.input.kind === "wake") {
|
|
2540
|
+
const originalWakeId = retry.input.wakeId;
|
|
2541
|
+
const original = store.listTaskWakes(input.fence.taskId).find(w => w.id === originalWakeId);
|
|
2542
|
+
if (original !== undefined && original.status !== "consumed") {
|
|
2543
|
+
store.saveTaskWake(input.fence.taskId, markTaskWakeConsumed(original, now));
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2485
2546
|
return "applied";
|
|
2486
2547
|
}
|
|
2487
2548
|
}
|