@zq-silk/yui 1.1.2 → 2.1.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 (115) 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 +344 -324
  5. package/dist/cli/commandDiscovery.js +69 -0
  6. package/dist/cli/completion.js +22 -227
  7. package/dist/cli/dynamicCompletion.js +12 -6
  8. package/dist/cli/invocationAuthority.js +2 -2
  9. package/dist/cli/invocationRouter.js +18 -7
  10. package/dist/cli/managedDiagnostics.js +2 -1
  11. package/dist/cli/updatePorts.js +0 -5
  12. package/dist/cli.js +184 -130
  13. package/dist/commands/capabilityCommands.js +5 -5
  14. package/dist/commands/controllerCommands.js +2 -1
  15. package/dist/commands/globalRoleCommands.js +86 -44
  16. package/dist/commands/grantCommands.js +48 -8
  17. package/dist/commands/taskCommands.js +157 -196
  18. package/dist/commands/taskContextCommand.js +29 -6
  19. package/dist/commands/taskFactCommands.js +10 -69
  20. package/dist/commands/taskInputCommands.js +26 -35
  21. package/dist/commands/taskPublicationCommands.js +2 -35
  22. package/dist/commands/taskPublicationVerifyCommand.js +1 -2
  23. package/dist/commands/taskUpstreamCommands.js +2 -1
  24. package/dist/context/runContextPack.js +8 -29
  25. package/dist/context/sessionBootstrapManifest.js +5 -101
  26. package/dist/context/taskContext.js +104 -41
  27. package/dist/controller/clientRuntime.js +16 -8
  28. package/dist/controller/fileSchedulerStoreAdapter.js +24 -0
  29. package/dist/controller/runtime.js +6 -0
  30. package/dist/core/controllerClient.js +46 -21
  31. package/dist/core/protocol.js +12 -7
  32. package/dist/doctor/doctor.js +4 -3
  33. package/dist/errors/cliError.js +4 -4
  34. package/dist/errors/cliFailure.js +213 -0
  35. package/dist/executor/fileRoleLaunchPlanner.js +1 -4
  36. package/dist/grant/capabilityGrant.js +13 -0
  37. package/dist/grant/taskAuthorization.js +127 -0
  38. package/dist/kernel/accessAssessment.js +1 -0
  39. package/dist/kernel/builtinCapabilities.js +88 -9
  40. package/dist/kernel/capabilityRegistry.js +64 -19
  41. package/dist/kernel/kernelPorts.js +2 -2
  42. package/dist/nativeAgent/agent.js +173 -0
  43. package/dist/nativeAgent/contracts.js +1 -0
  44. package/dist/nativeAgent/demo.js +29 -0
  45. package/dist/nativeAgent/index.js +3 -0
  46. package/dist/nativeAgent/mockProvider.js +43 -0
  47. package/dist/nativeAgent/textTools.js +156 -0
  48. package/dist/nativeAgent/validation.js +95 -0
  49. package/dist/output/boundedRead.js +118 -0
  50. package/dist/plugins/pluginService.js +98 -8
  51. package/dist/release/releaseWorkflowEngine.js +8 -1
  52. package/dist/resources/projectResourceService.js +23 -1
  53. package/dist/runtime/managedCaller.js +2 -2
  54. package/dist/runtime/managedIdentity.js +6 -0
  55. package/dist/runtime/runtimeCoherence.js +10 -8
  56. package/dist/storage/sqliteSchema.js +10 -0
  57. package/dist/storage/storageVersions.js +1 -1
  58. package/dist/storage/taskStore.js +3 -0
  59. package/dist/surface/surfaceContributions.js +3 -0
  60. package/dist/task/leaderArchive.js +111 -0
  61. package/dist/task/leaderArchiveAuthority.js +24 -0
  62. package/dist/task/taskAuthority.js +5 -10
  63. package/dist/tmux/commandExecutor.js +7 -5
  64. package/dist/web/assets/assetManifest.js +38 -22
  65. package/dist/web/assets/client/api.js +115 -0
  66. package/dist/web/assets/client/app.js +555 -863
  67. package/dist/web/assets/client/components.js +323 -897
  68. package/dist/web/assets/client/detail.js +333 -0
  69. package/dist/web/assets/client/dock.js +225 -0
  70. package/dist/web/assets/client/dom.js +55 -3
  71. package/dist/web/assets/client/evidence.js +172 -0
  72. package/dist/web/assets/client/format.js +25 -14
  73. package/dist/web/assets/client/forms.js +230 -0
  74. package/dist/web/assets/client/i18n.js +1020 -791
  75. package/dist/web/assets/client/overview.js +115 -0
  76. package/dist/web/assets/client/records.js +63 -0
  77. package/dist/web/assets/client/sections.js +313 -0
  78. package/dist/web/assets/client/sidebar.js +117 -0
  79. package/dist/web/assets/client/theme.js +61 -22
  80. package/dist/web/assets/icons.js +39 -0
  81. package/dist/web/assets/shell.js +124 -118
  82. package/dist/web/assets/styles/base.js +41 -0
  83. package/dist/web/assets/styles/components.js +167 -0
  84. package/dist/web/assets/styles/layout.js +49 -77
  85. package/dist/web/assets/styles/markdown.js +16 -24
  86. package/dist/web/assets/styles/responsive.js +33 -44
  87. package/dist/web/assets/styles/tokens.js +65 -89
  88. package/dist/web/assets/styles/views.js +289 -0
  89. package/dist/web/webServer.js +2 -1
  90. package/docs/architecture/README.md +1 -0
  91. package/docs/architecture/README.zh-CN.md +1 -0
  92. package/docs/cli-information-contract.md +107 -0
  93. package/docs/cli-information-contract.zh-CN.md +81 -0
  94. package/docs/managed-turn-and-session-runtime.md +9 -0
  95. package/docs/managed-turn-and-session-runtime.zh-CN.md +6 -0
  96. package/docs/plugin-sdk.md +7 -3
  97. package/docs/plugin-sdk.zh-CN.md +5 -2
  98. package/docs/release-workflow.md +42 -9
  99. package/docs/release-workflow.zh-CN.md +30 -7
  100. package/docs/storage-baseline.md +5 -0
  101. package/docs/testing/verification-levels.md +2 -2
  102. package/i18n/README.zh-CN.md +2 -0
  103. package/package.json +1 -1
  104. package/skills/yui-leader/SKILL.md +8 -1
  105. package/skills/yui-leader/references/authorization.md +64 -0
  106. package/skills/yui-leader/references/execution.md +18 -4
  107. package/skills/yui-leader/references/task-plugins.md +4 -2
  108. package/skills/yui-operator/SKILL.md +9 -2
  109. package/skills/yui-runtime/SKILL.md +77 -9
  110. package/skills/yui-runtime/references/publication.md +3 -1
  111. package/dist/web/assets/client/taskSummary.js +0 -350
  112. package/dist/web/assets/client/taskSurface.js +0 -616
  113. package/dist/web/assets/client/view.js +0 -608
  114. package/dist/web/assets/styles/cards.js +0 -247
  115. package/dist/web/assets/styles/widgets.js +0 -168
