@zq-silk/yui 2.22.0 → 2.25.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 (89) hide show
  1. package/README.md +9 -4
  2. package/dist/artifacts/artifactCapability.js +6 -6
  3. package/dist/artifacts/gitArtifactRef.js +10 -10
  4. package/dist/artifacts/jobArtifact.js +5 -5
  5. package/dist/artifacts/managedGit.js +15 -12
  6. package/dist/artifacts/taskArtifactRepository.js +35 -33
  7. package/dist/cli/commandCatalog.js +2 -2
  8. package/dist/cli/updateCommand.js +20 -3
  9. package/dist/cli/updateOrchestrator.js +13 -0
  10. package/dist/cli/updatePorts.js +27 -0
  11. package/dist/cli.js +9 -2
  12. package/dist/commands/configCommands.js +64 -1
  13. package/dist/commands/projectCommands.js +9 -9
  14. package/dist/commands/taskCompletionGate.js +1 -1
  15. package/dist/commands/taskIntegrationCommands.js +1 -1
  16. package/dist/commands/taskPublicationAdoptCommand.js +1 -1
  17. package/dist/commands/taskRoleRuntimeStatus.js +4 -0
  18. package/dist/commands/taskUpstreamCommands.js +1 -1
  19. package/dist/config/configCatalog.js +3 -0
  20. package/dist/config/gitConfig.js +20 -0
  21. package/dist/config/yuiConfig.js +1 -0
  22. package/dist/context/currentSkills.js +32 -1
  23. package/dist/context/sessionBootstrapManifest.js +17 -4
  24. package/dist/context/sessionSkillNotice.js +33 -12
  25. package/dist/controlPlaneCli.js +4 -4
  26. package/dist/controller/clientRuntime.js +37 -18
  27. package/dist/controller/controller.js +16 -1
  28. package/dist/controller/fileSchedulerStoreAdapter.js +2 -0
  29. package/dist/controller/hostLifecycle.js +153 -0
  30. package/dist/controller/hostLifecycleChecks.js +27 -0
  31. package/dist/controller/hostUpgrade.js +344 -0
  32. package/dist/controller/jobArtifactControl.js +3 -3
  33. package/dist/controller/jobDefinitionControl.js +1 -1
  34. package/dist/controller/runtime.js +67 -16
  35. package/dist/controller/sessionNotify.js +5 -46
  36. package/dist/execution/codexThreadNaming.js +54 -43
  37. package/dist/executor/fileRoleLaunchPlanner.js +18 -3
  38. package/dist/integration/gitIntegrationService.js +1 -1
  39. package/dist/kernel/builtinCapabilities.js +3 -3
  40. package/dist/repository/gitCommand.js +153 -0
  41. package/dist/repository/gitCompatibility.js +3 -2
  42. package/dist/repository/gitWorkspace.js +579 -563
  43. package/dist/repository/gitWorktreeInventory.js +15 -19
  44. package/dist/repository/projectRefresh.js +1 -1
  45. package/dist/repository/taskBaseFreshness.js +1 -4
  46. package/dist/repository/taskWorkspacePreparer.js +1 -1
  47. package/dist/runtime/agentEndpoint.js +11 -1
  48. package/dist/runtime/agentEndpointIdentity.js +2 -0
  49. package/dist/runtime/agentHost.js +167 -42
  50. package/dist/runtime/agentHostImplementation.js +3 -1
  51. package/dist/runtime/codexAppServerRuntime.js +3 -0
  52. package/dist/runtime/codexInteractiveHost.js +62 -17
  53. package/dist/runtime/codexNativeAccess.js +1 -1
  54. package/dist/runtime/contextRecovery.js +15 -0
  55. package/dist/runtime/hostSessionLifecycle.js +83 -0
  56. package/dist/runtime/hostUpgradeGate.js +43 -0
  57. package/dist/runtime/lifecycleChecks.js +47 -0
  58. package/dist/runtime/lifecycleReservation.js +9 -1
  59. package/dist/runtime/nativeExecutable.js +5 -2
  60. package/dist/runtime/nativeTitleReceipt.js +31 -0
  61. package/dist/runtime/nativeTitleSync.js +192 -0
  62. package/dist/runtime/platform.js +11 -0
  63. package/dist/runtime/providerRetry.js +4 -4
  64. package/dist/runtime/structuredProviderHost.js +42 -10
  65. package/dist/storage/sqliteSchema.js +30 -0
  66. package/dist/storage/storageVersions.js +1 -1
  67. package/dist/storage/taskStore.js +7 -1
  68. package/dist/web/webConversation.js +2 -2
  69. package/dist/web/webMaterials.js +19 -19
  70. package/dist/web/webTaskSurface.js +3 -3
  71. package/dist/workspace/workItemChangeSetManager.js +3 -3
  72. package/docs/managed-turn-and-session-runtime.md +96 -1
  73. package/docs/managed-turn-and-session-runtime.zh-CN.md +70 -1
  74. package/docs/project-refresh.md +3 -2
  75. package/docs/project-refresh.zh-CN.md +2 -1
  76. package/docs/release-workflow.md +86 -2
  77. package/docs/release-workflow.zh-CN.md +65 -1
  78. package/docs/roles-and-configuration.md +34 -0
  79. package/docs/roles-and-configuration.zh-CN.md +27 -0
  80. package/docs/storage-baseline.md +14 -1
  81. package/docs/storage-baseline.zh-CN.md +7 -1
  82. package/docs/testing/verification-levels.md +7 -7
  83. package/docs/testing/verification-levels.zh-CN.md +6 -6
  84. package/i18n/README.zh-CN.md +8 -4
  85. package/package.json +1 -1
  86. package/skills/yui-runtime/SKILL.md +7 -2
  87. package/skills/yui-runtime/VERSION +1 -1
  88. package/dist/runtime/native/darwin-x64/claude-process-owner +0 -0
  89. package/dist/runtime/native/darwin-x64/process-identity +0 -0
