@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
@@ -4,14 +4,14 @@ import { taskNotFound, usageError } from "../errors/cliError.js";
4
4
  import { expandTaskMessageResult } from "../message/message.js";
5
5
  import { resolveManagedTaskReader } from "../runtime/managedCaller.js";
6
6
  import { hasManagedIdentity } from "../runtime/managedIdentity.js";
7
- import { providerRetryProjection } from "../runtime/providerRetry.js";
7
+ import { providerRetrySummary } from "../runtime/providerRetry.js";
8
8
  import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
9
9
  import { retiredTaskRecordIds, TASK_RECORD_RETIRED_EVENT } from "../task/taskRecordRetirement.js";
10
10
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
11
11
  import { contextContentDigest } from "./contextSnapshot.js";
12
12
  import { buildRunContextPack } from "./runContextPack.js";
13
13
  import { sourceRunContextValue } from "./sourceRunContext.js";
14
- import { boundedDocument, recordPage } from "../output/boundedRead.js";
14
+ import { boundedDocument, fitsReadBudget, summaryText, recordPage } from "../output/boundedRead.js";
15
15
  const MAX_RECORDS = 64;
16
16
  const MAX_VALUE_BYTES = 2048;
17
17
  const MAX_PAGE_BYTES = 24 * 1024;