@@ -3,16 +3,18 @@ import { isGitArtifactRefString, parseGitArtifactRef } from "../artifacts/gitArt
3
3
  import { taskNotFound, usageError } from "../errors/cliError.js";
4
4
  import { expandTaskMessageResult } from "../message/message.js";
5
5
  import { resolveManagedTaskReader } from "../runtime/managedCaller.js";
6
+ import { hasManagedIdentity } from "../runtime/managedIdentity.js";
6
7
  import { providerRetryProjection } from "../runtime/providerRetry.js";
7
8
  import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
9
+ import { retiredTaskRecordIds, TASK_RECORD_RETIRED_EVENT } from "../task/taskRecordRetirement.js";
8
10
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
9
11
  import { contextContentDigest } from "./contextSnapshot.js";
10
12
  import { buildRunContextPack } from "./runContextPack.js";
11
13
  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;
14
+ import { boundedDocument, recordPage } from "../output/boundedRead.js";
15
+ const MAX_RECORDS = 64;
16
+ const MAX_VALUE_BYTES = 2048;
17
+ const MAX_PAGE_BYTES = 24 * 1024;
16
18
  const MAX_EVENTS = 100;
17
19
  const MAX_ATTENTION_REFS = 8;
18
20
  /** A current read model, not a second snapshot store or a delivery receipt.
@@ -21,7 +23,7 @@ const MAX_ATTENTION_REFS = 8;
21
23
  export function readTaskContext(store, taskId, environment = {}) {
22
24
  return store.readTransaction((reader) => {
23
25
  const scope = authorizeContext(reader, taskId, environment);
24
- const sources = authorizedSources(reader, scope);
26
+ const sources = authorizedSources(reader, scope, true);
25
27
  const count = sources.reduce((total, source) => total + source.count, 0);
26
28
  const cursor = currentCursor(reader, taskId);
27
29
  // Reserve a small, classified view before ordinary history consumes the
@@ -34,10 +36,12 @@ export function readTaskContext(store, taskId, environment = {}) {
34
36
  for (const entry of source.read()) {
35
37
  if (records.length >= MAX_RECORDS)
36
38
  break page;
39
+ if (records.filter(record => record.ref.store === source.family).length >= 8)
40
+ continue;
37
41
  if (entry.ref.store === "run")
38
42
  runtimeEvents ??= reader.listEventsByType(taskId, ["runtime.agent-error"]);
39
43
  const valueBytes = Buffer.byteLength(JSON.stringify(entry.value));
40
- const content = valueBytes > MAX_VALUE_BYTES
44
+ const content = valueBytes > MAX_VALUE_BYTES || ["task-message", "run", "work-item", "project-knowledge"].includes(entry.ref.store)
41
45
  ? { ref: entry.ref, summary: summarize(entry.value), omitted: true }
42
46
  : { ...entry, omitted: false };
43
47
  const record = { ...content, ...(entry.ref.store === "run" ? {
@@ -53,11 +57,24 @@ export function readTaskContext(store, taskId, environment = {}) {
53
57
  }
54
58
  return {
55
59
  taskId, coreCursor: encode(cursor), throughCursor: encode(cursor),
60
+ view: "current",
61
+ collections: Object.entries(sources.reduce((counts, source) => {
62
+ counts[source.family] = (counts[source.family] ?? 0) + source.count;
63
+ return counts;
64
+ }, {})).map(([store, total]) => ({
65
+ store, total, returned: records.filter(record => record.ref.store === store).length
66
+ })),
67
+ discovery: `task context list ${taskId} --store <store>`,
68
+ commandDiscovery: {
69
+ help: "yui help", describe: "yui --json help",
70
+ note: "Help and completion share the current caller's offline command view. Follow that view for operation-specific discovery; execution always revalidates authority."
71
+ },
72
+ detail: `task context inspect ${taskId} --store <store> --ref <refId> --digest <digest>`,
56
73
  attention,
57
74
  records, count,
58
75
  omitted: { records: count - records.length, values: records.filter((r) => r.omitted).length },
59
76
  observations: [],
60
- limits: { records: MAX_RECORDS, valueBytes: MAX_VALUE_BYTES, pageBytes: MAX_PAGE_BYTES,
77
+ limits: { records: MAX_RECORDS, valueBytes: MAX_VALUE_BYTES, pageBytes: 32 * 1024, recordBytes: MAX_PAGE_BYTES,
61
78
  attentionRefsPerCategory: MAX_ATTENTION_REFS, attentionRefBytesPerCategory: MAX_VALUE_BYTES }
62
79
  };
63
80
  });
@@ -65,15 +82,16 @@ export function readTaskContext(store, taskId, environment = {}) {
65
82
  function summarizeAttention(store, taskId, allow) {
66
83
  const group = () => ({ count: 0, refs: [], omittedRefs: 0 });
67
84
  const attention = {
68
- openInputs: group(), pendingOperations: group(), unknownOperations: group()
85
+ openInputs: group(), pendingOperations: group(), unknownOperations: group(), messages: group()
69
86
  };
70
87
  for (const [category, family, statuses] of [
71
88
  ["openInputs", "input-request", ["open"]],
72
89
  ["pendingOperations", "job", ["queued", "running"]],
73
- ["unknownOperations", "job", ["unknown-needs-attention"]]
90
+ ["unknownOperations", "job", ["unknown-needs-attention"]],
91
+ ["messages", "task-message", undefined]
74
92
  ]) {
75
93
  const page = store.queryContextRecords(taskId, {
76
- family, statuses, ids: allowedIds(allow, family), limit: MAX_ATTENTION_REFS
94
+ family, statuses, descending: category === "messages", ids: allowedIds(allow, family), limit: MAX_ATTENTION_REFS
77
95
  });
78
96
  const summary = attention[category];
79
97
  summary.count = page.count;
@@ -152,22 +170,58 @@ export function inspectTaskContext(store, taskId, selector, environment = {}) {
152
170
  currentRef: entry.ref
153
171
  });
154
172
  }
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
173
  const expansion = selector.store === "task-message"
159
174
  ? expandTaskMessageResult(value, (task, run) => reader.getRun(task, run))
160
175
  : undefined;
161
176
  const response = { ...entry, ...(expansion !== undefined && "result" in expansion
162
177
  ? { result: expansion.result } : {}),
163
178
  ...(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 });
179
+ };
180
+ return boundedDocument(response, `task:${taskId}/${selector.store}/${selector.refId}`, selector.cursor);
181
+ });
182
+ }
183
+ /** Discover one authorized family without copying its bodies or unrelated Task
184
+ * state. Content-version cursors fail explicitly if the collection changes.
185
+ */
186
+ export function listTaskContext(store, taskId, family, options = {}, environment = {}) {
187
+ return store.readTransaction(reader => {
188
+ const scope = authorizeContext(reader, taskId, environment);
189
+ const sources = authorizedSources(reader, scope, false, family);
190
+ if (!CONTEXT_FAMILIES.includes(family))
191
+ throw usageError(`Unknown Context store: ${family}.`);
192
+ if (options.after !== undefined && !Number.isFinite(Date.parse(options.after)))
193
+ throw usageError("Invalid --after timestamp.");
194
+ const retiredKind = family === "task-message" ? "message" : family === "run" ? "run" : undefined;
195
+ const retired = retiredKind === undefined ? undefined
196
+ : retiredTaskRecordIds(reader.listEventsByType(taskId, [TASK_RECORD_RETIRED_EVENT]), retiredKind);
197
+ const records = [];
198
+ for (const source of sources) {
199
+ for (let offset = 0; offset < source.count; offset += MAX_RECORDS) {
200
+ for (const entry of source.read(offset)) {
201
+ const value = entry.value;
202
+ if (options.status !== undefined && value.status !== options.status
203
+ || options.workItemId !== undefined && value.workItemId !== options.workItemId
204
+ || options.after !== undefined && Date.parse(String(value.createdAt)) <= Date.parse(options.after))
205
+ continue;
206
+ records.push({ ref: entry.ref, summary: summarize(value),
207
+ ...(retired === undefined ? {} : { history: retired.has(entry.ref.refId) ? "retired" : "active" }),
208
+ ...Object.fromEntries(["status", "kind", "type", "createdAt", "roleName", "assignee", "purpose", "workItemId", "reviewRoundId"]
209
+ .filter(key => typeof value[key] === "string").map(key => [key, value[key]])) });
210
+ }
211
+ }
167
212
  }
168
- return response;
213
+ return recordPage(records, `task:${taskId}/${family}/${JSON.stringify({
214
+ status: options.status, after: options.after, workItemId: options.workItemId
215
+ })}`, options);
169
216
  });
170
217
  }
