@zq-silk/yui 2.6.0 → 2.8.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 (126) hide show
  1. package/README.md +23 -0
  2. package/dist/cli/commandCatalog.js +34 -7
  3. package/dist/cli/updateCommand.js +3 -0
  4. package/dist/commands/configCommands.js +1 -0
  5. package/dist/commands/globalRoleCommands.js +62 -35
  6. package/dist/commands/inboxCommands.js +372 -0
  7. package/dist/commands/roleSkillValidation.js +2 -0
  8. package/dist/commands/taskCommands.js +39 -7
  9. package/dist/commands/taskRoleRuntimeStatus.js +2 -0
  10. package/dist/context/currentSkills.js +48 -0
  11. package/dist/context/roleSessionContext.js +13 -16
  12. package/dist/context/runContextPack.js +50 -3
  13. package/dist/context/runInputContract.js +1 -1
  14. package/dist/context/sessionBootstrapManifest.js +27 -4
  15. package/dist/context/skillCatalog.js +2 -3
  16. package/dist/context/taskContext.js +38 -12
  17. package/dist/controlPlaneCli.js +58 -7
  18. package/dist/controller/controller.js +8 -2
  19. package/dist/controller/fileSchedulerStoreAdapter.js +26 -6
  20. package/dist/controller/globalInputDelivery.js +86 -16
  21. package/dist/controller/globalRuntimeAttention.js +136 -6
  22. package/dist/controller/operatorNotification.js +6 -1
  23. package/dist/controller/providerRetryDelivery.js +3 -5
  24. package/dist/controller/runtime.js +6 -2
  25. package/dist/controller/structuredProviderObservation.js +51 -15
  26. package/dist/controller/taskHostTerminalRecovery.js +33 -0
  27. package/dist/doctor/doctor.js +25 -2
  28. package/dist/executor/effectiveLaunch.js +2 -2
  29. package/dist/executor/fileRoleLaunchPlanner.js +8 -15
  30. package/dist/interaction/notificationPresentation.js +2 -2
  31. package/dist/interaction/operatorPresentation.js +1 -1
  32. package/dist/message/globalInputBacklog.js +69 -0
  33. package/dist/message/globalMessageQuery.js +1 -0
  34. package/dist/message/inboxReceipt.js +62 -0
  35. package/dist/message/message.js +6 -1
  36. package/dist/message/messageContinuation.js +34 -13
  37. package/dist/message/taskInboxOwnership.js +43 -0
  38. package/dist/models/modelManagement.js +469 -0
  39. package/dist/models/modelManagementConfig.js +46 -0
  40. package/dist/models/modelManagementFiles.js +90 -0
  41. package/dist/models/modelManagementHttp.js +90 -0
  42. package/dist/models/modelManagementTypes.js +1 -0
  43. package/dist/nativeAgent/session/backends.js +1 -1
  44. package/dist/observability/runtimeIdentity.js +1 -1
  45. package/dist/output/boundedRead.js +43 -14
  46. package/dist/resources/sqliteResourceRegistry.js +1 -1
  47. package/dist/runtime/agentEndpointIdentity.js +8 -4
  48. package/dist/runtime/agentEndpointOwnership.js +5 -6
  49. package/dist/runtime/agentHost.js +165 -24
  50. package/dist/runtime/agentHostImplementation.js +18 -0
  51. package/dist/runtime/managedCaller.js +25 -1
  52. package/dist/runtime/managedIdentity.js +1 -1
  53. package/dist/runtime/providerRetry.js +19 -1
  54. package/dist/runtime/sessionCaller.js +53 -0
  55. package/dist/scheduler/operatorEvent.js +49 -0
  56. package/dist/scheduler/operatorInputNotificationProcessor.js +5 -4
  57. package/dist/scheduler/taskWake.js +5 -0
  58. package/dist/sqlite/driver.js +108 -0
  59. package/dist/storage/contextRecords.js +28 -0
  60. package/dist/storage/sqliteSchema.js +21 -1
  61. package/dist/storage/sqliteStore.js +64 -1
  62. package/dist/storage/storageSchema.js +1 -1
  63. package/dist/storage/storageVersions.js +1 -1
  64. package/dist/storage/storeRpc.js +1 -0
  65. package/dist/storage/taskStore.js +4 -0
  66. package/dist/storage/upgrade/upgradeOrchestrator.js +1 -1
  67. package/dist/task/completionReadiness.js +13 -6
  68. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  69. package/dist/web/assets/assetManifest.js +28 -2
  70. package/dist/web/assets/client/app/manage.js +6 -0
  71. package/dist/web/assets/client/app/models.js +20 -0
  72. package/dist/web/assets/client/app/settings.js +41 -9
  73. package/dist/web/assets/client/i18n/en.js +180 -2
  74. package/dist/web/assets/client/i18n/zh-CN.js +178 -2
  75. package/dist/web/assets/client/index.js +8 -0
  76. package/dist/web/assets/client/layout/manageShell.js +94 -0
  77. package/dist/web/assets/client/lib/api.js +13 -1
  78. package/dist/web/assets/client/lib/i18n.js +5 -3
  79. package/dist/web/assets/client/views/dock/conversation.js +231 -80
  80. package/dist/web/assets/client/views/models.js +777 -0
  81. package/dist/web/assets/client/views/skills.js +22 -15
  82. package/dist/web/assets/shared/icons.js +2 -0
  83. package/dist/web/assets/shared/manageNav.js +13 -0
  84. package/dist/web/assets/shell/manage.js +54 -0
  85. package/dist/web/assets/shell/models.js +7 -0
  86. package/dist/web/assets/shell/settings.js +1 -35
  87. package/dist/web/assets/shell/settingsContent.js +30 -0
  88. package/dist/web/assets/shell/sidebar.js +1 -0
  89. package/dist/web/assets/styles/index.js +6 -0
  90. package/dist/web/assets/styles/views/conversation.js +133 -0
  91. package/dist/web/assets/styles/views/manage.js +71 -0
  92. package/dist/web/assets/styles/views/models.js +129 -0
  93. package/dist/web/assets/styles/views/settings.js +28 -14
  94. package/dist/web/assets/styles/views/sidebar.js +1 -0
  95. package/dist/web/webServer.js +59 -9
  96. package/docs/cli-information-contract.md +42 -11
  97. package/docs/cli-information-contract.zh-CN.md +28 -7
  98. package/docs/managed-turn-and-session-runtime.md +127 -5
  99. package/docs/managed-turn-and-session-runtime.zh-CN.md +83 -3
  100. package/docs/release-workflow.md +105 -9
  101. package/docs/release-workflow.zh-CN.md +73 -5
  102. package/docs/storage-baseline.md +22 -7
  103. package/docs/storage-baseline.zh-CN.md +14 -5
  104. package/docs/task-delivery.md +17 -8
  105. package/docs/task-delivery.zh-CN.md +11 -5
  106. package/docs/testing/review-evaluation.md +148 -0
  107. package/docs/testing/review-evaluation.zh-CN.md +117 -0
  108. package/docs/testing/verification-levels.md +26 -4
  109. package/docs/testing/verification-levels.zh-CN.md +11 -1
  110. package/i18n/README.zh-CN.md +19 -0
  111. package/package.json +5 -2
  112. package/skills/yui-leader/SKILL.md +28 -11
  113. package/skills/yui-leader/VERSION +1 -0
  114. package/skills/yui-leader/references/execution.md +29 -0
  115. package/skills/yui-leader/references/replicated-execution.md +7 -0
  116. package/skills/yui-operator/SKILL.md +15 -3
  117. package/skills/yui-operator/VERSION +1 -0
  118. package/skills/yui-operator/references/task-delivery.md +2 -0
  119. package/skills/yui-reviewer/SKILL.md +43 -1
  120. package/skills/yui-reviewer/VERSION +1 -0
  121. package/skills/yui-runtime/SKILL.md +144 -15
  122. package/skills/yui-runtime/VERSION +1 -0
  123. package/skills/yui-runtime/references/publication.md +16 -5
  124. package/skills/yui-runtime/references/recovery.md +24 -5
  125. package/skills/yui-worker/SKILL.md +8 -1
  126. package/skills/yui-worker/VERSION +1 -0