@@ -41,13 +41,13 @@ export function readTaskContext(store, taskId, environment = {}) {
41
41
  if (entry.ref.store === "run")
42
42
  runtimeEvents ??= reader.listEventsByType(taskId, ["runtime.agent-error"]);
43
43
  const valueBytes = Buffer.byteLength(JSON.stringify(entry.value));
44
- const content = valueBytes > MAX_VALUE_BYTES || ["task-message", "run", "work-item", "project-knowledge"].includes(entry.ref.store)
44
+ const content = valueBytes > MAX_VALUE_BYTES || ["role", "task-message", "run", "work-item", "project-knowledge"].includes(entry.ref.store)
45
45
  ? { ref: entry.ref, summary: summarize(entry.value), omitted: true }
46
46
  : { ...entry, omitted: false };
47
47
  const record = { ...content, ...(entry.ref.store === "run" ? {
48
48
  execution: runExecutionObservation(entry.value, reader.getTaskRoleSessionSet(taskId, entry.value.roleName)?.providerBinding, runtimeEvents)
49
49
  } : {}), ...(entry.ref.store === "role" ? {
50
- providerRetry: providerRetryProjection(reader.getTaskRoleSessionSet(taskId, entry.value.name)?.providerBinding)
50
+ providerRetry: providerRetrySummary(reader.getTaskRoleSessionSet(taskId, entry.value.name)?.providerBinding)
51
51
  } : {}) };
52
52
  const size = Buffer.byteLength(JSON.stringify(record));
53
53
  if (bytes + size > MAX_PAGE_BYTES)
@@ -55,7 +55,7 @@ export function readTaskContext(store, taskId, environment = {}) {
55
55
  records.push(record);
56
56
  bytes += size;
57
57
  }
58
- return {
58
+ const result = {
59
59
  taskId, coreCursor: encode(cursor), throughCursor: encode(cursor),
60
60
  view: "current",
61
61
  collections: Object.entries(sources.reduce((counts, source) => {
@@ -79,6 +79,18 @@ export function readTaskContext(store, taskId, environment = {}) {
79
79
  limits: { records: MAX_RECORDS, valueBytes: MAX_VALUE_BYTES, pageBytes: 32 * 1024, recordBytes: MAX_PAGE_BYTES,
80
80
  attentionRefsPerCategory: MAX_ATTENTION_REFS, attentionRefBytesPerCategory: MAX_VALUE_BYTES }
81
81
  };
82
+ // Account for nested retry facts, collection counts, commands and JSON
83
+ // structure together. Exact omitted records stay discoverable by family.
84
+ while (!fitsReadBudget(result) && records.length > 0) {
85
+ records.pop();
86
+ result.collections.forEach(collection => {
87
+ collection.returned = records.filter(record => record.ref.store === collection.store).length;
88
+ });
89
+ result.omitted = { records: count - records.length, values: records.filter(record => record.omitted).length };
90
+ }
91
+ if (!fitsReadBudget(result))
92
+ throw usageError("Context identities exceed the response budget; use scoped discovery.");
93
+ return result;
82
94
  });
83
95
  }
84
96
  function summarizeAttention(store, taskId, allow) {
@@ -93,7 +105,8 @@ function summarizeAttention(store, taskId, allow) {
93
105
  ["messages", "task-message", undefined]
94
106
  ]) {
95
107
  const page = store.queryContextRecords(taskId, {
96
- family, statuses, descending: category === "messages", ids: allowedIds(allow, family), limit: MAX_ATTENTION_REFS
108
+ family, statuses, descending: category === "messages",
109
+ unhandledMessages: category === "messages", ids: allowedIds(allow, family), limit: MAX_ATTENTION_REFS
97
110
  });
98
111
  const summary = attention[category];
99
112
  summary.count = page.count;
@@ -262,11 +275,11 @@ export async function withContextObservations(core, providers, timeoutMs = 250)
262
275
  if (!["known", "partial", "unknown"].includes(value.coverage)
263
276
  || Buffer.byteLength(JSON.stringify(value)) > MAX_VALUE_BYTES)
264
277
  throw new Error("unbounded observation");
265
- return { source: provider.source.slice(0, 200), observedAt: new Date().toISOString(),
278
+ return { source: summaryText(provider.source, 200), observedAt: new Date().toISOString(),
266
279
  coverage: value.coverage, status: "available", value: JSON.parse(JSON.stringify(value.value)) };
267
280
  }
268
281
  catch {
269
- return { source: provider.source.slice(0, 200), observedAt: new Date().toISOString(),
282
+ return { source: summaryText(provider.source, 200), observedAt: new Date().toISOString(),
270
283
  coverage: "unknown", status: "unavailable" };
271
284
  }
272
285
  finally {
@@ -275,7 +288,18 @@ export async function withContextObservations(core, providers, timeoutMs = 250)
275
288
  controller.abort();
276
289
  }
277
290
  }));
278
- return { ...core, observations };
291
+ const result = { ...core, observations: [] };
292
+ for (const observation of observations) {
293
+ result.observations.push(observation);
294
+ if (!fitsReadBudget(result)) {
295
+ result.observations.pop();
296
+ const { value: _value, ...identity } = observation;
297
+ result.observations.push({ ...identity, omitted: true });
298
+ if (!fitsReadBudget(result))
299
+ result.observations.pop();
300
+ }
301
+ }
302
+ return { ...result, omittedObservations: observations.length - result.observations.length };
279
303
  }
280
304
  /** Shared current/historical Session read identity; discovery may only narrow it. */
281
305
  export function resolveContextReader(store, environment) {
@@ -513,7 +537,7 @@ function authorizedSources(store, { task, allow, caller }, current = false, requ
513
537
  }
514
538
  if (!current)
515
539
  query("change-set");
516
- query("task-message", { descending: true });
540
+ query("task-message", { descending: true, unhandledMessages: current });
517
541
  query("run", { descending: true, ...(current ? { statuses: ["active"] } : {}) });
518
542
  for (const id of allowedIds(allow, "source-turn") ?? []) {
519
543
  const run = store.getRun(taskId, id);
@@ -585,8 +609,8 @@ function freeze(value) {
585
609
  }
586
610
  function summarize(value) {
587
611
  const record = value;
588
- return ["title", "scope", "expiresWhen", "version", "objective", "question", "summary", "body", "status", "type"]
612
+ return summaryText(["title", "name", "activeAgentId", "scope", "expiresWhen", "version", "objective", "question", "summary", "body", "status", "type"]
589
613
  .flatMap((key) => typeof record[key] === "string" || (key === "version" && typeof record[key] === "number")
590
614
  ? [`${key}: ${record[key]}`] : [])
591
- .join("; ").slice(0, 400);
615
+ .join("; "));
592
616
  }
@@ -22,7 +22,11 @@ import { allowsInteractiveSelection, resolveInteractiveArguments } from "./cli/i
22
22
  import { routeInvocation } from "./cli/invocationRouter.js";
23
23
  import { operatorOfflineCommand, taskDiagnosticTarget } from "./cli/managedDiagnostics.js";
24
24
  import { assertConfigurationAuthority, assertTaskInvocationScope } from "./cli/invocationAuthority.js";
25
- import { requireManagedGlobalCaller, ManagedRuntimeDriftError } from "./runtime/managedCaller.js";
25
+ import { requireManagedGlobalCaller, requireManagedGlobalReader, ManagedRuntimeDriftError } from "./runtime/managedCaller.js";
26
+ import { sessionCallerEnvironment, requireManagedCaller } from "./runtime/sessionCaller.js";
27
+ import { maintenanceCallerEnvironment } from "./storage/maintenanceCaller.js";
28
+ import { checkCurrentSkills, SKILL_REUSE_RULE } from "./context/currentSkills.js";
29
+ import { roleSkillIds } from "./context/roleSessionContext.js";
26
30
  import { hasManagedIdentity } from "./runtime/managedIdentity.js";
27
31
  import { resolveOperatorWizardArguments } from "./cli/operatorWizard.js";
28
32
  import { resolveGlobalRoleAgentConfigurationArguments, resolveRoleWizardArguments } from "./cli/roleWizard.js";
@@ -63,6 +67,7 @@ import { FileCompletionManager } from "./completion/fileCompletionManager.js";
63
67
  import { CONFIG_DOMAINS } from "./config/configCatalog.js";
64
68
  import { resolveTmuxBin, resolveTmuxHistoryLimit } from "./config/yuiConfig.js";
65
69
  import { checkSessionSkills, readSessionBootstrapManifest } from "./context/sessionBootstrapManifest.js";
70
+ import { boundedDocument, readOptions } from "./output/boundedRead.js";
66
71
  import { assertFileTaskControllerStorageCompatible, ensureFileTaskController, FileTaskWorkflowRuntime, refreshRunningFileTaskControllerConfiguration, refreshRunningFileTaskControllerEnvironment, restartFileTaskController, stopFileTaskController } from "./controller/clientRuntime.js";
67
72
  import { FileSchedulerStoreAdapter } from "./controller/fileSchedulerStoreAdapter.js";
68
73
  import { createControllerIntegrationJobPort } from "./controller/jobClient.js";
@@ -796,13 +801,22 @@ export async function main() {
796
801
  }
797
802
  if (resolved[0] === "session") {
798
803
  if (resolved[1] === "skills") {
799
- if (resolved.length !== 2)
800
- throw usageError("Usage: yui session skills --json");
804
+ const parsed = readOptions(resolved.slice(2), ["--cursor"]);
805
+ if (parsed.positionals.length !== 0)
806
+ throw usageError("Usage: yui session skills [--cursor <cursor>] --json");
801
807
  const path = process.env.YUI_SESSION_MANIFEST;
802
- if (!["task", "global"].includes(process.env.YUI_SESSION_SCOPE ?? "") || path === undefined) {
803
- throw usageError("Skill checks require the current managed Session and its Manifest.");
804
- }
805
- const data = checkSessionSkills(home, path);
808
+ const caller = requireManagedCaller(store, process.env);
809
+ const set = caller.scope === "global" ? store.getGlobalRoleSessionSet(caller.roleName)
810
+ : store.getTaskRoleSessionSet(caller.taskId, caller.roleName);
811
+ const effective = set.sessions[caller.agentId].effective;
812
+ const kind = caller.scope === "global" ? caller.roleName === "operator" ? "operator" : "global"
813
+ : effective.reviewRoundId !== undefined ? "reviewer" : caller.roleName === "leader" ? "leader" : "worker";
814
+ const checked = path === undefined ? {
815
+ skills: checkCurrentSkills(home, roleSkillIds(effective.context, kind)),
816
+ profile: effective.context, identity: caller, rule: SKILL_REUSE_RULE,
817
+ historicalPackagesAreAuditOnly: true
818
+ } : checkSessionSkills(home, path);
819
+ const data = boundedDocument(checked, "session:current-skills", parsed.cursor);
806
820
  emit(JSON.stringify(data, null, 2), false, data);
807
821
  return;
808
822
  }
@@ -1986,6 +2000,36 @@ async function preflightManagedTaskControlPlane() {
1986
2000
  if (taskFinalReviewInvocation.error !== undefined) {
1987
2001
  throw new Error(taskFinalReviewInvocation.error);
1988
2002
  }
2003
+ if (["doctor", "upgrade", "update"].includes(args[0] ?? "")) {
2004
+ const home = resolveYuiHome(process.env);
2005
+ Object.assign(process.env, maintenanceCallerEnvironment(home, process.env, args[0] !== "doctor"));
2006
+ if (!hasManagedIdentity(process.env)
2007
+ || (process.env.YUI_SESSION_SCOPE === "global" && process.env.YUI_ROLE === "operator")) {
2008
+ if (process.env.YUI_SESSION_SCOPE === "global") {
2009
+ const manifest = assertManagedSessionManifest(home, "global");
2010
+ if (manifest !== undefined && manifest.roleKind !== "operator") {
2011
+ throw new ManagedRuntimeDriftError("Managed global invocation does not match its Session Manifest.");
2012
+ }
2013
+ }
2014
+ if (taskFinalReviewInvocation.request !== undefined) {
2015
+ throw new Error("Task final-review contract cannot be established by Home maintenance.");
2016
+ }
2017
+ return { contract: undefined, verifiedStore: undefined };
2018
+ }
2019
+ }
2020
+ // Desktop clients may carry only their provider-native identity. Resolve
2021
+ // command-local hints before any public/Task/Global routing. This reads this
2022
+ // Home only, and never creates a Session or mutates its registration.
2023
+ if ((process.env.CODEX_THREAD_ID !== undefined || process.env.YUI_NATIVE_SESSION_ID !== undefined)
2024
+ && (process.env.YUI_SESSION_SCOPE === undefined || process.env.YUI_ROLE === undefined)) {
2025
+ const registry = openCurrentTaskStore(resolveYuiHome(process.env));
2026
+ try {
2027
+ Object.assign(process.env, sessionCallerEnvironment(registry, process.env));
2028
+ }
2029
+ finally {
2030
+ registry.close();
2031
+ }
2032
+ }
1989
2033
  if (process.env.YUI_SESSION_SCOPE !== "task") {
1990
2034
  if (process.env.YUI_SESSION_SCOPE === "global") {
1991
2035
  return await preflightManagedGlobalControlPlane();
@@ -2064,24 +2108,25 @@ async function preflightManagedGlobalControlPlane() {
2064
2108
  const home = resolveYuiHome(process.env);
2065
2109
  const manifest = assertManagedSessionManifest(home, "global");
2066
2110
  const expectedRoleKind = process.env.YUI_ROLE === "operator" ? "operator" : "global";
2067
- if (manifest.owner.scope !== "global"
2068
- || manifest.roleKind !== expectedRoleKind) {
2111
+ if (manifest !== undefined && (manifest.owner.scope !== "global"
2112
+ || manifest.roleKind !== expectedRoleKind)) {
2069
2113
  throw new ManagedRuntimeDriftError("Managed global invocation does not match its Session Manifest.");
2070
2114
  }
2071
- if (expectedRoleKind === "operator" && ["doctor", "upgrade", "update"].includes(args[0] ?? "")) {
2072
- // These commands inspect/adopt the storage contract themselves. Requiring
2073
- // current storage before reaching upgrade would make recovery impossible.
2074
- return { contract: undefined, verifiedStore: undefined };
2075
- }
2076
2115
  await assertRuntimeCoherence({ actualHome: home }, {
2077
2116
  checkController: !(expectedRoleKind === "operator" && operatorOfflineCommand(args))
2078
2117
  });
2079
2118
  const verifiedStore = openCurrentTaskStore(home);
2080
2119
  const internalCallback = args[0] === "internal"
2081
2120
  && ["agent-host", "session-notify", "runtime-hook"].includes(args[1] ?? "");
2082
- if (!internalCallback && !operatorOfflineCommand(args)) {
2121
+ if (!internalCallback) {
2122
+ const selfRead = args[0] === "session" && args[1] === "context" && args[2] === process.env.YUI_ROLE
2123
+ || args[0] === "role" && args[1] === "message" && ["list", "show"].includes(args[2] ?? "")
2124
+ && args[3] === process.env.YUI_ROLE;
2083
2125
  try {
2084
- requireManagedGlobalCaller(verifiedStore, process.env);
2126
+ if (selfRead)
2127
+ requireManagedGlobalReader(verifiedStore, process.env);
2128
+ else
2129
+ requireManagedGlobalCaller(verifiedStore, process.env);
2085
2130
  }
2086
2131
  catch (error) {
2087
2132
  verifiedStore.close();
@@ -2093,7 +2138,9 @@ async function preflightManagedGlobalControlPlane() {
2093
2138
  function assertManagedSessionManifest(home, scope) {
2094
2139
  const manifestPath = process.env.YUI_SESSION_MANIFEST;
2095
2140
  if (manifestPath === undefined) {
2096
- throw new ManagedRuntimeDriftError("Managed control-plane invocation requires its Session Manifest.");
2141
+ // A Manifest is a context pointer, not an authority credential. Native
2142
+ // identity is validated against the current Home immediately below.
2143
+ return undefined;
2097
2144
  }
2098
2145
  let manifest;
2099
2146
  try {
@@ -692,6 +692,7 @@ export class FileTaskController {
692
692
  #globalInputDelivery;
693
693
  #providerRetry;
694
694
  #continuationReconciler;
695
+ #hostTerminalRecovery;
695
696
  #current;
696
697
  #operatorCurrent;
697
698
  #runtimeObserverCurrent;
@@ -741,6 +742,7 @@ export class FileTaskController {
741
742
  this.#leaderWakeFence = options.leaderWakeFence;
742
743
  this.#jobSupervisor = options.jobSupervisor;
743
744
  this.#continuationReconciler = options.continuationReconciler;
745
+ this.#hostTerminalRecovery = options.hostTerminalRecovery;
744
746
  this.#globalInputDelivery = options.globalInputDelivery;
745
747
  this.#providerRetry = options.providerRetry;
746
748
  this.#signalScheduler = new MailboxScheduler(async (keys) => { await this.#requestPass({ kind: "dirty", keys }); }, {
@@ -995,6 +997,9 @@ export class FileTaskController {
995
997
  }
996
998
  }
997
999
  const firstRuntimeDrain = await this.#drainRuntimeEvents();
1000
+ if (scope.kind === "full" && this.#hostTerminalRecovery !== undefined) {
1001
+ await this.#observeMaintenance("Host terminal recovery", this.#hostTerminalRecovery);
1002
+ }
998
1003
  await this.#providerRetry?.reconcile();
999
1004
  await this.#globalInputDelivery?.({ reconcile: scope.kind === "full", now: this.#now() });
1000
1005
  for (const taskId of runtimeTaskFailureIds(firstRuntimeDrain)) {
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
- import { taskMessageInboxReceipt, unfinishedLeaderInbox } from "../message/taskInboxOwnership.js";
3
+ import { deliveredMessageWake, pinMessageRevisions, taskMessageInboxReceipt, unfinishedLeaderInbox } from "../message/taskInboxOwnership.js";
4
4
  import { contextSnapshotRef } from "../context/contextSnapshot.js";
5
5
  import { freezeRunContextSnapshot } from "../context/runContextPack.js";
6
6
  import { taskNotificationPresentation } from "../interaction/notificationPresentation.js";
@@ -29,7 +29,7 @@ import { activeRoleAgentBinding } from "../role/role.js";
29
29
  import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
30
30
  import { standardAgentError } from "../runtime/agentError.js";
31
31
  import { recordTaskAgentError } from "./taskAgentError.js";
32
- import { recordGlobalRuntimeAttention, recordGlobalRuntimeFailure } from "./globalRuntimeAttention.js";
32
+ import { recordGlobalRuntimeAttention, recordGlobalRuntimeFailure, recordGlobalRuntimeRecovery } from "./globalRuntimeAttention.js";
33
33
  import { queueOperatorNotification } from "./operatorNotification.js";
34
34
  import { claimOperatorAttention } from "../scheduler/operatorEvent.js";
35
35
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
@@ -450,6 +450,10 @@ export class FileSchedulerStoreAdapter {
450
450
  binding = settleProviderTurn(binding, { attemptId: receipt, nativeTurnId: fence.nativeTurnId,
451
451
  status: input.kind === "turn.completed" ? "completed" : input.kind === "turn.cancelled" ? "cancelled" : "failed",
452
452
  settledAt: at });
453
+ if (input.kind === "turn.completed")
454
+ recordGlobalRuntimeRecovery(store, fence.roleName, input.eventId, {
455
+ nativeSessionId: fence.nativeSessionId, evidence: `provider-structured:${receipt}`
456
+ }, now);
453
457
  if (fence.nativeTurnId !== undefined)
454
458
  sessions = rememberRoleAgentCompletedTurn(sessions, fence.agentId, fence.nativeSessionId, fence.nativeTurnId, new Date(at));
455
459
  }
@@ -1875,7 +1879,8 @@ export class FileSchedulerStoreAdapter {
1875
1879
  const session = sessions?.sessions[sessions.activeAgentId];
1876
1880
  const obsoleteIds = new Set();
1877
1881
  for (const message of store.listMessages(taskId)) {
1878
- if (pendingMessageIds.has(message.id) && taskMessageInboxReceipt(store, message) !== undefined) {
1882
+ if (pendingMessageIds.has(message.id) && (taskMessageInboxReceipt(store, message) !== undefined
1883
+ || deliveredMessageWake(store, message) !== undefined)) {
1879
1884
  obsoleteIds.add(message.id);
1880
1885
  }
1881
1886
  }
@@ -1925,7 +1930,7 @@ export class FileSchedulerStoreAdapter {
1925
1930
  const wakeId = store.nextTaskWakeId(taskId);
1926
1931
  const wake = createTaskWake({
1927
1932
  id: wakeId, taskId, reasons: mailbox.pending.reasons,
1928
- refs: selectedRefs,
1933
+ refs: pinMessageRevisions(store, taskId, selectedRefs),
1929
1934
  fromCursor: latest?.toCursor ?? task.createdAt,
1930
1935
  toCursor: new Date(Math.max(now.getTime(), Date.parse(mailbox.pending.lastQueuedAt))).toISOString(), now
1931
1936
  });
@@ -5,15 +5,13 @@ import { markGlobalRoleMessageNotDelivered } from "../message/message.js";
5
5
  import { providerRetryPending } from "../runtime/providerRetry.js";
6
6
  import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
7
7
  import { globalMessagePresentation } from "../interaction/notificationPresentation.js";
8
- import { recordGlobalRuntimeAttention } from "./globalRuntimeAttention.js";
8
+ import { recordGlobalRuntimeAttention, recordGlobalRuntimeRecovery } from "./globalRuntimeAttention.js";
9
9
  /** Messages are the only queue. Host's atomic begin arbitrates native writers. */
10
10
  export async function deliverGlobalInputs(home, store, ensure, onError, options = {}) {
11
11
  for (const role of store.listGlobalRoles()) {
12
12
  try {
13
- if (store.queryGlobalRoleMessages(role.name, { handling: true, limit: 0 }).count > 0)
14
- continue;
15
13
  let pending = store.queryGlobalRoleMessages(role.name, { pending: true, limit: 64 }).messages;
16
- if (pending.length === 0 || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
14
+ if (hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
17
15
  continue;
18
16
  let sessions = store.getGlobalRoleSessionSet(role.name);
19
17
  // A detached Host keeps an active Session and may reconnect. An explicit
@@ -47,19 +45,31 @@ export async function deliverGlobalInputs(home, store, ensure, onError, options
47
45
  authority: { epoch: bindingBefore.authority.epoch, owner: "controller",
48
46
  holderId: bindingBefore.authority.holderId } }
49
47
  });
48
+ if (observed.reconciliation === "unsupported") {
49
+ throw new Error("Host does not advertise reconcile-turn. Preserve the accepted input and original Host; consume retained Inbox evidence or use an explicitly authorized, quiescence-verified refresh.");
50
+ }
50
51
  if (observed.outcome === "rejected" || ["idle", "failed", "exited"].includes(observed.snapshot.state)
51
52
  && observed.reconciliation !== "terminal-republished") {
52
53
  throw new Error(observed.failure?.detail ?? observed.snapshot.detail ?? "Host cannot reconcile this exact terminal.");
53
54
  }
55
+ if (observed.reconciliation === "terminal-republished") {
56
+ store.transaction(tx => recordGlobalRuntimeRecovery(tx, role.name, `terminal-reconcile:${ownedTurn.attemptId}`, {
57
+ nativeSessionId: currentProviderConversation(bindingBefore).conversationId,
58
+ reason: "host-terminal-unobservable", evidence: "exact-terminal-republished"
59
+ }, options.now ?? new Date()));
60
+ }
54
61
  }
55
62
  catch (error) {
56
63
  store.transaction(tx => recordGlobalRuntimeAttention(tx, role.name, `terminal-reconcile:${currentProviderConversation(bindingBefore).conversationId}:${ownedTurn.attemptId}`, { attemptId: ownedTurn.attemptId, nativeTurnId: ownedTurn.nativeTurnId,
57
64
  nativeSessionId: currentProviderConversation(bindingBefore).conversationId,
58
65
  reason: "host-terminal-unobservable",
59
66
  cause: observed === undefined ? "host-control-unavailable"
60
- : observed.outcome === "rejected" ? "host-control-rejected" : "exact-terminal-not-retained",
67
+ : observed.reconciliation === "unsupported" ? "host-control-unsupported"
68
+ : observed.outcome === "rejected" ? "host-control-rejected" : "exact-terminal-not-retained",
61
69
  hostState: observed?.snapshot.state ?? null,
62
70
  endpointImplementation: observed?.snapshot.endpointImplementation ?? null,
71
+ hostImplementation: observed?.snapshot.hostImplementation ?? null,
72
+ controlCapabilities: observed?.snapshot.controlCapabilities ?? null,
63
73
  eventDelivery: observed?.snapshot.eventDelivery ?? null,
64
74
  // Endpoint generation is not a loaded Host/package version.
65
75
  inputKind: ownedTurn.attemptId.startsWith("direct:") ? "direct" : "managed",
@@ -78,6 +88,8 @@ export async function deliverGlobalInputs(home, store, ensure, onError, options
78
88
  if (pending.length === 0)
79
89
  continue;
80
90
  }
91
+ if (store.queryGlobalRoleMessages(role.name, { handling: true, limit: 0 }).count > 0)
92
+ continue;
81
93
  // Interrupt-then reserves the next legal slot even behind a large queue.
82
94
  const reservedPage = store.queryGlobalRoleMessages(role.name, {
83
95
  pending: true, interruptThen: true, limit: 64
@@ -1,14 +1,115 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { createGlobalRoleMessage } from "../message/message.js";
3
- import { serializeAgentErrorRaw } from "../runtime/agentError.js";
3
+ import { redactAgentErrorText, serializeAgentErrorRaw } from "../runtime/agentError.js";
4
+ import { summaryMetadata, summaryText } from "../output/boundedRead.js";
5
+ const FAILURE_HEADER = "Yui Global failure evidence ";
6
+ const RECOVERY_HEADER = "Yui Global recovery evidence ";
7
+ const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
8
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
9
+ /** This is a projection of original Message documents, never a persisted
10
+ * aggregate or a new handling/recovery state. Exact observations remain in the
11
+ * existing record-only Message history. Polling the same identity is a no-op. */
12
+ function diagnosticIdentity(store, roleName, identity, facts) {
13
+ const value = object(facts), fence = object(value.fence);
14
+ const failure = object(value.failure), retry = object(value.retry);
15
+ const error = object(failure.error ?? value.error ?? retry.error);
16
+ const sessions = store.getGlobalRoleSessionSet(roleName);
17
+ const binding = sessions?.providerBinding;
18
+ const nativeSessionId = value.nativeSessionId ?? fence.nativeSessionId ?? retry.nativeSessionId
19
+ ?? sessions?.sessions[sessions.activeAgentId]?.nativeSessionId;
20
+ const source = {
21
+ roleName, nativeSessionId,
22
+ host: binding?.authority.holderId,
23
+ endpointImplementation: value.endpointImplementation,
24
+ hostImplementation: value.hostImplementation,
25
+ controlCapabilities: value.controlCapabilities,
26
+ driverId: fence.driverId ?? binding?.providerNamespace,
27
+ source: error.source,
28
+ phase: error.phase,
29
+ reason: value.reason, cause: value.cause,
30
+ agentId: fence.agentId ?? sessions?.activeAgentId, providerNamespace: binding?.providerNamespace,
31
+ accountScope: binding?.accountScope
32
+ };
33
+ const classified = typeof nativeSessionId === "string" && (typeof error.code === "string" && error.code !== "unknown"
34
+ || value.reason === "host-terminal-unobservable" && typeof value.cause === "string");
35
+ const classification = {
36
+ ...source, category: error.category, code: error.code, cause: value.cause,
37
+ detail: value.reason === "host-terminal-unobservable" ? hash(value.detail ?? null) : undefined,
38
+ // Explicit impact, not Turn IDs or free-form provider text.
39
+ inputDisposition: error.inputDisposition ?? failure.inputDisposition,
40
+ sessionDisposition: error.sessionDisposition ?? failure.sessionDisposition,
41
+ confirmed: ["submitting", "accepted", "delivery-unknown", "completed", "failed", "cancelled"].includes(value.confirmed)
42
+ ? value.confirmed : undefined,
43
+ status: value.status,
44
+ // Unclassified failures must never be guessed equivalent.
45
+ unclassified: classified ? undefined : identity
46
+ };
47
+ return { key: hash(classification), source, summary: summaryText(redactAgentErrorText(String(error.message ?? value.detail ?? value.reason ?? "Runtime failure; inspect original evidence"))
48
+ .replace(/[\u0000-\u001f\u007f]/gu, " ")) };
49
+ }
50
+ function evidence(message) {
51
+ if (message.kind !== "system" || ![FAILURE_HEADER, RECOVERY_HEADER].some(header => message.body.startsWith(header)))
52
+ return;
53
+ try {
54
+ return object(JSON.parse(message.body.slice(message.body.indexOf("\n") + 1)));
55
+ }
56
+ catch {
57
+ return;
58
+ }
59
+ }
60
+ export function runtimeDiagnosticGroups(messages) {
61
+ const groups = [];
62
+ const active = new Map();
63
+ for (const message of messages) {
64
+ const value = evidence(message);
65
+ if (!value)
66
+ continue;
67
+ if (message.body.startsWith(RECOVERY_HEADER)) {
68
+ for (const [key, group] of active) {
69
+ if (group.source.nativeSessionId !== value.nativeSessionId || group.source.host !== value.host
70
+ || value.reason !== undefined && group.source.reason !== value.reason
71
+ || value.reason === undefined && group.source.phase !== "turn-execute"
72
+ && group.source.phase !== "turn-submit")
73
+ continue;
74
+ group.recoveredAt = message.createdAt;
75
+ group.recoveryEvidence = message.id;
76
+ active.delete(key);
77
+ }
78
+ continue;
79
+ }
80
+ const diagnostic = value.diagnostic;
81
+ if (typeof diagnostic?.key !== "string")
82
+ continue; // Old evidence remains in ordinary history.
83
+ const existing = active.get(diagnostic.key);
84
+ if (existing) {
85
+ existing.count++;
86
+ existing.lastAt = message.createdAt;
87
+ existing.latestEvidence = message.id;
88
+ }
89
+ else {
90
+ const group = { ...diagnostic, episode: message.id, count: 1,
91
+ firstAt: message.createdAt, lastAt: message.createdAt,
92
+ firstEvidence: message.id, latestEvidence: message.id };
93
+ groups.push(group);
94
+ active.set(diagnostic.key, group);
95
+ }
96
+ }
97
+ return groups;
98
+ }
99
+ export function runtimeDiagnosticEvidence(messages, key) {
100
+ return messages.filter(message => evidence(message)?.diagnostic?.key === key);
101
+ }
4
102
  /** Global observations have no Task event history. Keep original failed
5
103
  * attempts as ordinary record-only Messages before replacing the live binding. */
6
104
  export function recordGlobalRuntimeFailure(store, roleName, identity, facts, now) {
7
- const header = `Yui Global failure evidence ${createHash("sha256").update(identity).digest("hex")}\n`;
105
+ const diagnostic = diagnosticIdentity(store, roleName, identity, facts);
106
+ const header = `${FAILURE_HEADER}${hash([identity, diagnostic.key])}\n`;
8
107
  const previous = store.listGlobalRoleMessages(roleName).find(message => message.kind === "system" && message.body.startsWith(header));
9
108
  if (previous !== undefined)
10
109
  return `${roleName}/${previous.id}`;
11
- const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), roleName, header + serializeAgentErrorRaw(facts), "system", { type: "system" }, now);
110
+ const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), roleName, header + redactAgentErrorText(JSON.stringify({
111
+ ...object(facts), diagnostic
112
+ })), "system", { type: "system" }, now);
12
113
  store.saveGlobalRoleMessage(message);
13
114
  return `${roleName}/${message.id}`;
14
115
  }
@@ -18,17 +119,46 @@ export function recordGlobalRuntimeAttention(store, roleName, identity, facts, n
18
119
  // A Home may have a custom Global Role without an Operator. Keep its own
19
120
  // diagnostic readable; never create a Role or launch a substitute Agent.
20
121
  const recipient = store.getGlobalRole("operator") === null ? roleName : "operator";
21
- const requestId = `runtime-attention:${createHash("sha256").update(`${roleName}\0${identity}`).digest("hex")}`;
122
+ const failureRef = recordGlobalRuntimeFailure(store, roleName, identity, facts, now);
123
+ const diagnostic = diagnosticIdentity(store, roleName, identity, facts);
124
+ const group = runtimeDiagnosticGroups(store.listGlobalRoleMessages(roleName))
125
+ .reverse().find(group => group.key === diagnostic.key && group.recoveredAt === undefined);
126
+ const requestId = `runtime-attention:${hash([roleName, diagnostic.key, group?.episode ?? identity])}`;
22
127
  if (store.queryGlobalRoleMessages(recipient, { requestId, limit: 1 }).messages.length > 0)
23
128
  return;
24
129
  const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), recipient, [
130
+ `Yui-Summary: ${diagnostic.summary}`,
25
131
  `Global Role ${roleName} needs attention. No recovery or new authority is implied.`,
26
132
  "Inspect the exact Session and original input before choosing a next action; unknown effects must not be replayed.",
27
133
  `Original failed attempts remain in Role ${roleName}'s record-only failure Messages; failureRef names the latest evidence.`,
28
- serializeAgentErrorRaw(facts)
134
+ redactAgentErrorText(JSON.stringify({ diagnosticKey: diagnostic.key, source: summaryMetadata(diagnostic.source), failureRef,
135
+ read: `yui role message list ${roleName} --diagnostic ${diagnostic.key}` }))
29
136
  ].join("\n"), "system", { type: "system" }, now, { inputControl: { action: "queue", requestId } });
30
137
  store.saveGlobalRoleMessage(recipient === roleName ? {
31
138
  ...message,
32
139
  notDelivered: { reason: "runtime-needs-attention; retained for user or successor inspection", at: now.toISOString() }
33
140
  } : message);
34
141
  }
142
+ /** Called only on an established exact successful observation. A diagnostic
143
+ * recovery says nothing about Message handling, prior effects or Task outcome. */
144
+ export function recordGlobalRuntimeRecovery(store, roleName, identity, facts, now) {
145
+ const messages = store.listGlobalRoleMessages(roleName);
146
+ const host = store.getGlobalRoleSessionSet(roleName)?.providerBinding?.authority.holderId;
147
+ const active = runtimeDiagnosticGroups(messages).filter(group => !group.recoveredAt
148
+ && group.source.nativeSessionId === facts.nativeSessionId && group.source.host === host
149
+ && (facts.reason !== undefined ? group.source.reason === facts.reason
150
+ : ["turn-execute", "turn-submit"].includes(group.source.phase)));
151
+ if (active.length === 0)
152
+ return;
153
+ const header = `${RECOVERY_HEADER}${hash(identity)}\n`;
154
+ if (messages.some(message => message.body.startsWith(header)))
155
+ return;
156
+ const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), roleName, header + serializeAgentErrorRaw({ ...facts, host }), "system", { type: "system" }, now);
157
+ store.saveGlobalRoleMessage(message);
158
+ const recipient = store.getGlobalRole("operator") === null ? roleName : "operator";
159
+ const notice = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), recipient, `Yui-Summary: Global Role ${roleName} has new runtime recovery evidence; no input handling or Task completion is implied.\n`
160
+ + JSON.stringify({ evidence: `${roleName}/${message.id}`, recoveredGroups: active.length }), "system", { type: "system" }, now, { inputControl: { action: "queue", requestId: `runtime-recovery:${hash([roleName, identity])}` } });
161
+ store.saveGlobalRoleMessage(recipient === roleName ? { ...notice,
162
+ notDelivered: { reason: "runtime-recovery; retained for user or successor inspection", at: now.toISOString() }
163
+ } : notice);
164
+ }
@@ -40,8 +40,10 @@ import { createWebTaskSurface } from "../web/webTaskSurface.js";
40
40
  import { createWebConversationSurface } from "../web/webConversation.js";
41
41
  import { WebRequestRejected } from "../web/webMutation.js";
42
42
  import { inspectAgentHost, sendAgentHostNativeControl, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
43
+ import { recoverTaskHostTerminals } from "./taskHostTerminalRecovery.js";
43
44
  import { archiveOrdinaryTask } from "../task/ordinaryArchive.js";
44
45
  import { createWebSettings } from "../web/webSettings.js";
46
+ import { createModelManagement } from "../models/modelManagement.js";
45
47
  import { createWebSkills } from "../web/webSkills.js";
46
48
  import { isTaskOwnedWorkspace, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
47
49
  import { AgentRuntimeObserver } from "./agentRuntimeObserver.js";
@@ -410,6 +412,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
410
412
  const web = createControllerWeb(store, {
411
413
  previews: () => createTaskPreviews(store, home),
412
414
  skills: createWebSkills(store),
415
+ models: createModelManagement(store),
413
416
  settings: createWebSettings(store, {
414
417
  environment: options.environment ?? process.env, catalogs,
415
418
  refreshConfiguration: () => {
@@ -560,6 +563,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
560
563
  }
561
564
  }),
562
565
  jobSupervisor,
566
+ hostTerminalRecovery: () => recoverTaskHostTerminals(home, store, options.onError ?? (() => undefined)),
563
567
  globalInputDelivery: (deliveryOptions) => deliverGlobalInputs(home, store, async (roleName) => {
564
568
  await lifecycleDispatcher("runtime.ensure-role-session", { scope: "global", roleName });
565
569
  }, options.onError ?? (() => undefined), deliveryOptions),