218
+ export const CONTEXT_FAMILIES = [
219
+ "task", "task-brief", "remote-delivery", "role", "role-profile",
220
+ "project-policy", "project-knowledge", "input-request", "work-item",
221
+ "accepted-work-item", "candidate", "environment-preparation", "managed-workspace",
222
+ "change-set", "task-message", "run", "source-run", "review-round", "mailbox",
223
+ "task-decision", "task-milestone", "job", "publication", "task-event"
224
+ ];
171
225
  export function listContextMessages(store, taskId, environment = {}) {
172
226
  return store.readTransaction((reader) => {
173
227
  const { allow } = authorizeContext(reader, taskId, environment);
@@ -220,8 +274,7 @@ export function resolveContextReader(store, environment) {
220
274
  throw usageError("Only Operator may read Task context from a global Session.");
221
275
  }
222
276
  if (caller === undefined && environment.YUI_SESSION_SCOPE === undefined
223
- && (environment.YUI_ROLE !== undefined || environment.YUI_NATIVE_SESSION_ID !== undefined
224
- || environment.YUI_TASK_ID !== undefined || environment.YUI_AGENT_ID !== undefined)) {
277
+ && hasManagedIdentity(environment)) {
225
278
  throw usageError("Incomplete managed Context caller identity.");
226
279
  }
227
280
  if (environment.YUI_SESSION_SCOPE !== undefined && !["task", "global"].includes(environment.YUI_SESSION_SCOPE)) {
@@ -241,7 +294,7 @@ function authorizeContext(store, taskId, environment) {
241
294
  if (caller.currentRunId === undefined)
242
295
  throw usageError("A managed Role needs a current AgentRun to read its scoped Context.");
243
296
  const pack = buildRunContextPack(store, taskId, caller.currentRunId);
244
- allow = new Set(pack.authority.readableRefs.map((ref) => `${ref.store}:${ref.refId}`));
297
+ allow = new Set(pack.pointers.map((ref) => `${ref.store}:${ref.refId}`));
245
298
  allow.add(`role:${caller.roleName}`);
246
299
  allow.add(`turn:${caller.currentRunId}`);
247
300
  // Shared current user intent and this exact Assignment's live steers augment
@@ -389,31 +442,35 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
389
442
  default: return null;
390
443
  }
391
444
  }
392
- function authorizedSources(store, { task, allow, caller }) {
445
+ function authorizedSources(store, { task, allow, caller }, current = false, requestedFamily) {
393
446
  const taskId = task.id;
394
447
  const sources = [];
395
448
  const add = (family, id, value) => {
396
- if (value !== null && isAllowed(allow, family, id)) {
397
- sources.push({ count: 1, read: () => [materialize(family, id, value)] });
449
+ if ((requestedFamily === undefined || requestedFamily === family) && value !== null && isAllowed(allow, family, id)) {
450
+ sources.push({ family, count: 1, read: () => [materialize(family, id, value)] });
398
451
  }
399
452
  };
400
453
  const query = (family, options = {}) => {
454
+ if (requestedFamily !== undefined && requestedFamily !== family)
455
+ return;
401
456
  const selection = { ...options, family, ids: allowedIds(allow, family) };
402
457
  const { count } = store.queryContextRecords(taskId, { ...selection, limit: 0 });
403
- sources.push({ count, *read() {
458
+ sources.push({ family, count, *read(offset = 0) {
404
459
  for (const { id, value } of store.queryContextRecords(taskId, {
405
- ...selection, limit: MAX_RECORDS
460
+ ...selection, offset, limit: current ? 8 : MAX_RECORDS
406
461
  }).records)
407
462
  yield materialize(family, id, value);
408
463
  } });
409
464
  };
410
465
  add("task", taskId, task);
411
466
  add("task-brief", taskId, store.getTaskBrief(taskId));
412
- if (allow === undefined)
467
+ if (allow === undefined && (requestedFamily === undefined || requestedFamily === "remote-delivery")) {
413
468
  add("remote-delivery", taskId, projectTaskRemoteDeliveryFromStore(store, task));
469
+ }
414
470
  for (const role of store.listRoles(taskId)) {
415
471
  add("role", role.name, role);
416
- add("role-profile", role.name, roleProfile(role));
472
+ if (!current)
473
+ add("role-profile", role.name, roleProfile(role));
417
474
  }
418
475
  for (const binding of task.projectBindings) {
419
476
  const project = store.getProject(binding.projectId);
@@ -427,42 +484,48 @@ function authorizedSources(store, { task, allow, caller }) {
427
484
  }
428
485
  // Current responsibilities and unanswered inputs precede historical bulk.
429
486
  query("input-request", { statuses: ["open"] });
430
- query("work-item");
487
+ query("work-item", current ? { excludeStatuses: ["accepted", "retired"] } : {});
431
488
  for (const id of allowedIds(allow, "accepted-work-item") ?? []) {
432
489
  add("accepted-work-item", id, store.getWorkItem(taskId, id));
433
490
  }
434
- query("candidate");
491
+ if (!current)
492
+ query("candidate");
435
493
  // File artifacts are not enumerated into the synchronous Context listing: the
436
494
  // core cursor does not cover file edits and an ambient directory index here
437
495
  // would be the forbidden update-time mirror (§3.7). Current artifacts are
438
496
  // listed on demand via the async `artifact.list` capability; frozen Candidate
439
497
  // artifacts remain reachable as commit-pinned pointers under their Candidate.
440
- query("environment-preparation");
498
+ if (!current)
499
+ query("environment-preparation");
441
500
  query("managed-workspace");
442
501
  if (caller?.currentRunId !== undefined) {
443
502
  add("managed-workspace", `${taskId}/${caller.roleName}`, store.getRun(taskId, caller.currentRunId)?.workspace ?? null);
444
503
  }
445
- query("change-set");
504
+ if (!current)
505
+ query("change-set");
446
506
  query("task-message", { descending: true });
447
- query("run", { descending: true });
507
+ query("run", { descending: true, ...(current ? { statuses: ["active"] } : {}) });
448
508
  for (const id of allowedIds(allow, "source-turn") ?? []) {
449
509
  const run = store.getRun(taskId, id);
450
510
  if (run !== null)
451
511
  add("source-run", id, sourceRunContextValue(run));
452
512
  }
453
- query("review-round", { descending: true });
454
- query("input-request", { excludeStatuses: ["open"] });
455
- if (allow === undefined) {
513
+ query("review-round", { descending: true, ...(current ? { statuses: ["pending", "running"] } : {}) });
514
+ if (!current)
515
+ query("input-request", { excludeStatuses: ["open"] });
516
+ if (!current && allow === undefined) {
456
517
  add("mailbox", "task", store.getWorkMailbox({ kind: "task", taskId }));
457
518
  for (const role of store.listRoles(taskId)) {
458
519
  add("mailbox", `role:${role.name}`, store.getWorkMailbox({ kind: "role", taskId, roleName: role.name }));
459
520
  }
460
521
  }
461
- query("task-decision");
462
- query("task-milestone");
463
- query("job");
464
- query("publication");
465
- query("task-event", { descending: true });
522
+ query("task-decision", current ? { statuses: ["active"] } : {});
523
+ if (!current) {
524
+ query("task-milestone");
525
+ query("publication");
526
+ query("task-event", { descending: true });
527
+ }
528
+ query("job", current ? { statuses: ["queued", "running", "unknown-needs-attention"] } : {});
466
529
  return sources;
467
530
  }
468
531
  export function materialize(store, refId, value) {
@@ -512,7 +575,7 @@ function freeze(value) {
512
575
  }
513
576
  function summarize(value) {
514
577
  const record = value;
515
- return ["title", "objective", "summary", "body", "status"]
578
+ return ["title", "objective", "question", "summary", "body", "status", "type"]
516
579
  .flatMap((key) => typeof record[key] === "string" ? [`${key}: ${record[key]}`] : [])
517
580
  .join("; ").slice(0, 400);
518
581
  }
@@ -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
  }