@zq-silk/yui 1.1.2 → 2.0.0

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 (64) hide show
  1. package/ARCHITECTURE.md +2 -1
  2. package/ARCHITECTURE.zh-CN.md +2 -1
  3. package/README.md +2 -0
  4. package/dist/cli/commandCatalog.js +66 -54
  5. package/dist/cli/managedDiagnostics.js +2 -1
  6. package/dist/cli/updatePorts.js +0 -5
  7. package/dist/cli.js +113 -107
  8. package/dist/commands/controllerCommands.js +2 -1
  9. package/dist/commands/globalRoleCommands.js +84 -43
  10. package/dist/commands/grantCommands.js +48 -8
  11. package/dist/commands/taskCommands.js +146 -196
  12. package/dist/commands/taskContextCommand.js +29 -6
  13. package/dist/commands/taskFactCommands.js +10 -69
  14. package/dist/commands/taskInputCommands.js +26 -35
  15. package/dist/commands/taskPublicationCommands.js +2 -35
  16. package/dist/commands/taskPublicationVerifyCommand.js +1 -2
  17. package/dist/commands/taskUpstreamCommands.js +2 -1
  18. package/dist/context/runContextPack.js +8 -29
  19. package/dist/context/sessionBootstrapManifest.js +5 -101
  20. package/dist/context/taskContext.js +98 -39
  21. package/dist/controller/clientRuntime.js +16 -8
  22. package/dist/controller/fileSchedulerStoreAdapter.js +24 -0
  23. package/dist/controller/runtime.js +6 -0
  24. package/dist/core/controllerClient.js +46 -21
  25. package/dist/core/protocol.js +12 -7
  26. package/dist/doctor/doctor.js +4 -3
  27. package/dist/errors/cliError.js +4 -4
  28. package/dist/errors/cliFailure.js +213 -0
  29. package/dist/executor/fileRoleLaunchPlanner.js +1 -4
  30. package/dist/grant/capabilityGrant.js +13 -0
  31. package/dist/grant/taskAuthorization.js +127 -0
  32. package/dist/kernel/builtinCapabilities.js +60 -8
  33. package/dist/kernel/kernelPorts.js +2 -2
  34. package/dist/output/boundedRead.js +118 -0
  35. package/dist/release/releaseWorkflowEngine.js +8 -1
  36. package/dist/runtime/managedCaller.js +2 -2
  37. package/dist/runtime/runtimeCoherence.js +10 -8
  38. package/dist/storage/sqliteSchema.js +10 -0
  39. package/dist/storage/storageVersions.js +1 -1
  40. package/dist/storage/taskStore.js +3 -0
  41. package/dist/task/leaderArchive.js +111 -0
  42. package/dist/task/leaderArchiveAuthority.js +24 -0
  43. package/dist/tmux/commandExecutor.js +7 -5
  44. package/docs/architecture/README.md +1 -0
  45. package/docs/architecture/README.zh-CN.md +1 -0
  46. package/docs/cli-information-contract.md +107 -0
  47. package/docs/cli-information-contract.zh-CN.md +81 -0
  48. package/docs/managed-turn-and-session-runtime.md +9 -0
  49. package/docs/managed-turn-and-session-runtime.zh-CN.md +6 -0
  50. package/docs/plugin-sdk.md +7 -3
  51. package/docs/plugin-sdk.zh-CN.md +5 -2
  52. package/docs/release-workflow.md +19 -9
  53. package/docs/release-workflow.zh-CN.md +14 -7
  54. package/docs/storage-baseline.md +5 -0
  55. package/docs/testing/verification-levels.md +2 -2
  56. package/i18n/README.zh-CN.md +2 -0
  57. package/package.json +1 -1
  58. package/skills/yui-leader/SKILL.md +8 -1
  59. package/skills/yui-leader/references/authorization.md +64 -0
  60. package/skills/yui-leader/references/execution.md +18 -4
  61. package/skills/yui-leader/references/task-plugins.md +4 -2
  62. package/skills/yui-operator/SKILL.md +9 -2
  63. package/skills/yui-runtime/SKILL.md +51 -9
  64. package/skills/yui-runtime/references/publication.md +3 -1
@@ -5,14 +5,15 @@ import { expandTaskMessageResult } from "../message/message.js";
5
5
  import { resolveManagedTaskReader } from "../runtime/managedCaller.js";
6
6
  import { providerRetryProjection } from "../runtime/providerRetry.js";
7
7
  import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
8
+ import { retiredTaskRecordIds, TASK_RECORD_RETIRED_EVENT } from "../task/taskRecordRetirement.js";
8
9
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
9
10
  import { contextContentDigest } from "./contextSnapshot.js";
10
11
  import { buildRunContextPack } from "./runContextPack.js";
11
12
  import { sourceRunContextValue } from "./sourceRunContext.js";
12
- const MAX_RECORDS = 256;
13
- const MAX_VALUE_BYTES = 4096;
14
- const MAX_PAGE_BYTES = 128 * 1024;
15
- const MAX_INSPECT_BYTES = 4 * 1024 * 1024;
13
+ import { boundedDocument, recordPage } from "../output/boundedRead.js";
14
+ const MAX_RECORDS = 64;
15
+ const MAX_VALUE_BYTES = 2048;
16
+ const MAX_PAGE_BYTES = 24 * 1024;
16
17
  const MAX_EVENTS = 100;