@@ -164,16 +164,16 @@ export function completeProviderRetry(binding, turn) {
164
164
  export function providerRetryAttemptId(retry) {
165
165
  return `provider-retry:${retry.chainId}/${retry.transportSequence + 1}`;
166
166
  }
167
- export function providerRetryPrompt(retry, original) {
167
+ /** Recovery guidance only. User input is delivered separately, once. */
168
+ export function providerRetryContext(retry) {
168
169
  if (retry.failedNativeTurnId === undefined)
169
- return original;
170
+ return undefined;
170
171
  return [
171
172
  "Yui infrastructure recovery, not new user authorization.",
172
173
  `Continue the existing input after failed attempt ${retry.failureAttemptId}`,
173
174
  `(native Turn ${retry.failedNativeTurnId}; failure ${retry.failureRef}).`,
174
175
  "Read current authorized context and the original input. Inspect existing work and receipts first;",
175
- "continue only unfinished work. Preserve local changes and do not repeat confirmed side effects.",
176
- original
176
+ "continue only unfinished work. Preserve local changes and do not repeat confirmed side effects."
177
177
  ].join("\n");
178
178
  }
179
179
  /** The same presentation is consumed by explicit detail reads and Web. */
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { createNativeTitleSync } from "./nativeTitleSync.js";
3
4
  import { Duplex } from "node:stream";
4
5
  import { nativeExecutable } from "./nativeExecutable.js";
5
6
  import { startMissingCodexSharedDaemon } from "./codexSharedDaemon.js";
@@ -15,6 +16,7 @@ import { JsonLineChannel, PROVIDER_MESSAGE_MAX_BYTES, terminateProcessGroup } fr
15
16
  import { PROVIDER_ACCEPT_TIMEOUT_MS } from "./runtimeDeadlines.js";
16
17
  import { foldPublicReply } from "./publicReply.js";
17
18
  import { createCodexNativeAccess } from "./codexNativeAccess.js";
19
+ import { isNativeContextRecovery } from "./contextRecovery.js";
18
20
  const CODEX_PROXY_HANDSHAKE_TIMEOUT_MS = 10_000;
19
21
  /**
20
22
  * These Session implementations do not expose run-configuration observations.
@@ -396,7 +398,22 @@ class CodexStructuredProviderSession {
396
398
  onActivity;
397
399
  mirror;
398
400
  nativeAccountHome;
401
+ contextEpoch = 0;
402
+ async checkQuiescent() {
403
+ const snapshot = await this.runtime.readConversation(this.conversationId, { includeTurns: false });
404
+ if (snapshot.threadId !== this.conversationId || snapshot.status !== "idle"
405
+ || snapshot.activeTurnId !== undefined)
406
+ return false;
407
+ const backgrounds = await this.nativeAccess.read("thread/backgroundTerminals/list", {
408
+ threadId: this.conversationId, limit: 1
409
+ });
410
+ if (!Array.isArray(backgrounds.data) || !("nextCursor" in backgrounds)) {
411
+ throw new Error("Native background execution response is invalid.");
412
+ }
413
+ return backgrounds.data.length === 0 && backgrounds.nextCursor === null;
414
+ }
399
415
  appliedGeneratedTitle;
416
+ #titleSync;
400
417
  nativeAccess;
401
418
  #turnAttempts = new Map();
402
419
  adapterId = "codex";
@@ -456,16 +473,6 @@ class CodexStructuredProviderSession {
456
473
  throw error;
457
474
  }
458
475
  }
459
- if (control.sessionTitle !== undefined) {
460
- // The native title mutation can persist without replying and stall later
461
- // Goal/Turn requests, even through a separate metadata connection. Keep
462
- // Yui's launch title, but do not put that optional mutation on a managed
463
- // Session's execution path. Reconnect must not replay it either.
464
- // This is an intentional omission, not a failed delivery or unavailable
465
- // observer. Keep it in the Host log without waking an Agent to recover
466
- // an operation that was never attempted.
467
- mirror("stderr", "Native title synchronization was not submitted; Yui retains its Session title without blocking native Goal/Turn control.\n");
468
- }
469
476
  const ownedTurn = control.kind === "restore" ? control.ownedTurn : undefined;
470
477
  // Keep notifications buffered during paginated recovery. Otherwise a live
471
478
  // terminal can be emitted before open returns and then returned again as a
@@ -521,6 +528,15 @@ class CodexStructuredProviderSession {
521
528
  }
522
529
  }
523
530
  const goal = await runtime.readGoal(conversationId);
531
+ session.#titleSync = createNativeTitleSync(payload, conversationId, session.nativeAccountHome, result => {
532
+ if (result.status === "confirmed")
533
+ session.appliedGeneratedTitle = result.title;
534
+ mirror("stderr", `Native title: ${JSON.stringify(result)}\n`);
535
+ });
536
+ void exit.then(() => session.#titleSync?.close());
537
+ // Fresh Threads may not be persisted until their first accepted input.
538
+ if (control.mode === "resume")
539
+ session.#titleSync.notify();
524
540
  return Object.freeze({
525
541
  session,
526
542
  ...(recoveredTerminal === undefined ? {} : { recoveredTerminal }),
@@ -530,6 +546,8 @@ class CodexStructuredProviderSession {
530
546
  #observeMessage(message, emit) {
531
547
  const method = typeof message.method === "string" ? message.method : "";
532
548
  const params = object(message.params) ?? {};
549
+ if (isNativeContextRecovery("codex", this.conversationId, message))
550
+ this.contextEpoch += 1;
533
551
  const threadId = optionalId(params.threadId);
534
552
  if (emit)
535
553
  this.publicReply = foldPublicReply(this.publicReply, this.conversationId, method, params);
@@ -539,6 +557,13 @@ class CodexStructuredProviderSession {
539
557
  }
540
558
  return undefined;
541
559
  }
560
+ if (method === "thread/name/updated") {
561
+ this.#titleSync?.close();
562
+ if (params.threadName !== this.appliedGeneratedTitle)
563
+ this.appliedGeneratedTitle = undefined;
564
+ }
565
+ if (method === "turn/started")
566
+ this.#titleSync?.notify();
542
567
  let goal;
543
568
  try {
544
569
  goal = codexGoalNotification({ method, params });
@@ -663,6 +688,7 @@ class CodexStructuredProviderSession {
663
688
  conversationId: this.conversationId,
664
689
  attemptId: turn.attemptId,
665
690
  text: turn.boundedText,
691
+ ...(turn.additionalContext === undefined ? {} : { additionalContext: turn.additionalContext }),
666
692
  expectedNoActiveTurn: true,
667
693
  ...(turn.requireQuiescent ? { requireQuiescent: true } : {}),
668
694
  ...(turn.expectedFailedNativeTurnId === undefined ? {} : {
@@ -682,6 +708,7 @@ class CodexStructuredProviderSession {
682
708
  this.#clientOwnedTurnId = acceptance.turnId;
683
709
  this.#clientOwnedAttemptId = turn.attemptId;
684
710
  this.#turnAttempts.set(acceptance.turnId, turn.attemptId);
711
+ this.#titleSync?.notify();
685
712
  return Object.freeze({
686
713
  attemptId: turn.attemptId,
687
714
  conversationId: this.conversationId,
@@ -712,6 +739,7 @@ class CodexStructuredProviderSession {
712
739
  conversationId: this.conversationId,
713
740
  expectedTurnId: nativeTurnId,
714
741
  text: turn.boundedText,
742
+ ...(turn.additionalContext === undefined ? {} : { additionalContext: turn.additionalContext }),
715
743
  clientUserMessageId: turn.attemptId
716
744
  });
717
745
  if (acceptance.status === "unknown") {
@@ -776,6 +804,7 @@ class CodexStructuredProviderSession {
776
804
  return result === "interrupted" ? "requested" : result;
777
805
  }
778
806
  terminate(signal) {
807
+ this.#titleSync?.close();
779
808
  terminateProcessGroup(this.child, signal);
780
809
  }
781
810
  }
@@ -787,6 +816,7 @@ class ClaudeStructuredProviderSession {
787
816
  channel;
788
817
  onGoal;
789
818
  mirror;
819
+ contextEpoch = 0;
790
820
  adapterId = "claude";
791
821
  get ownedProcessId() { return this.child.pid; }
792
822
  #activeAttemptId;
@@ -903,6 +933,8 @@ class ClaudeStructuredProviderSession {
903
933
  terminateProcessGroup(this.child, signal);
904
934
  }
905
935
  #receive(message, onTerminal, onAccepted, onActivity) {
936
+ if (isNativeContextRecovery("claude", this.conversationId, message))
937
+ this.contextEpoch += 1;
906
938
  const goal = claudeActiveGoal(message, this.conversationId);
907
939
  if (goal !== undefined) {
908
940
  const key = JSON.stringify(goal === null ? null : {
@@ -295,6 +295,36 @@ const MINOR_UPGRADES = Object.freeze([{
295
295
  targetChecksum: CURRENT_SCHEMA_CHECKSUM,
296
296
  // Optional workspace-write permission fields; preserve all existing payloads.
297
297
  sql: ""
298
+ }, {
299
+ fromVersion: "1.26",
300
+ toVersion: "1.27",
301
+ name: "host-upgrade-lifecycle-signals",
302
+ introducedIn: "next",
303
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
304
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
305
+ // New versioned signal sources in the existing role-runtime mailbox. No
306
+ // historical requests are inferred and existing records remain unchanged.
307
+ sql: ""
308
+ }, {
309
+ fromVersion: "1.27",
310
+ toVersion: "1.28",
311
+ name: "native-title-effect-receipts",
312
+ introducedIn: "next",
313
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
314
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
315
+ // Additive exact-identity intent/confirmation files under runtime/native-titles.
316
+ // Never invent native ownership or replay history during migration.
317
+ sql: ""
318
+ }, {
319
+ fromVersion: "1.28",
320
+ toVersion: "1.29",
321
+ name: "git-command-budgets",
322
+ introducedIn: "2.24.0",
323
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
324
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
325
+ // Optional local/remote Git budgets in config. Absence uses runtime defaults;
326
+ // preserve every existing payload byte rather than persisting defaults.
327
+ sql: ""
298
328
  }]);
299
329
  export function storageMinorUpgradePlan(from) {
300
330
  if (from === CURRENT_STORAGE_VERSION)
@@ -1,4 +1,4 @@
1
- export const CURRENT_STORAGE_VERSION = "1.26";
1
+ export const CURRENT_STORAGE_VERSION = "1.29";
2
2
  export const MIN_SUPPORTED_STORAGE_VERSION = "1.0";
3
3
  export const STORAGE_FORMAT = "yui-home";
4
4
  export function isStorageVersion(value) {
@@ -4,7 +4,7 @@ import { validateModelConnection } from "../models/modelManagementConfig.js";
4
4
  import { basename, dirname, join, resolve } from "node:path";
5
5
  import { isDeepStrictEqual } from "node:util";
6
6
  import { resolveTimeZone } from "../config/timeZone.js";
7
- import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours, resolveRuntimeHealth, resolveTelemetryEnabled, resolveTelemetryRunCap, resolveTelemetryTerminalKeep, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
7
+ import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveGitLocalTimeoutSeconds, resolveGitRemoteInactivityTimeoutSeconds, resolveGitRemoteTimeoutSeconds, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours, resolveRuntimeHealth, resolveTelemetryEnabled, resolveTelemetryRunCap, resolveTelemetryTerminalKeep, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
8
8
  import { validateCapabilityGrant } from "../grant/capabilityGrant.js";
9
9
  import { validateReleaseWorkflow } from "../release/releaseWorkflow.js";
10
10
  import { validateReviewConfig } from "../review/reviewConfig.js";
@@ -131,6 +131,9 @@ export function validateYuiConfig(config) {
131
131
  "controllerTaskConcurrency",
132
132
  "agentLaunchInactivityTimeoutSeconds",
133
133
  "deliveryTimeoutSeconds",
134
+ "gitLocalTimeoutSeconds",
135
+ "gitRemoteInactivityTimeoutSeconds",
136
+ "gitRemoteTimeoutSeconds",
134
137
  "resourcesQuarantineTtlHours",
135
138
  "tmuxBin",
136
139
  "tmuxHistoryLimit",
@@ -159,6 +162,9 @@ export function validateYuiConfig(config) {
159
162
  resolveControllerTaskConcurrency(config.controllerTaskConcurrency);
160
163
  resolveAgentLaunchInactivityTimeoutSeconds(config.agentLaunchInactivityTimeoutSeconds);
161
164
  resolveDeliveryTimeoutSeconds(config.deliveryTimeoutSeconds);
165
+ resolveGitLocalTimeoutSeconds(config.gitLocalTimeoutSeconds);
166
+ resolveGitRemoteInactivityTimeoutSeconds(config.gitRemoteInactivityTimeoutSeconds);
167
+ resolveGitRemoteTimeoutSeconds(config.gitRemoteTimeoutSeconds);
162
168
  resolveTmuxBin(config.tmuxBin);
163
169
  resolveTmuxHistoryLimit(config.tmuxHistoryLimit);
164
170
  resolveTelemetryEnabled(config.telemetryEnabled);
@@ -227,7 +227,7 @@ export function createWebConversationSurface(store, surface, plan, readPage = re
227
227
  if (["completed", "cancelled", "retired", "archived"].includes(task.status)) {
228
228
  throw new WebRequestRejected("Terminal Task materials are read-only.");
229
229
  }
230
- return saveTextMaterial(store.rootDirectory(), owner.taskId, id, input);
230
+ return saveTextMaterial(store.rootDirectory(), owner.taskId, id, input, () => store.getConfig());
231
231
  },
232
232
  state(owner, offset = 0) {
233
233
  const set = sessions(owner);
@@ -296,7 +296,7 @@ export function createWebConversationSurface(store, surface, plan, readPage = re
296
296
  if (owner.scope !== "task" || input.action === "interrupt")
297
297
  throw new WebRequestRejected("Materials require Task text input.");
298
298
  try {
299
- body = await materialInputBody(store.rootDirectory(), owner.taskId, required(body), input.materials);
299
+ body = await materialInputBody(store.rootDirectory(), owner.taskId, required(body), input.materials, () => store.getConfig());
300
300
  }
301
301
  catch (error) {
302
302
  throw new WebRequestRejected(error instanceof Error ? error.message : "Invalid material.");
@@ -16,24 +16,24 @@ const TEXT_EXTENSIONS = new Set([
16
16
  const TEXT_NAMES = new Set(["readme", "license", "dockerfile", "makefile"]);
17
17
  export const MAX_MATERIAL_BYTES = 256 * 1024;
18
18
  const PAGE_CHARACTERS = 12000;
19
- async function fixedImage(home, taskId, input) {
19
+ async function fixedImage(home, taskId, input, getConfig) {
20
20
  if (input.taskId !== taskId)
21
21
  throw new WebRequestRejected("Materials must belong to the same Task.");
22
22
  const ref = validateGitArtifactRef(input);
23
23
  if (ref.relativePath.length > 512)
24
24
  throw new WebRequestRejected("Image path is too long.");
25
- const file = await resolveGitArtifact(home, ref);
25
+ const file = await resolveGitArtifact(home, ref, getConfig);
26
26
  const metadata = rasterMetadata(file.bytes);
27
27
  return { ...ref, digest: file.digest, kind: "image", byteSize: file.bytes.length,
28
28
  ...metadata, base64: file.bytes.toString("base64") };
29
29
  }
30
30
  /** One selected immutable file, never a workspace path or a remote URL.
31
31
  * Markdown is bounded and whole so fences/tables are not split by pagination. */
32
- export async function readArtifactPreview(home, taskId, ref, offset = 0) {
32
+ export async function readArtifactPreview(home, taskId, ref, offset = 0, getConfig) {
33
33
  if (ref.taskId !== taskId)
34
34
  throw new WebRequestRejected("Materials must belong to the same Task.");
35
35
  const valid = validateGitArtifactRef(ref);
36
- const file = await resolveGitArtifact(home, valid);
36
+ const file = await resolveGitArtifact(home, valid, getConfig);
37
37
  return { ...valid, ...previewArtifactContent(file, offset) };
38
38
  }
39
39
  /** Shared data-only preview for Task and Job evidence; no path lookup or authority. */
@@ -81,12 +81,12 @@ function decodeText(bytes) {
81
81
  }
82
82
  return text;
83
83
  }
84
- async function fixedText(home, taskId, input) {
84
+ async function fixedText(home, taskId, input, getConfig) {
85
85
  if (input.taskId !== taskId)
86
86
  throw new WebRequestRejected("Materials must belong to the same Task.");
87
87
  const ref = validateGitArtifactRef(input);
88
88
  textPath(ref.relativePath);
89
- const file = await resolveGitArtifact(home, ref);
89
+ const file = await resolveGitArtifact(home, ref, getConfig);
90
90
  return { ref: { ...ref, digest: file.digest }, text: decodeText(file.bytes), bytes: file.bytes.length };
91
91
  }
92
92
  function page(text, offset = 0) {
@@ -100,26 +100,26 @@ function page(text, offset = 0) {
100
100
  return { content: text.slice(offset, end), offset, totalCharacters: text.length,
101
101
  nextOffset: end < text.length ? end : null };
102
102
  }
103
- export async function readTextArtifactPage(home, taskId, ref, offset = 0) {
104
- const file = await fixedText(home, taskId, ref);
103
+ export async function readTextArtifactPage(home, taskId, ref, offset = 0, getConfig) {
104
+ const file = await fixedText(home, taskId, ref, getConfig);
105
105
  return { ...file.ref, byteSize: file.bytes, ...page(file.text, offset) };
106
106
  }
107
- export async function compareTextArtifacts(home, taskId, before, after, offset = 0) {
108
- const left = await fixedText(home, taskId, before);
109
- const right = await fixedText(home, taskId, after);
107
+ export async function compareTextArtifacts(home, taskId, before, after, offset = 0, getConfig) {
108
+ const left = await fixedText(home, taskId, before, getConfig);
109
+ const right = await fixedText(home, taskId, after, getConfig);
110
110
  // Blob identities avoid interpreting user paths as options or reading the
111
111
  // working tree. Neither external diff drivers nor text converters can run.
112
- const repo = openTaskArtifactRepository(home, taskId);
112
+ const repo = openTaskArtifactRepository(home, taskId, getConfig);
113
113
  const diff = await managedGit(repo.repoPath, [
114
114
  "diff", "--no-ext-diff", "--no-textconv", "--no-color", "--unified=3",
115
115
  `${left.ref.commit}:${left.ref.relativePath}`, `${right.ref.commit}:${right.ref.relativePath}`, "--"
116
- ], { maxBuffer: 20 * 1024 * 1024, timeoutMs: 5000 });
116
+ ], { maxBuffer: 20 * 1024 * 1024, getConfig });
117
117
  return { taskId, before: left.ref, after: right.ref, ...page(diff, offset) };
118
118
  }
119
119
  /** Saves DATA only. Sending it is a separate explicit conversation action.
120
120
  * The normal artifact repository and commit message retain origin; no second
121
121
  * storage/receipt schema is introduced. Unknown saves are inspected, not replayed. */
122
- export async function saveTextMaterial(home, taskId, sessionId, input) {
122
+ export async function saveTextMaterial(home, taskId, sessionId, input, getConfig) {
123
123
  if (!/^[A-Za-z0-9_-]{1,128}$/u.test(input.requestId)
124
124
  || typeof input.name !== "string" || input.name.includes("/") || input.name.includes("\\")
125
125
  || typeof input.content !== "string")
@@ -130,7 +130,7 @@ export async function saveTextMaterial(home, taskId, sessionId, input) {
130
130
  throw new WebRequestRejected(`Material exceeds ${MAX_MATERIAL_BYTES} UTF-8 bytes or has invalid text.`);
131
131
  }
132
132
  decodeText(bytes);
133
- const repo = openTaskArtifactRepository(home, taskId);
133
+ const repo = openTaskArtifactRepository(home, taskId, getConfig);
134
134
  await repo.ensure();
135
135
  const head = (await repo.head());
136
136
  if ((await repo.list(head)).some(entry => entry.relativePath === relativePath)) {
@@ -141,22 +141,22 @@ export async function saveTextMaterial(home, taskId, sessionId, input) {
141
141
  return { taskId, relativePath, commit: head, digest: existing.digest };
142
142
  }
143
143
  return saveArtifactFile(home, taskId, { relativePath, bytes, expectedHead: head,
144
- message: `Text material from Leader Session ${sessionId}; request ${input.requestId}` });
144
+ message: `Text material from Leader Session ${sessionId}; request ${input.requestId}` }, getConfig);
145
145
  }
146
146
  /** Existing durable Message body carries self-contained frozen references.
147
147
  * Contents are deliberately absent: reading material cannot elevate it to an
148
148
  * instruction, execute it, or broaden the receiving Agent's Task authority. */
149
- export async function materialInputBody(home, taskId, body, refs) {
149
+ export async function materialInputBody(home, taskId, body, refs, getConfig) {
150
150
  if (!Array.isArray(refs) || refs.length > 8)
151
151
  throw new WebRequestRejected("Select at most eight material references.");
152
152
  const verified = [];
153
153
  for (const ref of refs) {
154
154
  if (isRasterArtifact(ref.relativePath)) {
155
- const image = await fixedImage(home, taskId, ref);
155
+ const image = await fixedImage(home, taskId, ref, getConfig);
156
156
  verified.push({ taskId, commit: image.commit, relativePath: image.relativePath, digest: image.digest });
157
157
  }
158
158
  else
159
- verified.push((await fixedText(home, taskId, ref)).ref);
159
+ verified.push((await fixedText(home, taskId, ref, getConfig)).ref);
160
160
  }
161
161
  if (!verified.length)
162
162
  return body;
@@ -136,7 +136,7 @@ export function createWebTaskSurface(store, options = {}, observations = [], hos
136
136
  artifacts: async (taskId, selectedCommit, offset = 0) => {
137
137
  if (store.getTask(taskId) === null)
138
138
  throw new WebRequestRejected("Task not found.");
139
- const repo = openTaskArtifactRepository(store.rootDirectory(), taskId);
139
+ const repo = openTaskArtifactRepository(store.rootDirectory(), taskId, () => store.getConfig());
140
140
  if (selectedCommit !== undefined && !repo.exists())
141
141
  throw new WebRequestRejected("Artifact revision unavailable.");
142
142
  const commit = selectedCommit ?? await repo.head();
@@ -151,12 +151,12 @@ export function createWebTaskSurface(store, options = {}, observations = [], hos
151
151
  throw new WebRequestRejected("Task not found.");
152
152
  // The browser always names a fixed revision. Never substitute HEAD after
153
153
  // a missing object, and never execute/render artifact HTML as an app.
154
- return readArtifactPreview(store.rootDirectory(), taskId, { taskId, relativePath, commit }, offset);
154
+ return readArtifactPreview(store.rootDirectory(), taskId, { taskId, relativePath, commit }, offset, () => store.getConfig());
155
155
  },
156
156
  artifactDiff: async (taskId, relativePath, commit, beforeCommit, offset = 0) => {
157
157
  if (store.getTask(taskId) === null)
158
158
  throw new WebRequestRejected("Task not found.");
159
- return compareTextArtifacts(store.rootDirectory(), taskId, { taskId, relativePath, commit: beforeCommit }, { taskId, relativePath, commit }, offset);
159
+ return compareTextArtifacts(store.rootDirectory(), taskId, { taskId, relativePath, commit: beforeCommit }, { taskId, relativePath, commit }, offset, () => store.getConfig());
160
160
  },
161
161
  globalState: (roleName) => {
162
162
  const role = store.getGlobalRole(roleName);
@@ -26,7 +26,7 @@ export class WorkItemChangeSetManager {
26
26
  const context = requireCapturableContext(this.store, taskId, workItemId, options.taskFinalReviewContract);
27
27
  const entries = capturableEntries(context);
28
28
  if (context.source === "task-main") {
29
- const git = new NodeGitWorkspace();
29
+ const git = new NodeGitWorkspace(() => this.store.getConfig());
30
30
  for (const { entry, expectedHead } of entries) {
31
31
  if (!await git.isClean(entry.path)) {
32
32
  throw new Error(`Exact direct Candidate Task main must be clean: ${context.workItemId}/${entry.projectId}.`);
@@ -64,7 +64,7 @@ export class WorkItemChangeSetManager {
64
64
  const workspace = this.store.getWorkItemWorkspace(item.taskId, item.id);
65
65
  if (workspace === null)
66
66
  return { proof: null, checks: [] };
67
- const git = new NodeGitWorkspace();
67
+ const git = new NodeGitWorkspace(() => this.store.getConfig());
68
68
  const projects = [];
69
69
  const checks = [];
70
70
  const sources = [`work-item:${taskId}/${workItemId}`,
@@ -189,7 +189,7 @@ export class WorkItemChangeSetManager {
189
189
  if (unresolved !== undefined) {
190
190
  throw new Error(`Task has an unresolved Integration Attempt: ${task.id}/${unresolved.id}.`);
191
191
  }
192
- const git = new NodeGitWorkspace();
192
+ const git = new NodeGitWorkspace(() => this.store.getConfig());
193
193
  const remoteHeads = new Map();
194
194
  const findPublishedCommit = async (entry, headCommit) => {
195
195
  const project = this.store.getProject(entry.projectId);
@@ -2,6 +2,99 @@
2
2
 
3
3
  # Session, AgentRun and notifications
4
4
 
5
+ ## Two lifecycle layers and nonblocking checks
6
+
7
+ Controller custody of a Host and the Host's native Session lifecycle have
8
+ separate event/guard/transition tables. Neither is an upgrade workflow.
9
+
10
+ | Layer | Events and transitions | Authority and correlation |
11
+ | --- | --- | --- |
12
+ | Controller → Host | Fresh physical observations recover `unavailable`/`stopped`/`starting`/`running`. Authorized launch enters `starting`; matching launch handshake enters `running`. Stop or graceful-exit request enters `stopping`; sending exit does not prove `stopped`. Physical absence confirms `stopped`; unknown effects enter `unavailable`. | Owner, local operation/revision, returned binding and native Session ID. Durable Role/Task intent, launch reservations and exact process custody remain authoritative. |
13
+ | Host → native Session | Launch, attachment, input acceptance, native activity, terminal, persistence, disconnect and exit drive the native states described below. | Native Session, proxy process, attempt and Turn identity; existing writer/resource fences still authorize effects. |
14
+
15
+ The Controller wraps the shared Host port for launch/restore, observation and
16
+ stop, including graceful-upgrade exit. A competing physical operation is
17
+ rejected before its side effect, not queued or replayed by this FSM. Late
18
+ reads and old bindings cannot overwrite newer custody; identical observations
19
+ produce no hook feedback loop. A Controller restart reconstructs by observation,
20
+ never by replaying the process-local state. Native completion/attachment exit
21
+ does not prove Host process exit. Legal same-conversation resume correlates the
22
+ layers without sharing their state enums or inventing another durable authority.
23
+
24
+ Host-side native Session lifecycle state is owned by an explicit transition table, not
25
+ by version detection. Launch/submit enters `starting`; attachment facts select
26
+ `idle`, `busy`, `ready` (owned input), or `delivery-unknown`; acceptance enters
27
+ `ready`; native execution enters `busy`; terminal handling enters `settling`;
28
+ persisted settlement returns to observed `idle`/`busy`. Disconnect enters
29
+ `starting`, classified failures retain their failure outcome, and exit enters
30
+ `exited`. Only an explicit launch can leave `exited`. Native-TUI attachment
31
+ readiness has its own event: `ready` there is not proof of native quiescence.
32
+ Illegal edges do not mutate state; stale process, attempt and terminal-Turn
33
+ facts are ignored. Exact writer/resource guards remain at command admission.
34
+ This process-local reducer reconstructs from existing launch and native facts;
35
+ it adds no persisted Session authority or upgrade workflow state machine.
36
+
37
+ Optional before/after observations are immutable, deferred and independently
38
+ coalesced (at most one running and one latest pending observation per check).
39
+ “Before” describes the transition's source, not an awaited precondition.
40
+ A slow or failing check cannot veto a transition or hold event folding/input
41
+ delivery; checks must yield for I/O and honour cancellation before effects.
42
+ They may recommend work to the separate fenced upgrade coordinator.
43
+ Controller Host-state hooks suggest version reconciliation. Host Session-state
44
+ hooks asynchronously observe current Skill metadata on attachment/settlement,
45
+ without reading Skill bodies, committing a notice or assuming model memory.
46
+ The exact per-input integrity check remains an ingress safeguard: invalid
47
+ Skills can reject that input, but an optional check cannot stall lifecycle.
48
+ Only accepted input advances the existing notice baseline; unchanged input
49
+ is passed through without any extra content.
50
+
51
+ Controller startup, Host connection/exit observations, Host state-change
52
+ signals and event persistence trigger version checks without an explicit
53
+ `update` request. Existing periodic reconciliation covers missed hints,
54
+ including providers without reliable background-execution-end events and
55
+ graceful exits that cannot emit a post-exit hint. Checks
56
+ compare actual loaded Host/Endpoint implementations with this Controller's
57
+ loaded target, never npm `latest`. Incomplete readiness on matching versions
58
+ is not an upgrade reason. Checks stop on shutdown; already-admitted work
59
+ retains the existing durable mailbox. A request pinned to a different target
60
+ stays waiting for explicit recovery, not silent retargeting. An explicitly
61
+ stopped Session is never restarted by a check.
62
+
63
+ ## Host upgrade and context checks
64
+
65
+ `update` reports package installation, verified Controller startup and each
66
+ Host separately. `update --hosts-status` reads current per-Host evidence without
67
+ installing, starting or advancing a switch. Supported controlled Codex Hosts fence new input immediately,
68
+ wait for the current native Turn and event persistence, inspect native background
69
+ execution, and release their owned Endpoint before exiting gracefully. The
70
+ existing role-runtime mailbox retains the exact generation, native Session,
71
+ configuration digest and drain receipt across Controller restarts. A successor
72
+ uses the recorded effective configuration and resumes that same conversation;
73
+ only its loaded Host/Endpoint and control/event handshake confirm success.
74
+ Queued Messages remain original and accepted/unknown inputs are never replayed.
75
+
76
+ Paused Roles stay paused. Older Hosts, native-TUI-only attachments and Endpoints
77
+ without a quiescence capability are retained with an unsupported diagnosis; no
78
+ unsupported drain command is sent. A busy/unknown native execution has no
79
+ bounded upgrade-completion promise. An uncertain launch is not automatically
80
+ repeated; a later exact successful handshake can settle the retained request.
81
+ This fence covers Host-controlled ingress, not arbitrary external native clients;
82
+ it never terminates a shared daemon or native conversation.
83
+
84
+ Host Skill checks still run before every input. Unchanged inputs are passed
85
+ through exactly, with no notice, marker, separator or extra native input part.
86
+ The attachment's loading guidance explains this managed-input contract; no
87
+ notice is not a reason to repeat the check. Unmediated clients or uncertain
88
+ current input routing retain the explicit Manifest check fallback; reusing a
89
+ conversation alone does not establish Host mediation. Cold attachment,
90
+ affected Skill changes and observed native compaction supply loading guidance;
91
+ compaction invalidates the next input's metadata baseline. Without a reliable
92
+ native event, the Agent must still recover any missing instructions using the
93
+ Manifest. Neither a version nor an accepted notice proves model memory.
94
+ Routine checks and empty inboxes are not user-facing progress.
95
+
96
+ ## Session stop and runtime observations
97
+
5
98
  Global Session stop/replacement uses the same exact native-execution quiescence
6
99
  boundary as Task Sessions. A departed Host or archived Session alone is not
7
100
  proof that an accepted input ended: its retained Provider binding is inspected
@@ -31,7 +124,9 @@ Samples use a process-monotonic clock and hashed target/request labels, with
31
124
  at most 16 recent and 16 active samples plus the slowest completed sample.
32
125
  In-flight request count is not queue depth; total client-to-route wait is not
33
126
  measured by the server alone. These observations neither identify an old incident
34
- retroactively nor establish quiescence; restarting a Controller does not refresh live Hosts.
127
+ retroactively nor establish quiescence. A Controller restart does not itself
128
+ prove Host replacement; asynchronous lifecycle checks still require a safe
129
+ drain and the successor's actual handshake.
35
130
 
36
131
  `observations.commands.routes.transport` correlates socket accept-to-route,
37
132
  complete-frame receive-to-route and route-to-local-flush stages by the same
@@ -2,6 +2,74 @@
2
2
 
3
3
  # Session、AgentRun 与通知
4
4
 
5
+ ## 两层生命周期与非阻塞检查
6
+
7
+ Controller 对 Host 的管理状态、Host 对原生 Session 的生命周期分别拥有自己的
8
+ 事件、guard 和转换表,都不是升级流程状态机。
9
+
10
+ | 层级 | 事件与转换 | 权限及关联 |
11
+ | --- | --- | --- |
12
+ | Controller → Host | 新鲜物理观测恢复 `unavailable`/`stopped`/`starting`/`running`;合法 launch 进入 `starting`,匹配启动握手进入 `running`。停止或优雅退出请求进入 `stopping`,发送退出不证明 `stopped`;物理不存在才确认 `stopped`,未知效果进入 `unavailable`。 | owner、本地操作/观测 revision、返回 binding 和原生 Session ID;持久 Role/Task 意图、启动 reservation 和精确进程权属仍是权威。 |
13
+ | Host → 原生 Session | launch、附件、输入接受、原生活动、终态、持久化、断连和退出驱动下述原生状态。 | 原生 Session、proxy 进程、attempt 和 Turn 身份;原有 writer/资源栅栏继续授权副作用。 |
14
+
15
+ Controller 在共享 Host port 处统一管理启动/恢复、观测与停止(含安全升级的优雅退出)。
16
+ 冲突物理操作在副作用前拒绝,不由状态机另建队列或重放。迟到读取和旧 binding 不能覆盖
17
+ 新管理状态;相同观测不重复触发检查反馈循环。Controller 重启后从观测重建,
18
+ 不重放进程内状态。原生轮次完成或附件退出不能证明 Host 进程已退出;
19
+ 合法恢复通过同一原生对话关联两层,不共用状态枚举或新增持久权威。
20
+
21
+ Host 内的原生 Session 生命周期由明确的转换表管理,不由版本检查管理。
22
+ launch/submit 进入 `starting`;附件事实选择 `idle`、`busy`、
23
+ `ready`(持有输入)或 `delivery-unknown`;接受输入进入 `ready`;
24
+ 原生执行进入 `busy`;终态处理进入 `settling`;持久化结清后回到观测到的
25
+ `idle`/`busy`。断连进入 `starting`,分类失败保留对应失败状态,退出进入
26
+ `exited`,只有显式 launch 可离开它。原生 TUI 附件就绪有独立事件,
27
+ 其 `ready` 不证明原生执行静止。非法边不改变状态;过期的进程、attempt、
28
+ 终态 Turn 事实被忽略。精确 writer/资源 guard 仍在命令准入处执行。
29
+ 状态机仅在进程内从现有 launch 和原生事实恢复,不新增持久 Session 权威或升级流程状态机。
30
+
31
+ 可组合的前/后检查接收不可变观测,各自异步执行并合并重复提示
32
+ (每项最多一个执行中和一个最新待处理观测)。“前”表示转换来源,不是等待完成的前置条件。
33
+ 慢检查或失败不能否决转换、阻塞事件折叠或输入投递;检查回调必须异步等待 I/O,
34
+ 并在请求副作用前响应取消。它们只能向独立的、受精确栅栏保护的升级协调器提出请求。
35
+ Controller 的 Host 状态 hook 提议版本对账;Host 的 Session 状态 hook 在附件和结清时
36
+ 异步观测当前 Skill 元数据,不读正文、不提交通知基线、不假定模型记忆。
37
+ 逐输入完整性核验仍是投递安全门:无效 Skill 可以拒绝该次输入,但可选检查不能卡住生命周期。
38
+ 仍只有被接受的输入推进通知基线,未变化输入原样通过,不额外注入内容。
39
+
40
+ Controller 启动、Host 连接/退出观测、Host 状态变化 signal 和事件持久化均可触发版本检查,
41
+ 不依赖显式 `update`。现有周期对账覆盖丢失提示、无法可靠提供后台执行结束事件的提供方,
42
+ 以及无法在退出后发出提示的优雅退出。
43
+ 比较对象是实际加载的 Host/Endpoint 实现与当前 Controller 已加载的目标,不查询 npm `latest`。
44
+ 版本匹配但就绪证据未齐不是升级理由。关闭时停止检查,已准入工作仍保留现有持久 mailbox;
45
+ 请求固定的目标与当前 Controller 不同时保持等待,交由显式恢复,不静默改目标。
46
+ 检查不会复活明确停止的 Session。
47
+
48
+ ## Host 升级与上下文检查
49
+
50
+ `update` 分别报告程序安装、Controller 启动核验及逐 Host 的状态。
51
+ `update --hosts-status` 只读查询逐 Host 证据,不安装、启动或推进切换。支持此能力的
52
+ 受控 Codex Host 先关闭新输入准入,等待当前原生轮次和事件持久化、核验后台执行,
53
+ 释放自己持有的 Endpoint 后优雅退出。现有 role-runtime mailbox 保存精确实现代际、
54
+ native Session、配置摘要和排空回执,Controller 重启后仍可继续。
55
+ 后继使用原 effective 配置接回同一对话;实际加载的 Host/Endpoint、控制与事件能力
56
+ 握手通过后才确认成功。队列中的 Message 原文不变,不重放 accepted 或 unknown 输入。
57
+
58
+ 暂停角色保持暂停。旧 Host、仅有原生 TUI 的附件及不能证明静止的 Endpoint
59
+ 保留并报告不支持,不反复发送不支持的排空命令。忙碌或未知执行不承诺限时升级。
60
+ 启动结果未知不自动重放;后续精确成功握手可结清原请求。
61
+ 准入栅栏覆盖 Host 控制的入口,不控制任意外部原生客户端,也不会终止共享 daemon 或原生对话。
62
+
63
+ Host 仍逐输入机械检查 Skills。未变化时原文原样通过,不添加检查通知、隐藏标记、
64
+ 分隔换行或原生输入片段。附件加载指引说明这一托管输入约定,不因没有通知而重复检查。
65
+ 绕过 Host 的客户端或当前输入路由不明时仍通过 Manifest 显式检查;复用同一对话
66
+ 本身不证明当前输入受 Host 托管。首次附件、局部 Skill 变化、已观测到的原生压缩
67
+ 提供相应加载指引;压缩使下一次输入的元数据基线失效。
68
+ 提供方无法可靠暴露事件时,Agent 仍须通过 Manifest 恢复缺失指令。
69
+ 版本和已接受的通知都不能证明模型记忆;例行检查、角色版本和空 inbox 不作为用户进度播报。
70
+
71
+ ## 会话停止与运行时观测
72
+
5
73
  局部停止等待自身的持久清理义务完成,不等待整个 Home 的 `scheduler.scan`
6
74
  循环排空。signal 只请求调度;完成仍以现有物理停止证明及清理事务为准。
7
75
  超时保留义务并阻止替换;选择失败和新会话启动未确认分别报告,不允许重放未知效果。
@@ -16,7 +84,8 @@ interrupt-owned 输入仍保留原处理路径。
16
84
  目标/请求标签仅保存哈希;每组最多保留 16 个近期样本、16 个活动样本及最慢完成样本。
17
85
  in-flight 请求数不是排队数,服务端不能独自测量客户端到路由的完整等待。
18
86
  观测不能反推旧事故的唯一根因,
19
- 也不构成静止证明;重启 Controller 不会刷新存活 Host。
87
+ 也不构成静止证明;Controller 重启本身不证明 Host 已替换,
88
+ 异步生命周期检查仍须经过安全排空和后继实际握手。
20
89
 
21
90
  `observations.commands.routes.transport` 使用与 route service time 相同的请求哈希,
22
91
  分别关联 socket accept→route、完整帧 receive→route、route→本地 flush。
@@ -80,7 +80,8 @@ upstream is left untouched and carries no such promise.
80
80
 
81
81
  ## Scope of adoption
82
82
 
83
- This operation adds no persistent Yui schema or migration. Installing code
84
- that implements it does not itself refresh a real Project. A normal authorized
83
+ Refresh bookkeeping adds no persistent Yui schema. The shared remote Git
84
+ budget settings require storage 1.29; see [configuration](roles-and-configuration.md#git-operation-budgets).
85
+ Installing this code does not itself refresh a real Project. A normal authorized
85
86
  refresh applies the behavior; Task completion, integration, publication and
86
87
  archive remain separate operations.
@@ -63,5 +63,6 @@ upstream 保持原样,不作这一承诺。
63
63
 
64
64
  ## 采用边界
65
65
 
66
- 本操作不新增 Yui 持久 schema 或迁移。安装实现代码不等于刷新真实 Project;
66
+ refresh 记账不新增 Yui 持久 schema;共享远程 Git 预算配置要求存储 1.29,
67
+ 见[配置说明](roles-and-configuration.zh-CN.md#git-操作预算)。安装实现代码不等于刷新真实 Project;
67
68
  正常获授权的 refresh 才应用此行为。Task 完成、集成、发布和归档仍是独立操作。