@zq-silk/yui 2.7.1 → 2.8.1

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 (92) hide show
  1. package/dist/cli/commandCatalog.js +8 -7
  2. package/dist/cli/updateCommand.js +3 -0
  3. package/dist/cli/updatePorts.js +20 -0
  4. package/dist/commands/configCommands.js +1 -0
  5. package/dist/commands/globalRoleCommands.js +42 -35
  6. package/dist/commands/inboxCommands.js +60 -40
  7. package/dist/commands/taskCommands.js +7 -3
  8. package/dist/commands/taskRoleRuntimeStatus.js +2 -0
  9. package/dist/context/runContextPack.js +48 -3
  10. package/dist/context/taskContext.js +36 -12
  11. package/dist/controlPlaneCli.js +64 -17
  12. package/dist/controller/controller.js +5 -0
  13. package/dist/controller/fileSchedulerStoreAdapter.js +9 -4
  14. package/dist/controller/globalInputDelivery.js +17 -5
  15. package/dist/controller/globalRuntimeAttention.js +135 -5
  16. package/dist/controller/runtime.js +4 -0
  17. package/dist/controller/structuredProviderObservation.js +51 -15
  18. package/dist/controller/taskHostTerminalRecovery.js +33 -0
  19. package/dist/executor/fileRoleLaunchPlanner.js +4 -9
  20. package/dist/message/inboxReceipt.js +30 -0
  21. package/dist/message/taskInboxOwnership.js +25 -3
  22. package/dist/models/modelManagement.js +469 -0
  23. package/dist/models/modelManagementConfig.js +46 -0
  24. package/dist/models/modelManagementFiles.js +90 -0
  25. package/dist/models/modelManagementHttp.js +90 -0
  26. package/dist/models/modelManagementTypes.js +1 -0
  27. package/dist/output/boundedRead.js +43 -14
  28. package/dist/runtime/agentEndpointIdentity.js +8 -4
  29. package/dist/runtime/agentEndpointOwnership.js +5 -6
  30. package/dist/runtime/agentHost.js +62 -10
  31. package/dist/runtime/agentHostImplementation.js +18 -0
  32. package/dist/runtime/managedCaller.js +25 -1
  33. package/dist/runtime/managedIdentity.js +1 -1
  34. package/dist/runtime/providerRetry.js +19 -1
  35. package/dist/runtime/sessionCaller.js +53 -0
  36. package/dist/storage/contextRecords.js +28 -0
  37. package/dist/storage/maintenanceCaller.js +69 -0
  38. package/dist/storage/sqliteSchema.js +12 -2
  39. package/dist/storage/sqliteStore.js +9 -5
  40. package/dist/storage/storageVersions.js +1 -1
  41. package/dist/storage/taskStore.js +4 -0
  42. package/dist/task/completionReadiness.js +3 -3
  43. package/dist/web/assets/assetManifest.js +28 -2
  44. package/dist/web/assets/client/app/manage.js +6 -0
  45. package/dist/web/assets/client/app/models.js +20 -0
  46. package/dist/web/assets/client/app/settings.js +41 -9
  47. package/dist/web/assets/client/i18n/en.js +180 -2
  48. package/dist/web/assets/client/i18n/zh-CN.js +178 -2
  49. package/dist/web/assets/client/index.js +8 -0
  50. package/dist/web/assets/client/layout/manageShell.js +94 -0
  51. package/dist/web/assets/client/lib/api.js +13 -1
  52. package/dist/web/assets/client/lib/i18n.js +5 -3
  53. package/dist/web/assets/client/views/dock/conversation.js +231 -80
  54. package/dist/web/assets/client/views/models.js +777 -0
  55. package/dist/web/assets/client/views/skills.js +22 -15
  56. package/dist/web/assets/shared/icons.js +2 -0
  57. package/dist/web/assets/shared/manageNav.js +13 -0
  58. package/dist/web/assets/shell/manage.js +54 -0
  59. package/dist/web/assets/shell/models.js +7 -0
  60. package/dist/web/assets/shell/settings.js +1 -35
  61. package/dist/web/assets/shell/settingsContent.js +30 -0
  62. package/dist/web/assets/shell/sidebar.js +1 -0
  63. package/dist/web/assets/styles/index.js +6 -0
  64. package/dist/web/assets/styles/views/conversation.js +133 -0
  65. package/dist/web/assets/styles/views/manage.js +71 -0
  66. package/dist/web/assets/styles/views/models.js +129 -0
  67. package/dist/web/assets/styles/views/settings.js +28 -14
  68. package/dist/web/assets/styles/views/sidebar.js +1 -0
  69. package/dist/web/webServer.js +59 -9
  70. package/docs/cli-information-contract.md +42 -11
  71. package/docs/cli-information-contract.zh-CN.md +28 -7
  72. package/docs/managed-turn-and-session-runtime.md +47 -1
  73. package/docs/managed-turn-and-session-runtime.zh-CN.md +32 -0
  74. package/docs/release-workflow.md +80 -7
  75. package/docs/release-workflow.zh-CN.md +55 -4
  76. package/docs/storage-baseline.md +6 -1
  77. package/docs/storage-baseline.zh-CN.md +4 -1
  78. package/docs/testing/review-evaluation.md +148 -0
  79. package/docs/testing/review-evaluation.zh-CN.md +117 -0
  80. package/docs/testing/verification-levels.md +6 -2
  81. package/docs/testing/verification-levels.zh-CN.md +3 -0
  82. package/package.json +1 -1
  83. package/skills/yui-leader/VERSION +1 -1
  84. package/skills/yui-leader/references/execution.md +29 -0
  85. package/skills/yui-leader/references/replicated-execution.md +7 -0
  86. package/skills/yui-operator/SKILL.md +5 -1
  87. package/skills/yui-operator/VERSION +1 -1
  88. package/skills/yui-reviewer/SKILL.md +35 -0
  89. package/skills/yui-reviewer/VERSION +1 -1
  90. package/skills/yui-runtime/SKILL.md +53 -7
  91. package/skills/yui-runtime/VERSION +1 -1
  92. package/skills/yui-runtime/references/recovery.md +24 -5