17
18
  const MAX_ATTENTION_REFS = 8;
18
19
  /** A current read model, not a second snapshot store or a delivery receipt.
@@ -21,7 +22,7 @@ const MAX_ATTENTION_REFS = 8;
21
22
  export function readTaskContext(store, taskId, environment = {}) {
22
23
  return store.readTransaction((reader) => {
23
24
  const scope = authorizeContext(reader, taskId, environment);
24
- const sources = authorizedSources(reader, scope);
25
+ const sources = authorizedSources(reader, scope, true);
25
26
  const count = sources.reduce((total, source) => total + source.count, 0);
26
27
  const cursor = currentCursor(reader, taskId);
27
28
  // Reserve a small, classified view before ordinary history consumes the
@@ -34,10 +35,12 @@ export function readTaskContext(store, taskId, environment = {}) {
34
35
  for (const entry of source.read()) {
35
36
  if (records.length >= MAX_RECORDS)
36
37
  break page;
38
+ if (records.filter(record => record.ref.store === source.family).length >= 8)
39
+ continue;
37
40
  if (entry.ref.store === "run")
38
41
  runtimeEvents ??= reader.listEventsByType(taskId, ["runtime.agent-error"]);
39
42
  const valueBytes = Buffer.byteLength(JSON.stringify(entry.value));
40
- const content = valueBytes > MAX_VALUE_BYTES
43
+ const content = valueBytes > MAX_VALUE_BYTES || ["task-message", "run", "work-item", "project-knowledge"].includes(entry.ref.store)
41
44
  ? { ref: entry.ref, summary: summarize(entry.value), omitted: true }
42
45
  : { ...entry, omitted: false };
43
46
  const record = { ...content, ...(entry.ref.store === "run" ? {
@@ -53,11 +56,20 @@ export function readTaskContext(store, taskId, environment = {}) {
53
56
  }
54
57
  return {
55
58
  taskId, coreCursor: encode(cursor), throughCursor: encode(cursor),
59
+ view: "current",
60
+ collections: Object.entries(sources.reduce((counts, source) => {
61
+ counts[source.family] = (counts[source.family] ?? 0) + source.count;
62
+ return counts;
63
+ }, {})).map(([store, total]) => ({
64
+ store, total, returned: records.filter(record => record.ref.store === store).length
65
+ })),
66
+ discovery: `task context list ${taskId} --store <store>`,
67
+ detail: `task context inspect ${taskId} --store <store> --ref <refId> --digest <digest>`,
56
68
  attention,
57
69
  records, count,
58
70
  omitted: { records: count - records.length, values: records.filter((r) => r.omitted).length },
59
71
  observations: [],
60
- limits: { records: MAX_RECORDS, valueBytes: MAX_VALUE_BYTES, pageBytes: MAX_PAGE_BYTES,
72
+ limits: { records: MAX_RECORDS, valueBytes: MAX_VALUE_BYTES, pageBytes: 32 * 1024, recordBytes: MAX_PAGE_BYTES,
61
73
  attentionRefsPerCategory: MAX_ATTENTION_REFS, attentionRefBytesPerCategory: MAX_VALUE_BYTES }
62
74
  };
63
75
  });
@@ -65,15 +77,16 @@ export function readTaskContext(store, taskId, environment = {}) {
65
77
  function summarizeAttention(store, taskId, allow) {
66
78
  const group = () => ({ count: 0, refs: [], omittedRefs: 0 });
67
79
  const attention = {
68
- openInputs: group(), pendingOperations: group(), unknownOperations: group()
80
+ openInputs: group(), pendingOperations: group(), unknownOperations: group(), messages: group()
69
81
  };
70
82
  for (const [category, family, statuses] of [
71
83
  ["openInputs", "input-request", ["open"]],
72
84
  ["pendingOperations", "job", ["queued", "running"]],
73
- ["unknownOperations", "job", ["unknown-needs-attention"]]
85
+ ["unknownOperations", "job", ["unknown-needs-attention"]],
86
+ ["messages", "task-message", undefined]
74
87
  ]) {
75
88
  const page = store.queryContextRecords(taskId, {
76
- family, statuses, ids: allowedIds(allow, family), limit: MAX_ATTENTION_REFS
89
+ family, statuses, descending: category === "messages", ids: allowedIds(allow, family), limit: MAX_ATTENTION_REFS
77
90
  });
78
91
  const summary = attention[category];
79
92
  summary.count = page.count;
@@ -152,22 +165,58 @@ export function inspectTaskContext(store, taskId, selector, environment = {}) {
152
165
  currentRef: entry.ref
153
166
  });
154
167
  }
155
- if (Buffer.byteLength(JSON.stringify(entry.value)) > MAX_INSPECT_BYTES) {
156
- throw usageError("Context value exceeds the bounded inspect limit.", undefined, { ref: entry.ref, maxBytes: MAX_INSPECT_BYTES });
157
- }
158
168
  const expansion = selector.store === "task-message"
159
169
  ? expandTaskMessageResult(value, (task, run) => reader.getRun(task, run))
160
170
  : undefined;
161
171
  const response = { ...entry, ...(expansion !== undefined && "result" in expansion
162
172
  ? { result: expansion.result } : {}),
163
173
  ...(selector.store === "run" ? { execution: runExecutionObservation(value, reader.getTaskRoleSessionSet(taskId, value.roleName)?.providerBinding, reader.listEventsByType(taskId, ["runtime.agent-error"])) } : {}),
164
- coreCursor: encode(currentCursor(reader, taskId)) };
165
- if (Buffer.byteLength(JSON.stringify(response)) > MAX_INSPECT_BYTES) {
166
- throw usageError("Expanded Context exceeds the bounded inspect limit.", undefined, { ref: entry.ref, maxBytes: MAX_INSPECT_BYTES });
174
+ };
175
+ return boundedDocument(response, `task:${taskId}/${selector.store}/${selector.refId}`, selector.cursor);
176
+ });
177
+ }
178
+ /** Discover one authorized family without copying its bodies or unrelated Task
179
+ * state. Content-version cursors fail explicitly if the collection changes.
180
+ */
181
+ export function listTaskContext(store, taskId, family, options = {}, environment = {}) {
182
+ return store.readTransaction(reader => {
183
+ const scope = authorizeContext(reader, taskId, environment);
184
+ const sources = authorizedSources(reader, scope, false, family);
185
+ if (!CONTEXT_FAMILIES.includes(family))
186
+ throw usageError(`Unknown Context store: ${family}.`);
187
+ if (options.after !== undefined && !Number.isFinite(Date.parse(options.after)))
188
+ throw usageError("Invalid --after timestamp.");
189
+ const retiredKind = family === "task-message" ? "message" : family === "run" ? "run" : undefined;
190
+ const retired = retiredKind === undefined ? undefined
191
+ : retiredTaskRecordIds(reader.listEventsByType(taskId, [TASK_RECORD_RETIRED_EVENT]), retiredKind);
192
+ const records = [];
193
+ for (const source of sources) {
194
+ for (let offset = 0; offset < source.count; offset += MAX_RECORDS) {
195
+ for (const entry of source.read(offset)) {
196
+ const value = entry.value;
197
+ if (options.status !== undefined && value.status !== options.status
198
+ || options.workItemId !== undefined && value.workItemId !== options.workItemId
199
+ || options.after !== undefined && Date.parse(String(value.createdAt)) <= Date.parse(options.after))
200
+ continue;
201
+ records.push({ ref: entry.ref, summary: summarize(value),
202
+ ...(retired === undefined ? {} : { history: retired.has(entry.ref.refId) ? "retired" : "active" }),
203
+ ...Object.fromEntries(["status", "kind", "type", "createdAt", "roleName", "assignee", "purpose", "workItemId", "reviewRoundId"]
204
+ .filter(key => typeof value[key] === "string").map(key => [key, value[key]])) });
205
+ }
206
+ }
167
207
  }
168
- return response;
208
+ return recordPage(records, `task:${taskId}/${family}/${JSON.stringify({
209
+ status: options.status, after: options.after, workItemId: options.workItemId
210
+ })}`, options);
169
211
  });
170
212
  }
