@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.
Files changed (101) hide show
  1. package/ARCHITECTURE.md +8 -4
  2. package/ARCHITECTURE.zh-CN.md +5 -2
  3. package/README.md +13 -5
  4. package/dist/agentRun/agentRun.js +3 -0
  5. package/dist/cli/commandCatalog.js +38 -10
  6. package/dist/cli/interactionPolicy.js +4 -0
  7. package/dist/cli/managedDiagnostics.js +1 -1
  8. package/dist/cli.js +46 -15
  9. package/dist/commands/executionAuditCommands.js +10 -0
  10. package/dist/commands/globalRoleCommands.js +27 -2
  11. package/dist/commands/projectCommands.js +44 -15
  12. package/dist/commands/taskCommands.js +95 -33
  13. package/dist/commands/taskIntegrationCommands.js +3 -1
  14. package/dist/commands/taskOverviewCommand.js +4 -3
  15. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  16. package/dist/commands/taskPublicationCommands.js +11 -2
  17. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  18. package/dist/commands/taskRemoteDeliveryCommand.js +18 -7
  19. package/dist/context/runContextPack.js +3 -0
  20. package/dist/context/taskCatalog.js +187 -0
  21. package/dist/context/taskContext.js +17 -4
  22. package/dist/controller/controller.js +11 -2
  23. package/dist/controller/fileSchedulerStoreAdapter.js +80 -19
  24. package/dist/controller/globalInputDelivery.js +13 -0
  25. package/dist/controller/providerRetryAdmission.js +100 -0
  26. package/dist/controller/providerRetryDelivery.js +218 -0
  27. package/dist/controller/runtime.js +36 -1
  28. package/dist/integration/gitIntegrationService.js +19 -6
  29. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  30. package/dist/message/globalProviderRetry.js +15 -0
  31. package/dist/observability/executionAudit.js +19 -0
  32. package/dist/repository/gitWorkspace.js +371 -105
  33. package/dist/repository/projectMaintenanceLock.js +75 -18
  34. package/dist/repository/taskWorkspaceCoordinator.js +71 -124
  35. package/dist/repository/taskWorkspacePreparer.js +86 -24
  36. package/dist/repository/workspaceCleanupInspection.js +187 -0
  37. package/dist/runtime/agentError.js +5 -3
  38. package/dist/runtime/agentHost.js +28 -11
  39. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  40. package/dist/runtime/codexAppServerRuntime.js +34 -3
  41. package/dist/runtime/providerControl.js +5 -1
  42. package/dist/runtime/providerRetry.js +198 -0
  43. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  44. package/dist/runtime/sessionTokenMetrics.js +15 -5
  45. package/dist/runtime/structuredProviderHost.js +6 -2
  46. package/dist/runtime/taskUsageMetrics.js +275 -0
  47. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  48. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  49. package/dist/scheduler/taskExecutionProjection.js +26 -5
  50. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  51. package/dist/storage/sqliteSchema.js +31 -0
  52. package/dist/storage/sqliteStore.js +17 -0
  53. package/dist/storage/storageVersions.js +1 -1
  54. package/dist/storage/storeRpc.js +1 -0
  55. package/dist/storage/taskCatalog.js +123 -0
  56. package/dist/storage/taskStore.js +2 -0
  57. package/dist/task/archiveDiagnostics.js +1 -0
  58. package/dist/task/archivePreflight.js +124 -0
  59. package/dist/task/publicationAdoption.js +56 -0
  60. package/dist/task/publicationReference.js +10 -0
  61. package/dist/task/remoteDelivery.js +31 -16
  62. package/dist/web/assets/client/app.js +92 -17
  63. package/dist/web/assets/client/components.js +55 -13
  64. package/dist/web/assets/client/i18n.js +72 -4
  65. package/dist/web/assets/client/taskSurface.js +2 -1
  66. package/dist/web/assets/client/view.js +35 -7
  67. package/dist/web/assets/shell.js +6 -0
  68. package/dist/web/assets/styles/layout.js +7 -0
  69. package/dist/web/webServer.js +14 -3
  70. package/dist/web/webSnapshot.js +12 -3
  71. package/dist/workspace/cleanupInspection.js +63 -0
  72. package/dist/workspace/workItemChangeSetManager.js +110 -50
  73. package/docs/agent-result-consumption.md +4 -0
  74. package/docs/agent-result-consumption.zh-CN.md +3 -0
  75. package/docs/agent-runtime-drivers.md +7 -0
  76. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  77. package/docs/architecture/README.md +2 -0
  78. package/docs/architecture/README.zh-CN.md +3 -1
  79. package/docs/architecture/capabilities-and-resources.md +30 -5
  80. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  81. package/docs/managed-turn-and-session-runtime.md +47 -0
  82. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  83. package/docs/observability/README.md +62 -0
  84. package/docs/observability/README.zh-CN.md +47 -0
  85. package/docs/project-refresh.md +77 -0
  86. package/docs/project-refresh.zh-CN.md +59 -0
  87. package/docs/provider-retry.md +70 -0
  88. package/docs/task-delivery.md +133 -13
  89. package/docs/task-delivery.zh-CN.md +99 -10
  90. package/docs/task-discovery.md +102 -0
  91. package/docs/task-discovery.zh-CN.md +86 -0
  92. package/docs/testing/verification-levels.md +16 -0
  93. package/docs/testing/verification-levels.zh-CN.md +13 -1
  94. package/i18n/README.zh-CN.md +13 -7
  95. package/package.json +1 -1
  96. package/skills/yui-leader/references/execution.md +3 -2
  97. package/skills/yui-operator/SKILL.md +13 -2
  98. package/skills/yui-reviewer/SKILL.md +4 -0
  99. package/skills/yui-runtime/SKILL.md +17 -2
  100. package/skills/yui-runtime/references/publication.md +26 -4
  101. 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(", ")}. Reopen and complete it again to record exact heads before archiving; `
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"}. Record confirmed current-head evidence with task publication upsert, `
80
- + "or use task archive --abandon for an intentional non-merge.");
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, or repeat task archive --integrated --force to explicitly "
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
- function authorizeContext(store, taskId, environment) {
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
- this.#workspacePreparer = options.workspacePreparer;
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
- routeRoleEvent(store, errorEvent, input.fence.roleName, wakeReason("agent-error", errorEvent.id), now);
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
- const binding = sessions?.providerBinding;
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
- const binding = sessions?.providerBinding;
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
- store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, settleProviderTurnSubmission(binding, { attemptId: input.attemptId, status: input.status,
1234
- reason: input.reason, resolvedAt: input.now.toISOString() }), input.now));
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
- const updated = settleProviderTurnSubmission(binding, {
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
- if (input.runId === undefined)
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 driver = run === null
1272
- ? null
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
  }