@@ -0,0 +1,90 @@
1
+ import { lookup } from "node:dns/promises";
2
+ import { request as httpRequest } from "node:http";
3
+ import { request as httpsRequest } from "node:https";
4
+ import { BlockList, isIP } from "node:net";
5
+ import { modelUrl } from "./modelManagementConfig.js";
6
+ const denied = new BlockList();
7
+ for (const [address, bits] of [
8
+ ["0.0.0.0", 8], ["10.0.0.0", 8], ["100.64.0.0", 10], ["169.254.0.0", 16],
9
+ ["172.16.0.0", 12], ["192.0.0.0", 24], ["192.168.0.0", 16], ["198.18.0.0", 15],
10
+ ["224.0.0.0", 4], ["240.0.0.0", 4]
11
+ ])
12
+ denied.addSubnet(address, bits, "ipv4");
13
+ for (const [address, bits] of [["::", 128], ["fc00::", 7], ["fe80::", 10], ["ff00::", 8], ["::ffff:0:0", 96]]) {
14
+ denied.addSubnet(address, bits, "ipv6");
15
+ }
16
+ /** Small fixed-route client: pinned DNS, no redirects/proxies, deadline and byte cap. */
17
+ export async function readModelJson(base, suffix, key, timeoutMs) {
18
+ const url = modelUrl(`${base.replace(/\/+$/, "")}${suffix}`);
19
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
20
+ const explicitLoopback = ["127.0.0.1", "::1", "localhost"].includes(hostname);
21
+ const abort = new AbortController();
22
+ const timer = setTimeout(() => abort.abort(), timeoutMs);
23
+ try {
24
+ const addresses = await Promise.race([
25
+ isIP(hostname) ? Promise.resolve([{ address: hostname, family: isIP(hostname) }]) : lookup(hostname, { all: true }),
26
+ new Promise((_, reject) => abort.signal.addEventListener("abort", () => reject(new Error("timeout")), { once: true }))
27
+ ]);
28
+ if (abort.signal.aborted || !addresses.length || addresses.some(({ address, family }) => {
29
+ const loopback = address === "::1" || address.startsWith("127.");
30
+ return (loopback && !explicitLoopback) || (!loopback && explicitLoopback)
31
+ || (!loopback && denied.check(address, family === 6 ? "ipv6" : "ipv4"));
32
+ }))
33
+ throw new Error("target-rejected");
34
+ const selected = addresses[0];
35
+ return await new Promise((resolve, reject) => {
36
+ const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, {
37
+ method: "GET", signal: abort.signal, agent: false,
38
+ headers: { accept: "application/json", ...(key ? { authorization: `Bearer ${key}` } : {}) },
39
+ lookup: (_name, options, callback) => options.all
40
+ ? callback(null, addresses)
41
+ : callback(null, selected.address, selected.family)
42
+ }, response => {
43
+ if (response.statusCode !== 200) {
44
+ response.destroy();
45
+ reject(new Error(response.statusCode === 401 || response.statusCode === 403 ? "authentication-failed"
46
+ : response.statusCode === 404 ? "route-unavailable" : "service-failed"));
47
+ return;
48
+ }
49
+ let bytes = 0;
50
+ const chunks = [];
51
+ response.on("data", chunk => {
52
+ bytes += chunk.length;
53
+ if (bytes > 2 * 1024 * 1024) {
54
+ response.destroy();
55
+ reject(new Error("response-too-large"));
56
+ }
57
+ else
58
+ chunks.push(chunk);
59
+ });
60
+ response.on("error", () => reject(new Error("read-failed")));
61
+ response.on("end", () => {
62
+ try {
63
+ // Even schema-allowed upstream strings must not echo our credential.
64
+ const text = Buffer.concat(chunks).toString("utf8");
65
+ const parsed = JSON.parse(text);
66
+ resolve(key ? redact(parsed, key) : parsed);
67
+ }
68
+ catch {
69
+ reject(new Error("invalid-response"));
70
+ }
71
+ });
72
+ });
73
+ request.on("error", () => reject(new Error(abort.signal.aborted ? "timeout" : "connection-failed")));
74
+ request.end();
75
+ });
76
+ }
77
+ finally {
78
+ clearTimeout(timer);
79
+ }
80
+ }
81
+ function redact(value, key) {
82
+ if (typeof value === "string")
83
+ return value.split(key).join("[redacted]");
84
+ if (Array.isArray(value))
85
+ return value.map(item => redact(item, key));
86
+ if (value && typeof value === "object")
87
+ return Object.fromEntries(Object.entries(value)
88
+ .map(([name, item]) => [name.split(key).join("[redacted]"), redact(item, key)]));
89
+ return value;
90
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -2,6 +2,30 @@ import { createHash } from "node:crypto";
2
2
  import { usageError } from "../errors/cliError.js";
3
3
  export const READ_PAGE_BYTES = 32 * 1024;
4
4
  export const INLINE_DOCUMENT_BYTES = 16 * 1024;
5
+ /** Compact JSON wire budget, including the CLI envelope and small command
6
+ * wrappers (for example Run Context's `context` field). Never count just items. */
7
+ export function fitsReadBudget(value, bytes = READ_PAGE_BYTES) {
8
+ return Buffer.byteLength(JSON.stringify({ ok: true, data: value })) + 256 <= bytes;
9
+ }
10
+ export function summaryText(text, bytes = 512) {
11
+ if (Buffer.byteLength(text) <= bytes)
12
+ return text;
13
+ let result = "";
14
+ let size = 0;
15
+ for (const character of text) {
16
+ const length = Buffer.byteLength(character);
17
+ if (size + length + 3 > bytes)
18
+ break;
19
+ result += character;
20
+ size += length;
21
+ }
22
+ return result + "…";
23
+ }
24
+ /** List-only nested metadata. The containing entry must expose an exact read. */
25
+ export function summaryMetadata(value, budget = 2048) {
26
+ const bytes = Buffer.byteLength(JSON.stringify(value));
27
+ return bytes <= budget ? value : { omitted: true, digest: readDigest(value), bytes };
28
+ }
5
29
  function assertSource(source) {
6
30
  if (Buffer.byteLength(source) > 1024)
7
31
  throw usageError("Read source identity exceeds its 1024-byte budget.");
@@ -22,7 +46,7 @@ export function encodeReadCursor(cursor) {
22
46
  export function decodeReadCursor(raw, source, digest) {
23
47
  let cursor;
24
48
  try {
25
- if (raw.length > 4096)
49
+ if (raw.length > 16 * 1024)
26
50
  throw new Error();
27
51
  cursor = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
28
52
  if (cursor.source !== source || !Number.isSafeInteger(cursor.offset) || cursor.offset < 0
@@ -46,7 +70,7 @@ export function readLimit(value) {
46
70
  /** For small in-memory authorized collections. Authorize/filter BEFORE calling.
47
71
  * Cursors bind the selected collection, never grant access, and store no state.
48
72
  */
49
- export function recordPage(records, source, options = {}) {
73
+ export function recordPage(records, source, options = {}, metadata = {}) {
50
74
  assertSource(source);
51
75
  const limit = readLimit(options.limit);
52
76
  const digest = readDigest(records);
@@ -54,24 +78,29 @@ export function recordPage(records, source, options = {}) {
54
78
  if (offset > records.length)
55
79
  throw usageError("Read cursor exceeds this collection.");
56
80
  const items = [];
57
- // Reserve room for identities, counts and continuation. Oversized individual
58
- // records belong in a document read, not silently truncated list metadata.
59
- let bytes = 0;
81
+ const page = () => {
82
+ const end = offset + items.length;
83
+ return {
84
+ ...metadata,
85
+ source, digest, items, total: records.length, complete: end === records.length,
86
+ nextCursor: end < records.length ? encodeReadCursor({ source, digest, offset: end }) : null,
87
+ limits: { items: limit, bytes: READ_PAGE_BYTES }
88
+ };
89
+ };
60
90
  for (const record of records.slice(offset, offset + limit)) {
61
91
  const size = Buffer.byteLength(JSON.stringify(record)) + 1;
62
92
  if (size > READ_PAGE_BYTES / 2)
63
93
  throw usageError("List entry exceeds its summary budget; read the exact record.");
64
- if (bytes + size > READ_PAGE_BYTES - 4096)
65
- break;
66
94
  items.push(record);
67
- bytes += size;
95
+ if (!fitsReadBudget(page())) {
96
+ items.pop();
97
+ break;
98
+ }
68
99
  }
69
- const end = offset + items.length;
70
- return {
71
- source, digest, items, total: records.length, complete: end === records.length,
72
- nextCursor: end < records.length ? encodeReadCursor({ source, digest, offset: end }) : null,
73
- limits: { items: limit, bytes: READ_PAGE_BYTES }
74
- };
100
+ if (!fitsReadBudget(page()) || items.length === 0 && offset < records.length) {
101
+ throw usageError("List entry and read identity exceed the response budget; read the exact record.");
102
+ }
103
+ return page();
75
104
  }
76
105
  /** Small exact details stay native JSON. Large details explicitly become a
77
106
  * contentPage. Concatenate text in order, then JSON.parse once, never each chunk.
@@ -78,12 +78,16 @@ export function validateAgentEndpointImplementation(ref) {
78
78
  }
79
79
  return ref;
80
80
  }
81
- /** Missing old code is an explicit inability to resume, never a Provider fallback. */
81
+ /**
82
+ * The built-in adapter identity is the supported native connection contract.
83
+ * Build generations are provenance, not a requirement to retain old code.
84
+ * Actual native resume still validates the conversation/account/configuration.
85
+ */
82
86
  export function requireBuiltinAgentEndpointImplementation(adapterId, ref) {
83
87
  validateAgentEndpointImplementation(ref);
84
88
  const current = builtinAgentEndpointImplementation(adapterId);
85
- if (ref.id !== current.id || ref.generation !== current.generation) {
86
- throw new Error(`Session Endpoint ${ref.id}@${ref.generation} is unavailable; explicitly select a new Session.`);
89
+ if (ref.id !== current.id) {
90
+ throw new Error(`Session Endpoint ${ref.id} is incompatible with adapter ${adapterId}.`);
87
91
  }
88
- return ref;
92
+ return current;
89
93
  }
@@ -8,9 +8,9 @@ import { createAgentEndpointFactory } from "./agentEndpoint.js";
8
8
  * owns its own Host in its own process and never holds an Endpoint client, so
9
9
  * the reference facts for a Session live where the Session actually runs.
10
10
  *
11
- * Only the running code can be attached here. Selecting between generations is
12
- * the launch decision recorded in the durable Session; this owner proves the
13
- * chosen generation is really held and really released.
11
+ * Only the running code can be attached here. This owner proves the loaded
12
+ * generation is held and released; historical launch generations do not select
13
+ * old code or block a compatible native resume.
14
14
  */
15
15
  export function createAgentEndpointOwner(factory = createAgentEndpointFactory()) {
16
16
  const host = new InstanceHost();
@@ -48,9 +48,8 @@ export function createAgentEndpointOwner(factory = createAgentEndpointFactory())
48
48
  await holder.handle.release();
49
49
  };
50
50
  /**
51
- * Pin the implementation for one Session. A pinned reference from the durable
52
- * Session must name this exact code; a mismatch is an explicit inability to
53
- * resume, never a silent start on a different generation.
51
+ * Hold the current implementation for one Session. A historical reference
52
+ * must name the same built-in adapter, not the same package build.
54
53
  */
55
54
  const pin = (adapterId, pinned) => {
56
55
  if (pinned !== undefined)
@@ -5,7 +5,7 @@ import { homedir, tmpdir } from "node:os";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { createInterface } from "node:readline";
7
7
  import { withSessionContextPointer } from "../context/sessionBootstrapManifest.js";
8
- import { publishStructuredConversationRecoverability, publishStructuredProviderAccepted, publishStructuredProviderActivity, publishStructuredProviderAttachmentExit, publishStructuredProviderConnection, publishStructuredProviderDiagnostic, publishStructuredProviderGoal, publishStructuredProviderInputObserved, publishStructuredProviderInputSettlement, publishStructuredProviderOpened, publishStructuredProviderStarted, publishStructuredProviderTerminal, structuredProviderEventDelivery } from "../controller/structuredProviderObservation.js";
8
+ import { publishStructuredConversationRecoverability, publishStructuredProviderAccepted, publishStructuredProviderActivity, publishStructuredProviderAttachmentExit, publishStructuredProviderConnection, publishStructuredProviderDiagnostic, publishStructuredProviderGoal, publishStructuredProviderInputObserved, publishStructuredProviderInputSettlement, publishStructuredProviderOpened, publishStructuredProviderStarted, publishStructuredProviderTerminal, replayStructuredProviderPersistence, structuredProviderEventDelivery } from "../controller/structuredProviderObservation.js";
9
9
  import { callController, controllerCallMayHaveApplied, ControllerClientError } from "../core/controllerClient.js";
10
10
  import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
11
11
  import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
@@ -14,6 +14,7 @@ import { tmuxSocketDirectory } from "../tmux/tmuxSocketEndpoint.js";
14
14
  import { createAgentEndpointOwner } from "./agentEndpointOwnership.js";
15
15
  import { providerDeliveryFailure, providerDeliveryFailureFrom, redactAgentErrorText, serializeAgentErrorRaw } from "./agentError.js";
16
16
  import { AGENT_HOST_CONTROL_PROTOCOL } from "./agentHostProtocol.js";
17
+ import { AGENT_HOST_IMPLEMENTATION } from "./agentHostImplementation.js";
17
18
  import { closeAgentHostEndpoints } from "./agentHostCleanup.js";
18
19
  import { readAgentRunConfigurationObservation, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
19
20
  import { CodexPreSubmissionError } from "./codexAppServerRuntime.js";
@@ -188,6 +189,9 @@ export async function runAgentHost(input) {
188
189
  && previous.started.nativeSessionId === session?.nativeSessionId) {
189
190
  await publishStructuredProviderTerminal({
190
191
  home: input.home, environment: previous.payload.environment, terminal: previous.terminal
192
+ }).catch(() => {
193
+ // The publisher retains the original unpersisted bytes. Still
194
+ // observe the new start; a failed old write must not hide its writer.
191
195
  });
192
196
  }
193
197
  if (session !== undefined && started.nativeSessionId === session.nativeSessionId
@@ -594,6 +598,8 @@ export async function runAgentHost(input) {
594
598
  throw new Error("Agent Host accepts only managed Provider control launches.");
595
599
  }
596
600
  if (activeRunPayload !== undefined
601
+ || sessionPayload !== undefined
602
+ && (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
597
603
  || ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
598
604
  throw new Error("Agent Host still owns an unsettled Provider Turn.");
599
605
  }
@@ -624,8 +630,7 @@ export async function runAgentHost(input) {
624
630
  || providerControl.mode !== "resume"
625
631
  || providerControl.nativeSessionId !== session.nativeSessionId
626
632
  || (providerControl.endpointImplementation !== undefined
627
- && (providerControl.endpointImplementation.id !== session.configuration.implementation.id
628
- || providerControl.endpointImplementation.generation !== session.configuration.implementation.generation))) {
633
+ && providerControl.endpointImplementation.id !== session.configuration.implementation.id)) {
629
634
  throw new Error("Agent Host launch does not match its live Provider Conversation.");
630
635
  }
631
636
  sessionPayload = next;
@@ -638,9 +643,8 @@ export async function runAgentHost(input) {
638
643
  }
639
644
  // The Controller revalidates the adopted execution environment while
640
645
  // redeeming this launch; a persistent Host must not reopen its schema.
641
- // Pin before opening: the Session holds this exact generation for every
642
- // later Turn. A pinned reference naming code this process is not running
643
- // fails here, rather than silently starting on a different generation.
646
+ // Hold the code actually loaded by this Host. Historical build identity
647
+ // is provenance; native resume and current authority remain mandatory.
644
648
  endpointLease ??= endpointOwner.pin(providerControl.adapterId, providerControl.endpointImplementation);
645
649
  endpointAdapterId = providerControl.adapterId;
646
650
  const started = await (providerControl.mode === "new"
@@ -786,6 +790,7 @@ export async function runAgentHost(input) {
786
790
  throw new Error("Agent Host rejected a stale Provider writer fence.");
787
791
  }
788
792
  if (activeRunPayload !== undefined || snapshot.state === "settling"
793
+ || (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
789
794
  || session.inspect().activeNativeTurnId !== undefined) {
790
795
  operation.failure = providerDeliveryFailure({
791
796
  detail: "Agent Host still owns an unsettled Provider Turn.",
@@ -1042,7 +1047,9 @@ export async function runAgentHost(input) {
1042
1047
  if (session === undefined || request.nativeSessionId !== session.nativeSessionId) {
1043
1048
  throw new Error("Agent Host authority targets a different Provider Conversation.");
1044
1049
  }
1045
- if (activeRunPayload !== undefined
1050
+ if (activeRunPayload !== undefined || session.inspect().activeNativeTurnId !== undefined
1051
+ || sessionPayload !== undefined
1052
+ && (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
1046
1053
  || ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
1047
1054
  throw new Error("Agent Host authority cannot transfer while a AgentRun is unsettled.");
1048
1055
  }
@@ -1118,7 +1125,9 @@ export async function runAgentHost(input) {
1118
1125
  return enqueueSerialized(async () => {
1119
1126
  const current = session;
1120
1127
  if (!current?.nativeAccess || current.nativeSessionId !== request.nativeSessionId
1121
- || !sessionPayload || activeNativeTurnId || authority?.owner !== "controller"
1128
+ || !sessionPayload || activeRunPayload || activeNativeTurnId
1129
+ || (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
1130
+ || authority?.owner !== "controller"
1122
1131
  || (sessionPayload.environment.YUI_SESSION_SCOPE === "task" && sessionPayload.environment.YUI_ROLE !== "leader")
1123
1132
  || (sessionPayload.environment.YUI_SESSION_SCOPE === "global" && sessionPayload.environment.YUI_ROLE !== "operator")) {
1124
1133
  throw new Error("Model switching requires the idle current controlled Codex Session, without a native terminal writer.");
@@ -1192,6 +1201,27 @@ export async function runAgentHost(input) {
1192
1201
  || authority === undefined || !sameProviderAuthorityFence(authority, request.authority)) {
1193
1202
  throw new Error("Terminal reconciliation targets a different Session or authority.");
1194
1203
  }
1204
+ const recovered = await enqueueSerialized(async () => {
1205
+ if (session?.nativeSessionId !== request.nativeSessionId || authority === undefined
1206
+ || !sameProviderAuthorityFence(authority, request.authority)) {
1207
+ throw new Error("Session changed before original event persistence recovery.");
1208
+ }
1209
+ const persisted = await replayStructuredProviderPersistence(input.home, sessionPayload.environment);
1210
+ if (persisted.length > 0 && activeRunPayload === undefined) {
1211
+ const active = session.inspect().activeNativeTurnId;
1212
+ updateSnapshot(hostSnapshot(active === undefined ? "idle" : "busy", {
1213
+ adapterId: session.adapterId, processInstanceId: session.processInstanceId,
1214
+ nativeSessionId: session.nativeSessionId, conversationId: session.conversationId,
1215
+ nativeTurnId: active, ...authorityFields()
1216
+ }));
1217
+ }
1218
+ return persisted.some(event => ["turn.completed", "turn.failed", "turn.cancelled"].includes(event.kind)
1219
+ && event.fence.nativeSessionId === request.nativeSessionId
1220
+ && (request.nativeTurnId === undefined || event.fence.nativeTurnId === request.nativeTurnId)
1221
+ && (event.fence.receiptId === request.attemptId
1222
+ || event.fence.receiptId === undefined && request.nativeTurnId !== undefined
1223
+ && request.attemptId === `direct:${request.nativeTurnId}`));
1224
+ });
1195
1225
  const direct = directObservation;
1196
1226
  const directMatch = direct?.terminal !== undefined
1197
1227
  && request.nativeTurnId !== undefined
@@ -1205,7 +1235,8 @@ export async function runAgentHost(input) {
1205
1235
  || terminal.nativeSessionId !== request.nativeSessionId
1206
1236
  || !directMatch && (terminal.clientOwned !== true || terminal.attemptId !== request.attemptId)
1207
1237
  || request.nativeTurnId !== undefined && terminal.nativeTurnId !== request.nativeTurnId) {
1208
- return { ...controlResult("status", snapshot), reconciliation: "unobserved" };
1238
+ return { ...controlResult("status", snapshot),
1239
+ reconciliation: recovered ? "terminal-republished" : "unobserved" };
1209
1240
  }
1210
1241
  // Serialize with submission/terminal persistence. The original Host and
1211
1242
  // original launch envelope remain the observer, including the full result.
@@ -1539,7 +1570,18 @@ export async function inspectAgentHost(input) {
1539
1570
  export async function reconcileAgentHostTurn(input) {
1540
1571
  // Periodic observation must not hold the scheduler behind an unavailable Host.
1541
1572
  // A timeout is only an observation failure, never permission to submit again.
1542
- return exchangeAgentHostControl(agentHostControlSocketPath(input), input.control, 1_000);
1573
+ const path = agentHostControlSocketPath(input);
1574
+ const observed = await exchangeAgentHostControl(path, {
1575
+ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "status"
1576
+ }, 1_000);
1577
+ if (observed.outcome === "rejected")
1578
+ return observed;
1579
+ if (!observed.snapshot.controlCapabilities?.includes("reconcile-turn")) {
1580
+ // Older v1 Hosts still serve status and their immutable Inbox remains
1581
+ // consumable. Neither idle nor absent eventDelivery proves a terminal.
1582
+ return { ...observed, reconciliation: "unsupported" };
1583
+ }
1584
+ return exchangeAgentHostControl(path, input.control, 1_000);
1543
1585
  }
1544
1586
  export async function waitForAgentHostLaunchAck(input) {
1545
1587
  const deadline = Date.now() + (input.timeoutMs ?? AGENT_HOST_READY_TIMEOUT_MS);
@@ -1696,6 +1738,11 @@ export async function openAgentHostControl(home, payload, snapshot, dispatch) {
1696
1738
  ...result,
1697
1739
  snapshot: {
1698
1740
  ...result.snapshot,
1741
+ hostImplementation: AGENT_HOST_IMPLEMENTATION,
1742
+ controlCapabilities: payload.providerControl === undefined ? ["status"] : [
1743
+ "status", "launch", "submit-turn", "steer-turn", "cancel", "set-authority",
1744
+ "native-open", "native-respond", "native-model", "reconcile-turn"
1745
+ ],
1699
1746
  ...(!["task", "global"].includes(payload.environment.YUI_SESSION_SCOPE ?? "") ? {} : {
1700
1747
  eventDelivery: structuredProviderEventDelivery(home, payload.environment, result.snapshot.nativeSessionId)
1701
1748
  })
@@ -1956,6 +2003,11 @@ function validateSnapshot(snapshot) {
1956
2003
  && snapshot.inputAcceptance !== "provider" && snapshot.inputAcceptance !== "transport") {
1957
2004
  throw new Error("Agent Host input acceptance evidence is invalid.");
1958
2005
  }
2006
+ if (snapshot.controlCapabilities !== undefined && (!Array.isArray(snapshot.controlCapabilities) || snapshot.controlCapabilities.length > 64
2007
+ || snapshot.controlCapabilities.some(value => typeof value !== "string"
2008
+ || value.length === 0 || value.length > 64 || value.includes("\0")))) {
2009
+ throw new Error("Agent Host control capability advertisement is invalid.");
2010
+ }
1959
2011
  const authorityFields = [
1960
2012
  snapshot.authorityEpoch,
1961
2013
  snapshot.authorityOwner,
@@ -0,0 +1,18 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { detectRunningRelease } from "../release/runtimeRelease.js";
5
+ // Capture once when this process loads its Host bridge. Never label a live
6
+ // process with the installation currently on disk at status-query time.
7
+ const release = detectRunningRelease(fileURLToPath(import.meta.url));
8
+ const modules = [
9
+ "agentHost.js", "agentHostProtocol.js", "agentHostImplementation.js",
10
+ "codexInteractiveHost.js", "../controller/structuredProviderObservation.js",
11
+ "../controller/runtimeEventInbox.js"
12
+ ];
13
+ export const AGENT_HOST_IMPLEMENTATION = Object.freeze({
14
+ id: "yui.agent-host",
15
+ generation: release?.manifest.packageDigest ?? `checkout-${createHash("sha256")
16
+ .update(modules.map(module => `${module}\0${readFileSync(new URL(module, import.meta.url), "utf8")}`).join("\0"))
17
+ .digest("hex")}`
18
+ });
@@ -16,12 +16,31 @@ export function requireManagedGlobalCaller(store, env) {
16
16
  || !role || !binding || !set || !session || session.status !== "active"
17
17
  || set.activeAgentId !== role.activeAgentId || session.agentId !== binding.agentId
18
18
  || session.adapterId !== binding.adapterId || nativeSessionId !== session.nativeSessionId
19
+ || (env.YUI_WORKSPACE !== undefined && env.YUI_WORKSPACE !== session.effective.workspace.root)
19
20
  || (env.YUI_AGENT_ID !== undefined && env.YUI_AGENT_ID !== session.agentId)
20
21
  || (env.YUI_ADAPTER_ID !== undefined && env.YUI_ADAPTER_ID !== session.adapterId)) {
21
22
  throw new ManagedRuntimeDriftError("This command requires the current managed global native Session; the previous Session has no write authority.");
22
23
  }
23
24
  return { roleName: role.name, agentId: session.agentId, adapterId: session.adapterId, nativeSessionId };
24
25
  }
26
+ /** Historical self diagnostics remain readable, but never authorize inbox
27
+ * handling, configuration, another Role's records, or lifecycle mutation. */
28
+ export function requireManagedGlobalReader(store, env) {
29
+ try {
30
+ requireManagedGlobalCaller(store, env);
31
+ return;
32
+ }
33
+ catch (error) {
34
+ const set = env.YUI_ROLE === undefined ? null : store.getGlobalRoleSessionSet(env.YUI_ROLE);
35
+ const known = [...Object.values(set?.sessions ?? {}), ...Object.values(set?.history ?? {})].some(session => session.nativeSessionId === identity(session.adapterId === "codex"
36
+ ? env.CODEX_THREAD_ID ?? env.YUI_NATIVE_SESSION_ID : env.YUI_NATIVE_SESSION_ID)
37
+ && (env.YUI_AGENT_ID === undefined || env.YUI_AGENT_ID === session.agentId)
38
+ && (env.YUI_ADAPTER_ID === undefined || env.YUI_ADAPTER_ID === session.adapterId)
39
+ && (env.YUI_WORKSPACE === undefined || env.YUI_WORKSPACE === session.effective.workspace.root));
40
+ if (env.YUI_SESSION_SCOPE !== "global" || env.YUI_TASK_ID !== undefined || !known)
41
+ throw error;
42
+ }
43
+ }
25
44
  /**
26
45
  * A managed Session presented valid self-identity but is no longer the current
27
46
  * runtime of its Task Role. This is a bounded diagnosis rather than a bare
@@ -62,7 +81,12 @@ export function resolveManagedTaskCaller(store, environment) {
62
81
  const self = managedTaskSessionIdentity(environment);
63
82
  if (self === undefined)
64
83
  return undefined;
65
- return requireCurrentRuntime(store, self);
84
+ const caller = requireCurrentRuntime(store, self);
85
+ if (environment?.YUI_AGENT_ID !== undefined && environment.YUI_AGENT_ID !== caller.agentId
86
+ || environment?.YUI_ADAPTER_ID !== undefined && environment.YUI_ADAPTER_ID !== caller.adapterId) {
87
+ throw new ManagedRuntimeDriftError("Managed Task Agent/adapter hints conflict with the current native Session.");
88
+ }
89
+ return caller;
66
90
  }
67
91
  /** Same as resolveManagedTaskCaller, but requires a managed Task Session. */
68
92
  export function requireManagedTaskCaller(store, environment) {
@@ -2,5 +2,5 @@
2
2
  * silently acquire the authority of an unbound public caller. */
3
3
  export function hasManagedIdentity(environment) {
4
4
  return ["YUI_SESSION_SCOPE", "YUI_ROLE", "YUI_AGENT_ID", "YUI_NATIVE_SESSION_ID",
5
- "YUI_TASK_ID", "YUI_SESSION_MANIFEST"].some(key => environment[key] !== undefined);
5
+ "YUI_TASK_ID", "YUI_SESSION_MANIFEST", "CODEX_THREAD_ID"].some(key => environment[key] !== undefined);
6
6
  }
@@ -176,7 +176,7 @@ export function providerRetryPrompt(retry, original) {
176
176
  original
177
177
  ].join("\n");
178
178
  }
179
- /** The same presentation is consumed by CLI, Context and Web. */
179
+ /** The same presentation is consumed by explicit detail reads and Web. */
180
180
  export function providerRetryProjection(binding) {
181
181
  const retry = binding?.retry;
182
182
  if (retry === undefined)
@@ -209,6 +209,24 @@ export function providerRetryProjection(binding) {
209
209
  ...(retry.successorRunId === undefined ? {} : { successorRunId: retry.successorRunId })
210
210
  };
211
211
  }
212
+ /** Entry-point facts, never accumulated retry history or raw diagnostics.
213
+ * Exact Session retry/show and failureRef retain the original evidence. */
214
+ export function providerRetrySummary(binding) {
215
+ const view = providerRetryProjection(binding);
216
+ if (view === null)
217
+ return null;
218
+ const { history, error, currentTurn, ...facts } = view;
219
+ return {
220
+ ...facts,
221
+ error: { source: error.source, phase: error.phase, category: error.category,
222
+ code: error.code, inputDisposition: error.inputDisposition,
223
+ sessionDisposition: error.sessionDisposition, retryable: error.retryable },
224
+ currentTurn: currentTurn === null ? null : {
225
+ attemptId: currentTurn.attemptId, status: currentTurn.status, nativeTurnId: currentTurn.nativeTurnId
226
+ },
227
+ historyCount: history.length, detailOmitted: true
228
+ };
229
+ }
212
230
  export function controlProviderRetry(binding, action, at) {
213
231
  if (action === "enable")
214
232
  return { ...binding, retryDisabled: false };
@@ -0,0 +1,53 @@
1
+ import { hasManagedIdentity } from "./managedIdentity.js";
2
+ import { ManagedRuntimeDriftError, requireManagedGlobalCaller, requireManagedTaskCaller } from "./managedCaller.js";
3
+ /** Resolve location from this Home's registry, not from a launch environment.
4
+ * The returned hints are not authority: consumers still validate the current
5
+ * binding (including revocation, Assignment and captured workspace).
6
+ * Native children with their own Codex thread cannot borrow a parent's hints.
7
+ */
8
+ export function sessionCallerEnvironment(store, env) {
9
+ const native = env.CODEX_THREAD_ID ?? env.YUI_NATIVE_SESSION_ID;
10
+ if (native === undefined) {
11
+ if (hasManagedIdentity(env))
12
+ throw new ManagedRuntimeDriftError("Managed Session identity has no native session id.");
13
+ return env;
14
+ }
15
+ if (!native || native.trim() !== native || native.includes("\0")) {
16
+ throw new ManagedRuntimeDriftError("Managed native Session identity is invalid.");
17
+ }
18
+ // Reuse the existing derived active-Session inventory; no new identity index
19
+ // or scan of every Task body/history is needed.
20
+ const matches = store.listRuntimeSessionCandidates().filter(session => session.nativeSessionId === (session.adapterId === "codex" ? env.CODEX_THREAD_ID ?? env.YUI_NATIVE_SESSION_ID : env.YUI_NATIVE_SESSION_ID));
21
+ if (matches.length !== 1) {
22
+ throw new ManagedRuntimeDriftError(matches.length
23
+ ? "Native Session identity is ambiguous in this YUI_HOME; no caller authority was selected."
24
+ : "Native Session is not registered in this YUI_HOME; refusing to infer public user authority.");
25
+ }
26
+ const match = matches[0];
27
+ const set = match.owner.scope === "global" ? store.getGlobalRoleSessionSet(match.owner.roleName)
28
+ : store.getTaskRoleSessionSet(match.owner.taskId, match.owner.roleName);
29
+ const session = set?.sessions[match.agentId];
30
+ if (set === null || session === undefined || session.nativeSessionId !== match.nativeSessionId) {
31
+ throw new ManagedRuntimeDriftError("Native Session registration changed during caller resolution; read current state again.");
32
+ }
33
+ const hints = {
34
+ YUI_SESSION_SCOPE: set.owner.scope, YUI_ROLE: set.owner.roleName,
35
+ YUI_AGENT_ID: session.agentId, YUI_ADAPTER_ID: session.adapterId,
36
+ YUI_WORKSPACE: session.effective.workspace.root,
37
+ ...(set.owner.scope === "task" ? { YUI_TASK_ID: set.owner.taskId } : {})
38
+ };
39
+ for (const key of [...Object.keys(hints), "YUI_TASK_ID"]) {
40
+ if (env[key] !== undefined && env[key] !== hints[key]) {
41
+ throw new ManagedRuntimeDriftError(`Managed Session hint ${key} conflicts with this Home's native Session registration.`);
42
+ }
43
+ }
44
+ return { ...env, ...hints };
45
+ }
46
+ /** One current caller for inbox and other own-address operations. */
47
+ export function requireManagedCaller(store, env) {
48
+ const current = sessionCallerEnvironment(store, env);
49
+ if (current.YUI_SESSION_SCOPE === "global") {
50
+ return { scope: "global", ...requireManagedGlobalCaller(store, current) };
51
+ }
52
+ return { scope: "task", ...requireManagedTaskCaller(store, current) };
53
+ }
@@ -58,6 +58,34 @@ export function queryContextRecords(db, taskId, query) {
58
58
  where.push(`${id} IN (SELECT value FROM json_each(@ids))`);
59
59
  params.ids = JSON.stringify(query.ids);
60
60
  }
61
+ if (query.unhandledMessages) {
62
+ if (query.family !== "task-message")
63
+ throw new Error("Unhandled selection requires Messages.");
64
+ where.push(`json_extract(r.payload, '$.inbox.ack') IS NULL`);
65
+ where.push(`NOT EXISTS (SELECT 1 FROM events e WHERE e.task_id = r.task_id
66
+ AND e.type = 'task.record-retired'
67
+ AND json_extract(e.payload, '$.payload.recordKind') = 'message'
68
+ AND json_extract(e.payload, '$.payload.recordId') = r.message_id)`);
69
+ // Match taskMessageInboxReceipt: a direct claim owns the Message, otherwise
70
+ // a wake receipt covers its exact reference and latest revision event.
71
+ where.push(`(json_extract(r.payload, '$.inbox') IS NOT NULL OR (
72
+ SELECT json_extract(w.payload, '$.inbox.ack') FROM task_wakes w
73
+ WHERE w.task_id = r.task_id AND json_extract(w.payload, '$.inbox') IS NOT NULL
74
+ AND EXISTS (SELECT 1 FROM json_each(w.payload, '$.refs') ref
75
+ WHERE json_extract(ref.value, '$.type') = 'message'
76
+ AND json_extract(ref.value, '$.taskId') = r.task_id
77
+ AND json_extract(ref.value, '$.id') = r.message_id)
78
+ AND (NOT EXISTS (SELECT 1 FROM events e WHERE e.task_id = r.task_id AND e.type = 'message.updated'
79
+ AND json_extract(e.payload, '$.payload.messageId') = r.message_id)
80
+ OR EXISTS (SELECT 1 FROM json_each(w.payload, '$.refs') update_ref
81
+ WHERE json_extract(update_ref.value, '$.type') = 'event'
82
+ AND json_extract(update_ref.value, '$.taskId') = r.task_id
83
+ AND json_extract(update_ref.value, '$.id') = (
84
+ SELECT e.event_id FROM events e WHERE e.task_id = r.task_id AND e.type = 'message.updated'
85
+ AND json_extract(e.payload, '$.payload.messageId') = r.message_id
86
+ ORDER BY CAST(substr(e.event_id, 7) AS INTEGER) DESC LIMIT 1)))
87
+ ORDER BY w.seq ASC LIMIT 1) IS NULL)`);
88
+ }
61
89
  for (const [label, statuses, not] of [
62
90
  ["statuses", query.statuses, ""],
63
91
  ["excludeStatuses", query.excludeStatuses, "NOT"]