213
+ export const CONTEXT_FAMILIES = [
214
+ "task", "task-brief", "remote-delivery", "role", "role-profile",
215
+ "project-policy", "project-knowledge", "input-request", "work-item",
216
+ "accepted-work-item", "candidate", "environment-preparation", "managed-workspace",
217
+ "change-set", "task-message", "run", "source-run", "review-round", "mailbox",
218
+ "task-decision", "task-milestone", "job", "publication", "task-event"
219
+ ];
171
220
  export function listContextMessages(store, taskId, environment = {}) {
172
221
  return store.readTransaction((reader) => {
173
222
  const { allow } = authorizeContext(reader, taskId, environment);
@@ -241,7 +290,7 @@ function authorizeContext(store, taskId, environment) {
241
290
  if (caller.currentRunId === undefined)
242
291
  throw usageError("A managed Role needs a current AgentRun to read its scoped Context.");
243
292
  const pack = buildRunContextPack(store, taskId, caller.currentRunId);
244
- allow = new Set(pack.authority.readableRefs.map((ref) => `${ref.store}:${ref.refId}`));
293
+ allow = new Set(pack.pointers.map((ref) => `${ref.store}:${ref.refId}`));
245
294
  allow.add(`role:${caller.roleName}`);
246
295
  allow.add(`turn:${caller.currentRunId}`);
247
296
  // Shared current user intent and this exact Assignment's live steers augment
@@ -389,31 +438,35 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
389
438
  default: return null;
390
439
  }
391
440
  }
392
- function authorizedSources(store, { task, allow, caller }) {
441
+ function authorizedSources(store, { task, allow, caller }, current = false, requestedFamily) {
393
442
  const taskId = task.id;
394
443
  const sources = [];
395
444
  const add = (family, id, value) => {
396
- if (value !== null && isAllowed(allow, family, id)) {
397
- sources.push({ count: 1, read: () => [materialize(family, id, value)] });
445
+ if ((requestedFamily === undefined || requestedFamily === family) && value !== null && isAllowed(allow, family, id)) {
446
+ sources.push({ family, count: 1, read: () => [materialize(family, id, value)] });
398
447
  }
399
448
  };
400
449
  const query = (family, options = {}) => {
450
+ if (requestedFamily !== undefined && requestedFamily !== family)
451
+ return;
401
452
  const selection = { ...options, family, ids: allowedIds(allow, family) };
402
453
  const { count } = store.queryContextRecords(taskId, { ...selection, limit: 0 });
403
- sources.push({ count, *read() {
454
+ sources.push({ family, count, *read(offset = 0) {
404
455
  for (const { id, value } of store.queryContextRecords(taskId, {
405
- ...selection, limit: MAX_RECORDS
456
+ ...selection, offset, limit: current ? 8 : MAX_RECORDS
406
457
  }).records)
407
458
  yield materialize(family, id, value);
408
459
  } });
409
460
  };
410
461
  add("task", taskId, task);
411
462
  add("task-brief", taskId, store.getTaskBrief(taskId));
412
- if (allow === undefined)
463
+ if (allow === undefined && (requestedFamily === undefined || requestedFamily === "remote-delivery")) {
413
464
  add("remote-delivery", taskId, projectTaskRemoteDeliveryFromStore(store, task));
465
+ }
414
466
  for (const role of store.listRoles(taskId)) {
415
467
  add("role", role.name, role);
416
- add("role-profile", role.name, roleProfile(role));
468
+ if (!current)
469
+ add("role-profile", role.name, roleProfile(role));
417
470
  }
418
471
  for (const binding of task.projectBindings) {
419
472
  const project = store.getProject(binding.projectId);
@@ -427,42 +480,48 @@ function authorizedSources(store, { task, allow, caller }) {
427
480
  }
428
481
  // Current responsibilities and unanswered inputs precede historical bulk.
429
482
  query("input-request", { statuses: ["open"] });
430
- query("work-item");
483
+ query("work-item", current ? { excludeStatuses: ["accepted", "retired"] } : {});
431
484
  for (const id of allowedIds(allow, "accepted-work-item") ?? []) {
432
485
  add("accepted-work-item", id, store.getWorkItem(taskId, id));
433
486
  }
434
- query("candidate");
487
+ if (!current)
488
+ query("candidate");
435
489
  // File artifacts are not enumerated into the synchronous Context listing: the
436
490
  // core cursor does not cover file edits and an ambient directory index here
437
491
  // would be the forbidden update-time mirror (§3.7). Current artifacts are
438
492
  // listed on demand via the async `artifact.list` capability; frozen Candidate
439
493
  // artifacts remain reachable as commit-pinned pointers under their Candidate.
440
- query("environment-preparation");
494
+ if (!current)
495
+ query("environment-preparation");
441
496
  query("managed-workspace");
442
497
  if (caller?.currentRunId !== undefined) {
443
498
  add("managed-workspace", `${taskId}/${caller.roleName}`, store.getRun(taskId, caller.currentRunId)?.workspace ?? null);
444
499
  }
445
- query("change-set");
500
+ if (!current)
501
+ query("change-set");
446
502
  query("task-message", { descending: true });
447
- query("run", { descending: true });
503
+ query("run", { descending: true, ...(current ? { statuses: ["active"] } : {}) });
448
504
  for (const id of allowedIds(allow, "source-turn") ?? []) {
449
505
  const run = store.getRun(taskId, id);
450
506
  if (run !== null)
451
507
  add("source-run", id, sourceRunContextValue(run));
452
508
  }
453
- query("review-round", { descending: true });
454
- query("input-request", { excludeStatuses: ["open"] });
455
- if (allow === undefined) {
509
+ query("review-round", { descending: true, ...(current ? { statuses: ["pending", "running"] } : {}) });
510
+ if (!current)
511
+ query("input-request", { excludeStatuses: ["open"] });
512
+ if (!current && allow === undefined) {
456
513
  add("mailbox", "task", store.getWorkMailbox({ kind: "task", taskId }));
457
514
  for (const role of store.listRoles(taskId)) {
458
515
  add("mailbox", `role:${role.name}`, store.getWorkMailbox({ kind: "role", taskId, roleName: role.name }));
459
516
  }
460
517
  }
461
- query("task-decision");
462
- query("task-milestone");
463
- query("job");
464
- query("publication");
465
- query("task-event", { descending: true });
518
+ query("task-decision", current ? { statuses: ["active"] } : {});
519
+ if (!current) {
520
+ query("task-milestone");
521
+ query("publication");
522
+ query("task-event", { descending: true });
523
+ }
524
+ query("job", current ? { statuses: ["queued", "running", "unknown-needs-attention"] } : {});
466
525
  return sources;
467
526
  }
468
527
  export function materialize(store, refId, value) {
@@ -512,7 +571,7 @@ function freeze(value) {
512
571
  }
513
572
  function summarize(value) {
514
573
  const record = value;
515
- return ["title", "objective", "summary", "body", "status"]
574
+ return ["title", "objective", "question", "summary", "body", "status", "type"]
516
575
  .flatMap((key) => typeof record[key] === "string" ? [`${key}: ${record[key]}`] : [])
517
576
  .join("; ").slice(0, 400);
518
577
  }
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { describeCliFailure } from "../errors/cliFailure.js";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { findLiveControllerProcessForHome, inspectLiveControllerProcess } from "../core/controllerProcessIdentity.js";
4
5
  import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
@@ -77,7 +78,7 @@ export async function ensureFileTaskController(home, options = {}) {
77
78
  ? findLiveControllerProcessForHome(readHomeFilesystemId(home))
78
79
  : undefined;
79
80
  const startupPid = existing?.pid
80
- ?? spawnController(home, controllerSpawnEnvironment(home, options.environment ?? process.env));
81
+ ?? await spawnController(home, controllerSpawnEnvironment(home, options.environment ?? process.env));
81
82
  const deadline = Date.now() + timeoutMs;
82
83
  for (;;) {
83
84
  try {
@@ -129,15 +130,15 @@ function assertControllerRuntimeProcessIdentity(value, expected) {
129
130
  || actual.controllerProtocolVersion !== expected.controllerProtocolVersion
130
131
  || actual.storageVersion !== expected.storageVersion
131
132
  || actual.minimumStorageVersion !== expected.minimumStorageVersion) {
132
- throw new Error("Authenticated Controller runtime identity does not match the captured executable, argv, package, protocol and storage versions; refusing readiness.");
133
+ throw new ControllerClientError("CONTROLLER_IDENTITY_MISMATCH", "Authenticated Controller runtime identity does not match the captured executable, argv, package, protocol and storage versions; refusing readiness.");
133
134
  }
134
135
  }
135
136
  function assertCompatibleControllerStatus(status, expectedIdentity) {
136
137
  const statusRecord = isJsonRecord(status) && status.running === true ? status : null;
137
138
  const actual = statusRecord?.protocolVersion;
138
139
  if (statusRecord === null || actual !== FILE_TASK_CONTROLLER_PROTOCOL_VERSION) {
139
- throw new Error(`Controller protocol is incompatible (expected ${FILE_TASK_CONTROLLER_PROTOCOL_VERSION}, found ${typeof actual === "number" ? actual : "unknown"}). `
140
- + "Run `yui controller restart` before writing new task records.");
140
+ throw new ControllerClientError("CONTROLLER_PROTOCOL_MISMATCH", `Controller protocol is incompatible (expected ${FILE_TASK_CONTROLLER_PROTOCOL_VERSION}, found ${typeof actual === "number" ? actual : "unknown"}). `
141
+ + "Verify Home, CLI and Controller identity before an authorized restart.");
141
142
  }
142
143
  const actualVersion = statusRecord.version;
143
144
  const current = yuiVersionIdentity();
@@ -146,14 +147,21 @@ function assertCompatibleControllerStatus(status, expectedIdentity) {
146
147
  version: typeof actualVersion === "string" ? actualVersion : current.version
147
148
  } : parseControllerIdentity(expectedIdentity));
148
149
  }
149
- function spawnDetachedFileTaskController(_home, environment) {
150
+ async function spawnDetachedFileTaskController(_home, environment) {
150
151
  const child = spawn(process.execPath, [fileURLToPath(new URL("./controllerMain.js", import.meta.url))], {
151
152
  env: environment,
152
153
  detached: true,
153
154
  stdio: "ignore"
154
155
  });
155
- child.unref();
156
- return child.pid;
156
+ // Route asynchronous spawn failures back through the command's normal error
157
+ // boundary instead of Node's unhandled "error" event / raw stack output.
158
+ return await new Promise((resolve, reject) => {
159
+ child.once("error", error => reject(new Error("Controller process could not start.", { cause: error })));
160
+ child.once("spawn", () => {
161
+ child.unref();
162
+ resolve(child.pid);
163
+ });
164
+ });
157
165
  }
158
166
  /** Stops the per-home Controller and waits until its owned discovery is gone. */
159
167
  export async function stopFileTaskController(home, options = {}) {
@@ -398,7 +406,7 @@ function classifyRefreshFailure(error, options) {
398
406
  options.onError?.(error);
399
407
  return {
400
408
  status: "failed",
401
- message: error instanceof Error ? error.message : String(error)
409
+ message: describeCliFailure(error).message
402
410
  };
403
411
  }
404
412
  /** Foreground command bridge. It never reads or writes Agent terminal bytes. */
@@ -6,6 +6,7 @@ import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
6
6
  import { interruptThenTerminalState, prepareMessageContinuations } from "../message/messageContinuation.js";
7
7
  import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
8
8
  import { deferProviderRetry, providerRetryPending, providerRetryProjection, recordProviderFailure } from "../runtime/providerRetry.js";
9
+ import { createTaskMessage } from "../message/message.js";
9
10
  import { AgentHostObservationDeferred, recordAgentHostConnection, resolveAgentHostObservation } from "./agentHostObservation.js";
10
11
  import { admitOwnedProviderInput, ownedProviderInput } from "./providerRetryAdmission.js";
11
12
  import { RuntimeHookRunFenceError } from "./runtimeHookRunFence.js";
@@ -1243,6 +1244,29 @@ export class FileSchedulerStoreAdapter {
1243
1244
  }
1244
1245
  }
1245
1246
  binding = admitOwnedProviderInput(store, binding, input);
1247
+ // The Host's human console is a user ingress. Persist its original text
1248
+ // before the Provider write; provider-visible userMessage items alone
1249
+ // are NOT proof of human authorship (managed prompts use those too).
1250
+ // This shares the exact writer/session admission and its transaction.
1251
+ if (input.authorityOwner === "human" && input.roleName === "leader"
1252
+ && input.runId === undefined && input.boundedText !== undefined) {
1253
+ const prior = store.listEvents(input.taskId).find(event => event.type === "message.native-user-input" && event.payload.attemptId === input.attemptId);
1254
+ if (prior !== undefined) {
1255
+ const message = store.listMessages(input.taskId).find(entry => entry.id === prior.payload.messageId);
1256
+ if (message?.body !== input.boundedText || prior.payload.nativeSessionId !== input.nativeSessionId) {
1257
+ throw new AgentHostProviderTurnFenceError("Native user input identity was reused with different content.");
1258
+ }
1259
+ }
1260
+ else {
1261
+ const message = createTaskMessage(store.nextMessageId(input.taskId), input.taskId, input.boundedText, "user", { type: "user" }, input.now, { intent: "record" });
1262
+ store.saveMessage(input.taskId, message);
1263
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "message.native-user-input", {
1264
+ messageId: message.id, attemptId: input.attemptId, nativeSessionId: input.nativeSessionId,
1265
+ authorityEpoch: String(input.authorityEpoch), holderId: input.holderId
1266
+ }, input.now));
1267
+ // No mailbox enqueue: this very input is already being delivered.
1268
+ }
1269
+ }
1246
1270
  store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, beginProviderTurn(binding, {
1247
1271
  ...(input.runId === undefined ? {} : { runId: input.runId }),
1248
1272
  attemptId: input.attemptId,
@@ -1,4 +1,6 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
+ import { archiveLeaderTask } from "../task/leaderArchive.js";
3
+ import { TaskWorkspaceCoordinator } from "../repository/taskWorkspaceCoordinator.js";
2
4
  import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
3
5
  import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
4
6
  import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveRuntimeHealth, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
@@ -359,6 +361,10 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
359
361
  // the poll interval.
360
362
  const kernel = createKernelPorts(store, createLinuxProcessPort(), (taskId) => {
361
363
  runningRuntime?.signal(`task:${taskId}`);
364
+ }, [], async (taskId, environment, request) => {
365
+ if (!(workspacePreparer instanceof FileTaskWorkspacePreparer))
366
+ throw new Error("Archive workspace owner unavailable.");
367
+ return archiveLeaderTask(store, new TaskWorkspaceCoordinator(store, workspacePreparer, webWorkflow), taskId, environment, request);
362
368
  });
363
369
  closeKernel = () => kernel.close();
364
370
  const webWorkflow = new FileTaskWorkflowRuntime(home, store, schedulerStore, planner, tmux, workspacePreparer, {
@@ -8,9 +8,11 @@ import { readHomeFilesystemId } from "./homeFilesystemIdentity.js";
8
8
  import { CONTROLLER_DISCOVERY_PATH, ControllerProtocolError, encodeControllerRequest, FILE_TASK_CONTROLLER_PROTOCOL_VERSION, MAX_CONTROLLER_MESSAGE_BYTES, parseControllerDiscovery, parseControllerResponse } from "./protocol.js";
9
9
  export class ControllerClientError extends Error {
10
10
  code;
11
- constructor(code, message, cause) {
11
+ diagnostic;
12
+ constructor(code, message, cause, diagnostic) {
12
13
  super(message, { cause });
13
14
  this.code = code;
15
+ this.diagnostic = diagnostic;
14
16
  this.name = "ControllerClientError";
15
17
  }
16
18
  }
@@ -33,12 +35,14 @@ export async function readControllerDiscovery(home) {
33
35
  try {
34
36
  const metadata = await lstat(discoveryPath);
35
37
  const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
36
- if (!metadata.isFile()
37
- || (metadata.mode & 0o077) !== 0
38
- || metadata.size > 4_096
39
- || (uid !== undefined && metadata.uid !== uid)) {
40
- throw invalidDiscovery();
41
- }
38
+ if (!metadata.isFile())
39
+ throw invalidDiscovery(new Error("Discovery path is not a regular file."));
40
+ if ((metadata.mode & 0o077) !== 0)
41
+ throw invalidDiscovery(new Error("Discovery record permissions are not owner-only."));
42
+ if (metadata.size > 4_096)
43
+ throw invalidDiscovery(new Error("Discovery record exceeds the 4096-byte limit."));
44
+ if (uid !== undefined && metadata.uid !== uid)
45
+ throw invalidDiscovery(new Error("Discovery record belongs to a different user."));
42
46
  const value = JSON.parse(await readFile(discoveryPath, "utf8"));
43
47
  const socketPath = discoverySocketPath(value);
44
48
  return parseControllerDiscovery(value, {
@@ -50,7 +54,7 @@ export async function readControllerDiscovery(home) {
50
54
  if (error instanceof ControllerClientError)
51
55
  throw error;
52
56
  if (isNodeError(error) && error.code === "ENOENT") {
53
- throw new ControllerClientError("CONTROLLER_NOT_RUNNING", withOriginalError("Controller is not running.", error), error);
57
+ throw new ControllerClientError("CONTROLLER_NOT_RUNNING", withOriginalError("Controller discovery record is missing; process state is unverified.", error), error, { delivery: "not-sent" });
54
58
  }
55
59
  if (isNodeError(error) && (error.code === "EPERM" || error.code === "EACCES")) {
56
60
  throw controllerAccessDenied(error, "discovery record");
@@ -72,12 +76,22 @@ function discoverySocketPath(value) {
72
76
  return socketPath;
73
77
  }
74
78
  export async function callController(home, method, params = {}, options = {}) {
75
- const discovery = await readControllerDiscovery(home);
79
+ const id = options.id ?? randomUUID();
80
+ let discovery;
81
+ try {
82
+ discovery = await readControllerDiscovery(home);
83
+ }
84
+ catch (error) {
85
+ if (!(error instanceof ControllerClientError))
86
+ throw error;
87
+ throw new ControllerClientError(error.code, error.message, error.cause, {
88
+ delivery: "not-sent", method, requestId: id
89
+ });
90
+ }
76
91
  const timeoutMs = options.timeoutMs ?? 5_000;
77
92
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
78
93
  throw new TypeError("Controller timeout must be a positive integer.");
79
94
  }
80
- const id = options.id ?? randomUUID();
81
95
  let requestLine;
82
96
  try {
83
97
  requestLine = encodeControllerRequest({
@@ -93,11 +107,13 @@ export async function callController(home, method, params = {}, options = {}) {
93
107
  }
94
108
  catch (error) {
95
109
  if (error instanceof ControllerProtocolError) {
96
- throw new ControllerClientError(error.code, error.message, error);
110
+ throw new ControllerClientError(error.code, error.message, error, {
111
+ delivery: "not-sent", method, requestId: id
112
+ });
97
113
  }
98
- throw new ControllerClientError("INVALID_REQUEST", withOriginalError("Invalid controller request.", error), error);
114
+ throw new ControllerClientError("INVALID_REQUEST", withOriginalError("Invalid controller request.", error), error, { delivery: "not-sent", method, requestId: id });
99
115
  }
100
- return exchange(discovery.socketPath, requestLine, id, timeoutMs);
116
+ return exchange(discovery.socketPath, requestLine, id, timeoutMs, method);
101
117
  }
102
118
  /**
103
119
  * Explicit restart recovery for an exact Controller whose discovery record
@@ -151,7 +167,7 @@ export async function stopOrphanedFileTaskController(home, timeoutMs, options =
151
167
  }
152
168
  return Object.freeze({ pid: candidate.pid });
153
169
  }
154
- function exchange(socketPath, requestLine, expectedId, timeoutMs) {
170
+ function exchange(socketPath, requestLine, expectedId, timeoutMs, method) {
155
171
  return new Promise((resolve, reject) => {
156
172
  const socket = createConnection(socketPath);
157
173
  let buffer = Buffer.alloc(0);
@@ -174,7 +190,12 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
174
190
  settled = true;
175
191
  clearTimeout(timer);
176
192
  socket.destroy();
177
- reject(error);
193
+ reject(new ControllerClientError(error.code, error.message, error.cause, {
194
+ delivery: error.diagnostic?.delivery ?? (deliveryStarted ? "unconfirmed" : "not-sent"),
195
+ method,
196
+ requestId: expectedId,
197
+ socketPath
198
+ }));
178
199
  };
179
200
  socket.on("connect", () => {
180
201
  // Once write begins, a missing response cannot prove that the Controller
@@ -202,7 +223,9 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
202
223
  if (response.ok)
203
224
  finish(response.result);
204
225
  else
205
- fail(new ControllerClientError(response.error.code, response.error.message));
226
+ fail(new ControllerClientError(response.error.code, response.error.message, undefined, {
227
+ delivery: "response-received"
228
+ }));
206
229
  }
207
230
  catch (error) {
208
231
  fail(invalidResponse(error));
@@ -219,22 +242,22 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
219
242
  }
220
243
  export function classifyControllerSocketError(error, deliveryStarted) {
221
244
  if (deliveryStarted) {
222
- return new ControllerClientError("CONTROLLER_DELIVERY_UNKNOWN", withOriginalError("Controller request delivery is unknown.", error), error);
245
+ return new ControllerClientError("CONTROLLER_DELIVERY_UNKNOWN", withOriginalError("Controller request delivery is unknown.", error), error, { delivery: "unconfirmed" });
223
246
  }
224
247
  if (isNodeError(error) && (error.code === "EPERM" || error.code === "EACCES")) {
225
248
  return controllerAccessDenied(error, "socket");
226
249
  }
227
- return new ControllerClientError("CONTROLLER_UNAVAILABLE", withOriginalError("Controller is unavailable.", error), error);
250
+ return new ControllerClientError("CONTROLLER_UNAVAILABLE", withOriginalError("Controller is unavailable.", error), error, { delivery: "not-sent" });
228
251
  }
229
252
  function controllerAccessDenied(error, target) {
230
253
  return new ControllerClientError("CONTROLLER_ACCESS_DENIED", `Controller ${target} access was denied (${error.code}) before any request was sent. `
231
254
  + "In Codex, request sandbox escalation to rerun the same Yui CLI command outside the sandbox "
232
- + `if authorized. If escalation is denied, report that to the user.${originalErrorDetail(error)}`, error);
255
+ + `if authorized. If escalation is denied, report that to the user.${originalErrorDetail(error)}`, error, { delivery: "not-sent" });
233
256
  }
234
257
  function invalidDiscovery(error) {
235
258
  return new ControllerClientError("CONTROLLER_DISCOVERY_INVALID", error === undefined
236
259
  ? "Controller discovery is invalid."
237
- : withOriginalError("Controller discovery is invalid.", error), error);
260
+ : withOriginalError("Controller discovery is invalid.", error), error, { delivery: "not-sent" });
238
261
  }
239
262
  function invalidResponse(error) {
240
263
  return new ControllerClientError("INVALID_RESPONSE", error === undefined
@@ -248,7 +271,9 @@ function originalErrorDetail(error) {
248
271
  const code = isNodeError(error) && typeof error.code === "string"
249
272
  ? ` (${error.code})`
250
273
  : "";
251
- return ` Original error${code}: ${error instanceof Error ? error.message : String(error)}`;
274
+ return ` Original error${code}: ${error instanceof SyntaxError
275
+ ? "Input could not be parsed; raw input omitted."
276
+ : error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown cause."}`;
252
277
  }
253
278
  function isNodeError(error) {
254
279
  return error instanceof Error && "code" in error;
@@ -129,11 +129,16 @@ export function parseControllerDiscovery(value, expected) {
129
129
  "processStartIdentity",
130
130
  "socketPath",
131
131
  "token"
132
- ])
133
- || value.schemaVersion !== 1
134
- || value.protocolVersion !== FILE_TASK_CONTROLLER_PROTOCOL_VERSION
135
- || !isHomeId(value.homeId)
136
- || value.homeFilesystemId !== expected.homeFilesystemId
132
+ ])) {
133
+ throw new ControllerProtocolError("CONTROLLER_DISCOVERY_INVALID", "Controller discovery fields are invalid.");
134
+ }
135
+ if (value.schemaVersion !== 1 || value.protocolVersion !== FILE_TASK_CONTROLLER_PROTOCOL_VERSION) {
136
+ throw new ControllerProtocolError("CONTROLLER_DISCOVERY_INVALID", "Controller discovery schema/protocol version does not match this CLI; verify the matching installation.");
137
+ }
138
+ if (value.homeFilesystemId !== expected.homeFilesystemId || value.socketPath !== expected.socketPath) {
139
+ throw new ControllerProtocolError("CONTROLLER_DISCOVERY_INVALID", "Controller discovery Home/socket identity does not match this invocation.");
140
+ }
141
+ if (!isHomeId(value.homeId)
137
142
  || !isHomeFilesystemId(value.homeFilesystemId)
138
143
  || typeof value.controllerInstanceId !== "string"
139
144
  || !/^[a-f0-9]{32}$/u.test(value.controllerInstanceId)
@@ -141,10 +146,10 @@ export function parseControllerDiscovery(value, expected) {
141
146
  || value.pid < 1
142
147
  || typeof value.processStartIdentity !== "string"
143
148
  || !/^[0-9]{1,32}$/u.test(value.processStartIdentity)
144
- || value.socketPath !== expected.socketPath
149
+ || typeof value.socketPath !== "string"
145
150
  || typeof value.token !== "string"
146
151
  || !/^[a-f0-9]{64}$/u.test(value.token)) {
147
- throw new ControllerProtocolError("CONTROLLER_DISCOVERY_INVALID", "Controller discovery is invalid.");
152
+ throw new ControllerProtocolError("CONTROLLER_DISCOVERY_INVALID", "Controller discovery process, Home or authentication identity is invalid; raw record omitted.");
148
153
  }
149
154
  return Object.freeze({
150
155
  schemaVersion: 1,
@@ -7,6 +7,7 @@ import { operationalAgentEnvironment } from "../agent/launchEnvironment.js";
7
7
  import { resolveTmuxBin } from "../config/yuiConfig.js";
8
8
  import { compileRoleSessionContext } from "../context/roleSessionContext.js";
9
9
  import { callController, ControllerClientError } from "../core/controllerClient.js";
10
+ import { describeCliFailure } from "../errors/cliFailure.js";
10
11
  import { EPHEMERAL_DOMAIN_GRACE_MS, readEphemeralDomainIdentity, readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
11
12
  import { usageError } from "../errors/cliError.js";
12
13
  import { inspectAgentCapabilities, resolveAgentAdapter } from "../executor/agentAdapter.js";
@@ -162,19 +163,19 @@ export async function checkController(home, probe = callController) {
162
163
  }
163
164
  catch (error) {
164
165
  if (error instanceof ControllerClientError && error.code === "CONTROLLER_NOT_RUNNING") {
165
- return { name: "controller", status: "missing", detail: `${error.message} Run yui start.` };
166
+ return { name: "controller", status: "missing", detail: describeCliFailure(error).message };
166
167
  }
167
168
  if (error instanceof ControllerClientError && error.code === "CONTROLLER_UNAVAILABLE") {
168
169
  return {
169
170
  name: "controller",
170
171
  status: "invalid",
171
- detail: `Controller is not reachable: ${error.message} Run yui start. Its process state is unverified.`
172
+ detail: describeCliFailure(error).message
172
173
  };
173
174
  }
174
175
  return {
175
176
  name: "controller",
176
177
  status: "invalid",
177
- detail: `Controller status could not be verified: ${error instanceof Error ? error.message : String(error)}. Inspect yui controller status --verbose.`
178
+ detail: describeCliFailure(error).message
178
179
  };
179
180
  }
180
181
  }
@@ -11,8 +11,8 @@ export class CliError extends Error {
11
11
  helpText;
12
12
  details;
13
13
  exitCode;
14
- constructor(code, message, helpText, details = {}) {
15
- super(message);
14
+ constructor(code, message, helpText, details = {}, options) {
15
+ super(message, options);
16
16
  this.code = code;
17
17
  this.helpText = helpText;
18
18
  this.details = details;
@@ -35,6 +35,6 @@ export function agentNotFound(id) {
35
35
  export function dataError(message) {
36
36
  return new CliError("DATA_ERROR", message);
37
37
  }
38
- export function runtimeError(message) {
39
- return new CliError("RUNTIME_ERROR", message);
38
+ export function runtimeError(message, options) {
39
+ return new CliError("RUNTIME_ERROR", message, undefined, {}, options);
40
40
  }