@@ -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 {};
@@ -1,4 +1,4 @@
1
- import Database from 'better-sqlite3';
1
+ import Database from "../../sqlite/driver.js";
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { isAbsolute, resolve } from 'node:path';
4
4
  import { existsSync, lstatSync } from 'node:fs';
@@ -12,7 +12,7 @@ import { createHash } from "node:crypto";
12
12
  import { spawnSync } from "node:child_process";
13
13
  import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
14
14
  import { join } from "node:path";
15
- import Database from "better-sqlite3";
15
+ import Database from "../sqlite/driver.js";
16
16
  import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "../storage/storageVersions.js";
17
17
  import { inspectStorageSchema } from "../storage/storageSchema.js";
18
18
  import { resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
@@ -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.
@@ -13,7 +13,7 @@
13
13
  import { existsSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { isDeepStrictEqual } from "node:util";
16
- import Database from "better-sqlite3";
16
+ import Database from "../sqlite/driver.js";
17
17
  import { validateSqliteSchema } from "../storage/sqliteSchema.js";
18
18
  import { emptyResourceRegistry, parseResourceRegistryState } from "./resourceRegistry.js";
19
19
  import { RESOURCE_REGISTRY_SCHEMA_VERSION } from "./resourceTypes.js";
@@ -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";
@@ -93,6 +94,9 @@ export async function runAgentHost(input) {
93
94
  let activeRunPayload;
94
95
  let lastTerminalPayload;
95
96
  let lastTerminal;
97
+ // One exact direct Turn witnessed by this Endpoint under its original fence.
98
+ // This is recovery evidence, never ownership of the Desktop client's input.
99
+ let directObservation;
96
100
  let activeRunAttemptId;
97
101
  let activeNativeTurnId;
98
102
  let codexClientAttachedAt;
@@ -166,6 +170,7 @@ export async function runAgentHost(input) {
166
170
  const handleStarted = (started, observedPayload = sessionPayload) => {
167
171
  if (started.clientOwned || observedPayload === undefined)
168
172
  return;
173
+ const observedAuthority = authority === undefined ? undefined : { ...authority };
169
174
  updateSnapshot(hostSnapshot("busy", {
170
175
  adapterId: observedPayload.providerControl?.adapterId,
171
176
  processInstanceId: session?.processInstanceId,
@@ -175,6 +180,25 @@ export async function runAgentHost(input) {
175
180
  ...authorityFields()
176
181
  }));
177
182
  void enqueueSerialized(async () => {
183
+ // Desktop can start another Turn before the Controller's periodic pass.
184
+ // Persist the previous exact terminal before replacing its bounded cache
185
+ // or publishing the new start. Never clear the newer native writer.
186
+ const previous = directObservation;
187
+ if (previous?.terminal !== undefined && authority !== undefined
188
+ && sameProviderAuthorityFence(previous.authority, authority)
189
+ && previous.started.nativeSessionId === session?.nativeSessionId) {
190
+ await publishStructuredProviderTerminal({
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.
195
+ });
196
+ }
197
+ if (session !== undefined && started.nativeSessionId === session.nativeSessionId
198
+ && started.conversationId === session.conversationId && observedAuthority !== undefined
199
+ && authority !== undefined && sameProviderAuthorityFence(observedAuthority, authority)) {
200
+ directObservation = { started, payload: observedPayload, authority: observedAuthority };
201
+ }
178
202
  await publishStructuredProviderStarted({
179
203
  home: input.home,
180
204
  environment: observedPayload.environment,
@@ -194,6 +218,14 @@ export async function runAgentHost(input) {
194
218
  // AgentRun audit record. It has no WorkItem ownership and no managed input
195
219
  // fence; the runtime observer records only visible input/output.
196
220
  void enqueueSerialized(async () => {
221
+ const direct = directObservation;
222
+ if (direct !== undefined && authority !== undefined
223
+ && sameProviderAuthorityFence(direct.authority, authority)
224
+ && direct.started.nativeSessionId === terminal.nativeSessionId
225
+ && direct.started.conversationId === terminal.conversationId
226
+ && direct.started.nativeTurnId === terminal.nativeTurnId) {
227
+ direct.terminal = terminal;
228
+ }
197
229
  await publishStructuredProviderTerminal({
198
230
  home: input.home,
199
231
  environment: busyPayload.environment,
@@ -274,6 +306,7 @@ export async function runAgentHost(input) {
274
306
  conversationId: terminal.conversationId,
275
307
  ...authorityFields()
276
308
  }));
309
+ signalRoleMailbox(input.home, currentPayload);
277
310
  promptHuman();
278
311
  }
279
312
  catch (error) {
@@ -565,6 +598,8 @@ export async function runAgentHost(input) {
565
598
  throw new Error("Agent Host accepts only managed Provider control launches.");
566
599
  }
567
600
  if (activeRunPayload !== undefined
601
+ || sessionPayload !== undefined
602
+ && (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
568
603
  || ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
569
604
  throw new Error("Agent Host still owns an unsettled Provider Turn.");
570
605
  }
@@ -595,8 +630,7 @@ export async function runAgentHost(input) {
595
630
  || providerControl.mode !== "resume"
596
631
  || providerControl.nativeSessionId !== session.nativeSessionId
597
632
  || (providerControl.endpointImplementation !== undefined
598
- && (providerControl.endpointImplementation.id !== session.configuration.implementation.id
599
- || providerControl.endpointImplementation.generation !== session.configuration.implementation.generation))) {
633
+ && providerControl.endpointImplementation.id !== session.configuration.implementation.id)) {
600
634
  throw new Error("Agent Host launch does not match its live Provider Conversation.");
601
635
  }
602
636
  sessionPayload = next;
@@ -609,9 +643,8 @@ export async function runAgentHost(input) {
609
643
  }
610
644
  // The Controller revalidates the adopted execution environment while
611
645
  // redeeming this launch; a persistent Host must not reopen its schema.
612
- // Pin before opening: the Session holds this exact generation for every
613
- // later Turn. A pinned reference naming code this process is not running
614
- // 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.
615
648
  endpointLease ??= endpointOwner.pin(providerControl.adapterId, providerControl.endpointImplementation);
616
649
  endpointAdapterId = providerControl.adapterId;
617
650
  const started = await (providerControl.mode === "new"
@@ -621,6 +654,7 @@ export async function runAgentHost(input) {
621
654
  await recordProviderConnection(input.home, next, started.session);
622
655
  lastTerminalPayload = undefined;
623
656
  lastTerminal = undefined;
657
+ directObservation = undefined;
624
658
  // Recoverable means a later process can rebind this Conversation by
625
659
  // its native id. The Endpoint answers that from the Driver capability
626
660
  // or, where the protocol settles it per connection, from what this
@@ -756,6 +790,7 @@ export async function runAgentHost(input) {
756
790
  throw new Error("Agent Host rejected a stale Provider writer fence.");
757
791
  }
758
792
  if (activeRunPayload !== undefined || snapshot.state === "settling"
793
+ || (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
759
794
  || session.inspect().activeNativeTurnId !== undefined) {
760
795
  operation.failure = providerDeliveryFailure({
761
796
  detail: "Agent Host still owns an unsettled Provider Turn.",
@@ -1012,7 +1047,9 @@ export async function runAgentHost(input) {
1012
1047
  if (session === undefined || request.nativeSessionId !== session.nativeSessionId) {
1013
1048
  throw new Error("Agent Host authority targets a different Provider Conversation.");
1014
1049
  }
1015
- if (activeRunPayload !== undefined
1050
+ if (activeRunPayload !== undefined || session.inspect().activeNativeTurnId !== undefined
1051
+ || sessionPayload !== undefined
1052
+ && (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
1016
1053
  || ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
1017
1054
  throw new Error("Agent Host authority cannot transfer while a AgentRun is unsettled.");
1018
1055
  }
@@ -1088,7 +1125,9 @@ export async function runAgentHost(input) {
1088
1125
  return enqueueSerialized(async () => {
1089
1126
  const current = session;
1090
1127
  if (!current?.nativeAccess || current.nativeSessionId !== request.nativeSessionId
1091
- || !sessionPayload || activeNativeTurnId || authority?.owner !== "controller"
1128
+ || !sessionPayload || activeRunPayload || activeNativeTurnId
1129
+ || (structuredProviderEventDelivery(input.home, sessionPayload.environment).unpersisted ?? 0) > 0
1130
+ || authority?.owner !== "controller"
1092
1131
  || (sessionPayload.environment.YUI_SESSION_SCOPE === "task" && sessionPayload.environment.YUI_ROLE !== "leader")
1093
1132
  || (sessionPayload.environment.YUI_SESSION_SCOPE === "global" && sessionPayload.environment.YUI_ROLE !== "operator")) {
1094
1133
  throw new Error("Model switching requires the idle current controlled Codex Session, without a native terminal writer.");
@@ -1157,6 +1196,73 @@ export async function runAgentHost(input) {
1157
1196
  }
1158
1197
  return controlResult("accepted", { ...snapshot, nativeTui: !!nativeTui });
1159
1198
  }
1199
+ if (request.type === "reconcile-turn") {
1200
+ if (session === undefined || session.nativeSessionId !== request.nativeSessionId
1201
+ || authority === undefined || !sameProviderAuthorityFence(authority, request.authority)) {
1202
+ throw new Error("Terminal reconciliation targets a different Session or authority.");
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
+ });
1225
+ const direct = directObservation;
1226
+ const directMatch = direct?.terminal !== undefined
1227
+ && request.nativeTurnId !== undefined
1228
+ && request.attemptId === `direct:${request.nativeTurnId}`
1229
+ && direct.started.nativeTurnId === request.nativeTurnId
1230
+ && direct.started.nativeSessionId === request.nativeSessionId
1231
+ && sameProviderAuthorityFence(direct.authority, request.authority);
1232
+ const terminal = directMatch ? direct.terminal : lastTerminal;
1233
+ const terminalPayload = directMatch ? direct.payload : lastTerminalPayload;
1234
+ if (terminal === undefined || terminalPayload === undefined
1235
+ || terminal.nativeSessionId !== request.nativeSessionId
1236
+ || !directMatch && (terminal.clientOwned !== true || terminal.attemptId !== request.attemptId)
1237
+ || request.nativeTurnId !== undefined && terminal.nativeTurnId !== request.nativeTurnId) {
1238
+ return { ...controlResult("status", snapshot),
1239
+ reconciliation: recovered ? "terminal-republished" : "unobserved" };
1240
+ }
1241
+ // Serialize with submission/terminal persistence. The original Host and
1242
+ // original launch envelope remain the observer, including the full result.
1243
+ await enqueueSerialized(async () => {
1244
+ if (session?.nativeSessionId !== request.nativeSessionId || authority === undefined
1245
+ || !sameProviderAuthorityFence(authority, request.authority)
1246
+ || directMatch && directObservation !== direct) {
1247
+ throw new Error("Session changed before terminal reconciliation.");
1248
+ }
1249
+ await publishStructuredProviderTerminal({
1250
+ home: input.home, environment: terminalPayload.environment, terminal
1251
+ });
1252
+ if (!directMatch && activeRunAttemptId === terminal.attemptId) {
1253
+ activeRunPayload = undefined;
1254
+ activeRunAttemptId = undefined;
1255
+ activeNativeTurnId = undefined;
1256
+ updateSnapshot(hostSnapshot(session.inspect().activeNativeTurnId === undefined ? "idle" : "busy", {
1257
+ adapterId: session.adapterId, processInstanceId: session.processInstanceId,
1258
+ nativeSessionId: session.nativeSessionId, conversationId: session.conversationId,
1259
+ ...authorityFields()
1260
+ }));
1261
+ }
1262
+ signalRoleMailbox(input.home, terminalPayload);
1263
+ });
1264
+ return { ...controlResult("status", snapshot), reconciliation: "terminal-republished" };
1265
+ }
1160
1266
  if (request.type === "status") {
1161
1267
  // Read the Agent's configuration at answer time, not from the stored
1162
1268
  // snapshot. The Agent may have changed it since the last state
@@ -1461,6 +1567,22 @@ export async function inspectAgentHost(input) {
1461
1567
  });
1462
1568
  return result.snapshot;
1463
1569
  }
1570
+ export async function reconcileAgentHostTurn(input) {
1571
+ // Periodic observation must not hold the scheduler behind an unavailable Host.
1572
+ // A timeout is only an observation failure, never permission to submit again.
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);
1585
+ }
1464
1586
  export async function waitForAgentHostLaunchAck(input) {
1465
1587
  const deadline = Date.now() + (input.timeoutMs ?? AGENT_HOST_READY_TIMEOUT_MS);
1466
1588
  let lastError;
@@ -1616,7 +1738,12 @@ export async function openAgentHostControl(home, payload, snapshot, dispatch) {
1616
1738
  ...result,
1617
1739
  snapshot: {
1618
1740
  ...result.snapshot,
1619
- ...(payload.environment.YUI_SESSION_SCOPE !== "task" ? {} : {
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
+ ],
1746
+ ...(!["task", "global"].includes(payload.environment.YUI_SESSION_SCOPE ?? "") ? {} : {
1620
1747
  eventDelivery: structuredProviderEventDelivery(home, payload.environment, result.snapshot.nativeSessionId)
1621
1748
  })
1622
1749
  }
@@ -1691,7 +1818,8 @@ function boundControlResponse(result) {
1691
1818
  // visible through an otherwise successful status request.
1692
1819
  result = {
1693
1820
  ...controlResult(result.outcome, validateSnapshot(result.snapshot), result.failure === undefined ? undefined : providerDeliveryFailure(result.failure)),
1694
- ...(result.cancellation === undefined ? {} : { cancellation: result.cancellation })
1821
+ ...(result.cancellation === undefined ? {} : { cancellation: result.cancellation }),
1822
+ ...(result.reconciliation === undefined ? {} : { reconciliation: result.reconciliation })
1695
1823
  };
1696
1824
  if (withinControlBound(result))
1697
1825
  return result;
@@ -1709,29 +1837,29 @@ function boundControlResponse(result) {
1709
1837
  };
1710
1838
  // Never make an omitted option list look like a complete empty enumeration.
1711
1839
  if (result.snapshot.runConfiguration?.status === "observed") {
1712
- result = controlResult(result.outcome, validateSnapshot({
1713
- ...result.snapshot,
1714
- runConfiguration: unknownAgentRunConfiguration("The Agent's reported configuration is too large for the Agent Host "
1715
- + "control response, so it could not be carried in full and none of it "
1716
- + "is shown rather than part of it.")
1717
- }), result.failure);
1840
+ result = { ...result, ...controlResult(result.outcome, validateSnapshot({
1841
+ ...result.snapshot,
1842
+ runConfiguration: unknownAgentRunConfiguration("The Agent's reported configuration is too large for the Agent Host "
1843
+ + "control response, so it could not be carried in full and none of it "
1844
+ + "is shown rather than part of it.")
1845
+ }), result.failure) };
1718
1846
  if (withinControlBound(result))
1719
1847
  return result;
1720
1848
  }
1721
- let bounded = controlResult(result.outcome, {
1722
- ...result.snapshot,
1723
- ...(result.snapshot.detail === undefined ? {} : { detail: clip(result.snapshot.detail, 1200) })
1724
- }, result.failure === undefined ? undefined : { ...result.failure, detail: clip(result.failure.detail, 1200) });
1849
+ let bounded = { ...result, ...controlResult(result.outcome, {
1850
+ ...result.snapshot,
1851
+ ...(result.snapshot.detail === undefined ? {} : { detail: clip(result.snapshot.detail, 1200) })
1852
+ }, result.failure === undefined ? undefined : { ...result.failure, detail: clip(result.failure.detail, 1200) }) };
1725
1853
  const raw = bounded.failure?.raw;
1726
1854
  if (withinControlBound(bounded) || raw === undefined)
1727
1855
  return bounded;
1728
1856
  let chars = raw.length;
1729
1857
  while (!withinControlBound(bounded) && chars > 0) {
1730
1858
  chars = Math.floor(chars / 2);
1731
- bounded = controlResult(bounded.outcome, bounded.snapshot, {
1732
- ...bounded.failure,
1733
- raw: clip(raw, chars)
1734
- });
1859
+ bounded = { ...bounded, ...controlResult(bounded.outcome, bounded.snapshot, {
1860
+ ...bounded.failure,
1861
+ raw: clip(raw, chars)
1862
+ }) };
1735
1863
  }
1736
1864
  return bounded;
1737
1865
  }
@@ -1764,6 +1892,14 @@ function validateControl(control) {
1764
1892
  }
1765
1893
  if (control.type === "status")
1766
1894
  return Object.freeze({ ...control });
1895
+ if (control.type === "reconcile-turn") {
1896
+ validateIdentity(control.nativeSessionId, "native Session id");
1897
+ validateIdentity(control.attemptId, "Provider input attempt id");
1898
+ if (control.nativeTurnId !== undefined)
1899
+ validateIdentity(control.nativeTurnId, "native Turn id");
1900
+ validateProviderAuthorityFence(control.authority);
1901
+ return Object.freeze({ ...control });
1902
+ }
1767
1903
  if (control.type === "native-open" || control.type === "native-respond" || control.type === "native-model") {
1768
1904
  validateIdentity(control.nativeSessionId, "native Session id");
1769
1905
  if (control.type === "native-model" && (typeof control.model !== "string"
@@ -1867,6 +2003,11 @@ function validateSnapshot(snapshot) {
1867
2003
  && snapshot.inputAcceptance !== "provider" && snapshot.inputAcceptance !== "transport") {
1868
2004
  throw new Error("Agent Host input acceptance evidence is invalid.");
1869
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
+ }
1870
2011
  const authorityFields = [
1871
2012
  snapshot.authorityEpoch,
1872
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
+ });