@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
@@ -109,7 +109,7 @@ async function diagnoseProject(args, store, options) {
109
109
  data: { project, diagnosis: "no-remote" }
110
110
  };
111
111
  }
112
- const git = options.git ?? new NodeGitWorkspace();
112
+ const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
113
113
  const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
114
114
  try {
115
115
  requireUnchangedMaintenanceProject(store, project, "diagnose");
@@ -153,7 +153,7 @@ async function discoverProjects(args, store, options) {
153
153
  const workspace = store.getConfig().defaultWorkspace;
154
154
  if (workspace === undefined)
155
155
  throw usageError("No default workspace is configured; run yui setup.");
156
- const git = options.git ?? new NodeGitWorkspace();
156
+ const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
157
157
  const entries = await readdir(workspace, { withFileTypes: true });
158
158
  const discovered = [];
159
159
  for (const entry of entries) {
@@ -195,7 +195,7 @@ function renderDiscoveredProjects(projects) {
195
195
  async function cloneProject(args, store, options) {
196
196
  const usage = "Project clone usage: yui project clone <name> <remote> [--alias <name> ...] [--stable <ref>] [--development <ref>] [--external].";
197
197
  const parsed = parseCloneArguments(args, usage);
198
- const git = options.git ?? new NodeGitWorkspace();
198
+ const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
199
199
  const name = validateProjectName(parsed.name);
200
200
  const stableRef = parsed.stable ?? "HEAD";
201
201
  const developmentRef = parsed.development ?? stableRef;
@@ -282,7 +282,7 @@ async function migrateProject(args, store, options) {
282
282
  if (project.remoteUrl === undefined) {
283
283
  throw usageError(`Project migrate requires a remote URL: ${project.id}.`);
284
284
  }
285
- const git = options.git ?? new NodeGitWorkspace();
285
+ const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
286
286
  const destination = managedProjectPath(store.rootDirectory(), project.id);
287
287
  // Migration rewrites the Project's Git repository: hold the per-Project
288
288
  // maintenance fence so no rebuild/archive/cleanup (or a second migrate)
@@ -444,7 +444,7 @@ async function addProject(args, store, options) {
444
444
  const usage = "Project add usage: yui project add <name> <path> [--alias <name> ...] [--remote <url>] [--stable <ref>] [--development <ref>].";
445
445
  const parsed = parseAddArguments(args, usage);
446
446
  assertOutsideManagedWorktrees(parsed.path, store.rootDirectory());
447
- const git = options.git ?? new NodeGitWorkspace();
447
+ const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
448
448
  const head = await git.inspect(parsed.path, "HEAD");
449
449
  assertOutsideManagedWorktrees(head.root, store.rootDirectory());
450
450
  if (!await git.isClean(head.root)) {
@@ -483,7 +483,7 @@ async function updateProject(args, store, options) {
483
483
  : { developmentBranch: parsed.development })
484
484
  };
485
485
  assertProjectAvailable(store, updateProjectMetadata(current, patch, now), current.id);
486
- const git = options.git ?? new NodeGitWorkspace();
486
+ const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
487
487
  if (parsed.stable !== undefined) {
488
488
  const head = await git.inspect(current.path, "HEAD");
489
489
  if (!await git.isClean(head.root)) {
@@ -568,7 +568,7 @@ async function resetProject(args, store, options) {
568
568
  if (project.stableBranch !== project.developmentBranch) {
569
569
  throw usageError(`Project reset requires matching stable and development branches: ${project.id}.`);
570
570
  }
571
- const git = options.git ?? new NodeGitWorkspace();
571
+ const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
572
572
  const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
573
573
  try {
574
574
  // Re-read under the fence so a concurrent catalog change can never drive
@@ -693,7 +693,7 @@ async function replaceProject(args, store, options) {
693
693
  throw usageError(`Project replace discards the current checkout and re-clones from ${project.remoteUrl}. `
694
694
  + "Re-run with --discard-local to acknowledge that the checkout and any uncommitted state will be discarded.");
695
695
  }
696
- const git = options.git ?? new NodeGitWorkspace();
696
+ const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
697
697
  const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
698
698
  try {
699
699
  const current = requireProject(store, project.id);
@@ -909,7 +909,7 @@ async function deleteProjectCommand(args, store, options) {
909
909
  // Heal a crashed earlier attempt before the prechecks turn.
910
910
  await healCheckoutSwap({ currentPath: project.path, backupPath: tombstone });
911
911
  if (existsSync(project.path)) {
912
- const git = options.git ?? new NodeGitWorkspace();
912
+ const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
913
913
  const worktrees = await git.listWorktrees(project.path);
914
914
  const linked = worktrees.filter((path) => path !== project.path);
915
915
  if (linked.length > 0) {
@@ -27,7 +27,7 @@ export async function verifyTaskCompletionPublishedTree(taskId, publicationId, s
27
27
  throw usageError(`Publication ${publication.id} must record exact local and remote commits.`);
28
28
  }
29
29
  const workspace = requireTaskWorkspace(store, task);
30
- const git = options.git ?? new NodeGitWorkspace();
30
+ const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
31
31
  const actualHeads = new Map();
32
32
  for (const taskBinding of task.projectBindings) {
33
33
  const entry = workspaceProjectEntry(workspace, taskBinding.projectId);
@@ -123,7 +123,7 @@ async function start(args, store, home, now, options) {
123
123
  throw usageError(`Task main worktree is not ready; reconcile the Task first: ${task.id}.`);
124
124
  }
125
125
  taskIntegrationTargetCheckout(store, options.environment, task.id, project.id, targetRef);
126
- const expectedHead = (await new NodeGitWorkspace().inspect(mainEntry.path, targetRef)).baseCommit;
126
+ const expectedHead = (await new NodeGitWorkspace(() => store.getConfig()).inspect(mainEntry.path, targetRef)).baseCommit;
127
127
  const snapshotEntry = candidate.workspace.entries.find(({ projectId }) => projectId === project.id);
128
128
  const resultCommit = candidate.gitSnapshot.projects.find(({ projectId }) => projectId === project.id)?.commit;
129
129
  if (snapshotEntry === undefined || resultCommit === undefined) {
@@ -74,7 +74,7 @@ export async function runTaskPublicationAdoptCommand(args, store, options = {})
74
74
  return { task, publication, completion, acceptedCommit, workspace, entry, integration, actor };
75
75
  };
76
76
  const facts = store.transaction(read);
77
- const git = options.git ?? new NodeGitWorkspace();
77
+ const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
78
78
  const localCommit = facts.publication.localCommit;
79
79
  const [acceptedTree, candidateTree, diff] = await Promise.all([
80
80
  git.resolveTree(facts.entry.path, facts.acceptedCommit),
@@ -19,6 +19,10 @@ export function taskRoleHostDiagnostic(host) {
19
19
  const delivery = snapshot.eventDelivery;
20
20
  return [
21
21
  `Provider Host=${snapshot.state}`,
22
+ `loaded Host=${snapshot.hostImplementation?.generation ?? "unreported"}`,
23
+ `loaded Endpoint=${snapshot.endpointImplementation?.generation ?? "unreported"}`,
24
+ snapshot.hostUpgrade === undefined ? undefined
25
+ : `Host upgrade=${snapshot.hostUpgrade.state} (${snapshot.hostUpgrade.requestId})${snapshot.hostUpgrade.detail ? `: ${snapshot.hostUpgrade.detail}` : ""}`,
22
26
  `terminal reconciliation=${snapshot.controlCapabilities?.includes("reconcile-turn") ? "supported" : "unreported/unsupported"}`,
23
27
  delivery === undefined ? "event delivery=unreported" :
24
28
  `pending facts=${delivery.pending}; native terminals awaiting acknowledgement=${delivery.pendingTerminals}`,
@@ -48,7 +48,7 @@ async function integrateUpstream(args, store, home, options) {
48
48
  throw usageError(`Task must be active to integrate upstream: ${taskId}/${task.status}.`);
49
49
  }
50
50
  taskLocalActor(store, options.environment, task.id);
51
- const git = options.git ?? new NodeGitWorkspace();
51
+ const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
52
52
  const now = options.now ?? (() => new Date());
53
53
  await new FileTaskWorkspacePreparer(home, store, git, now)
54
54
  .prepareTaskWorkspace(task.id);
@@ -20,6 +20,9 @@ export const CONFIG_DEFINITIONS = Object.freeze([
20
20
  { key: "resources-quarantine-ttl-hours", domain: "resources", property: "resourcesQuarantineTtlHours", label: "Resource quarantine TTL", summary: "Default observation window before purge, 1-720 hours (default: 24).", takesEffect: "The next resource GC command without an explicit TTL override." },
21
21
  { key: "tmux-bin", domain: "tools", property: "tmuxBin", label: "Tmux bin", summary: "Command or path to the tmux binary (default: tmux).", takesEffect: "The next CLI-owned tmux invocation; restart the Controller for Controller-owned launches." },
22
22
  { key: "tmux-history-limit", domain: "tools", property: "tmuxHistoryLimit", label: "Tmux history limit", summary: "History lines retained for newly created tmux sessions, 1000-1000000 (default: 100000).", takesEffect: "New tmux sessions; existing sessions report drift until recreated." },
23
+ { key: "git-local-timeout-seconds", domain: "tools", property: "gitLocalTimeoutSeconds", label: "Git local total timeout", summary: "Total elapsed-time limit for each local Git command, 1-86400 seconds (default: 30). Independent of remote transport budgets.", takesEffect: "The next local Git command, including Controller-owned commands; in-flight commands keep their budgets." },
24
+ { key: "git-remote-inactivity-timeout-seconds", domain: "tools", property: "gitRemoteInactivityTimeoutSeconds", label: "Git remote inactivity timeout", summary: "Maximum silence on stdout/stderr for clone, remote fetch and ls-remote, 1-86400 seconds (default: 120). Local Git uses its separate configurable total budget.", takesEffect: "The next remote Git command, including Controller-owned commands; in-flight commands keep their budgets." },
25
+ { key: "git-remote-timeout-seconds", domain: "tools", property: "gitRemoteTimeoutSeconds", label: "Git remote total timeout", summary: "Total elapsed-time limit for each clone, remote fetch or ls-remote, 1-86400 seconds (default: 1800), even while progress continues.", takesEffect: "The next remote Git command, including Controller-owned commands; in-flight commands keep their budgets." },
23
26
  { key: "telemetry-enabled", domain: "tools", property: "telemetryEnabled", label: "Diagnostic telemetry", summary: "Enable the optional SQLite diagnostic telemetry projection (default: false); requires a SQLite Home.", takesEffect: "Standalone telemetry commands immediately; scheduler writes after Controller restart." },
24
27
  { key: "telemetry-terminal-keep", domain: "tools", property: "telemetryTerminalKeep", label: "Telemetry terminal keep", summary: "Positive-integer rows retained per terminal AgentRun (default: 200).", takesEffect: "Explicit retention immediately; automatic retention after the Controller restarts." },
25
28
  { key: "telemetry-run-cap", domain: "tools", property: "telemetryRunCap", label: "Telemetry AgentRun cap", summary: `Positive-integer maximum rows retained per AgentRun, at most ${MAX_RUN_CAP} (default: 50000).`, takesEffect: "Explicit retention immediately; automatic write-time capping after the Controller restarts." }
@@ -0,0 +1,20 @@
1
+ export const DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS = 30;
2
+ export const DEFAULT_GIT_REMOTE_INACTIVITY_TIMEOUT_SECONDS = 120;
3
+ export const DEFAULT_GIT_REMOTE_TIMEOUT_SECONDS = 1_800;
4
+ export function resolveGitLocalTimeoutSeconds(value) {
5
+ return timeoutSeconds(value, DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS, "gitLocalTimeoutSeconds");
6
+ }
7
+ export function resolveGitRemoteInactivityTimeoutSeconds(value) {
8
+ return timeoutSeconds(value, DEFAULT_GIT_REMOTE_INACTIVITY_TIMEOUT_SECONDS, "gitRemoteInactivityTimeoutSeconds");
9
+ }
10
+ export function resolveGitRemoteTimeoutSeconds(value) {
11
+ return timeoutSeconds(value, DEFAULT_GIT_REMOTE_TIMEOUT_SECONDS, "gitRemoteTimeoutSeconds");
12
+ }
13
+ function timeoutSeconds(value, fallback, label) {
14
+ if (value === undefined)
15
+ return fallback;
16
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 86_400) {
17
+ throw new TypeError(`${label} must be an integer from 1 to 86400.`);
18
+ }
19
+ return value;
20
+ }
@@ -91,6 +91,7 @@ export const MAX_CONTROLLER_TASK_CONCURRENCY = 32;
91
91
  export const DEFAULT_AGENT_LAUNCH_INACTIVITY_TIMEOUT_SECONDS = 300;
92
92
  export const DEFAULT_DELIVERY_TIMEOUT_SECONDS = 120;
93
93
  export const DEFAULT_TMUX_HISTORY_LIMIT = 100_000;
94
+ export { DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS, DEFAULT_GIT_REMOTE_INACTIVITY_TIMEOUT_SECONDS, DEFAULT_GIT_REMOTE_TIMEOUT_SECONDS, resolveGitLocalTimeoutSeconds, resolveGitRemoteInactivityTimeoutSeconds, resolveGitRemoteTimeoutSeconds } from "./gitConfig.js";
94
95
  export function resolveRuntimeHealth(configured) {
95
96
  if (configured === undefined || configured === null)
96
97
  return DEFAULT_RUNTIME_HEALTH_POLICY;
@@ -1,4 +1,5 @@
1
1
  import { accessSync, constants, lstatSync, readFileSync, realpathSync } from "node:fs";
2
+ import { access, lstat, readFile, realpath } from "node:fs/promises";
2
3
  import { join, resolve } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  export const BUILTIN_YUI_SKILLS = new Set([
@@ -41,9 +42,39 @@ export function checkCurrentSkills(home, ids) {
41
42
  }
42
43
  });
43
44
  }
45
+ /** Advisory lifecycle observation uses asynchronous filesystem I/O; it neither
46
+ * blocks a state transition nor substitutes for the exact per-input check. */
47
+ export async function checkCurrentSkillsAsync(home, ids, signal) {
48
+ const skills = [];
49
+ for (const id of new Set(ids)) {
50
+ signal.throwIfAborted();
51
+ const path = currentSkillPath(home, id);
52
+ const versionPath = join(path, "VERSION");
53
+ try {
54
+ for (const file of [join(path, "SKILL.md"), versionPath]) {
55
+ if (await realpath(file) !== resolve(file) || !(await lstat(file)).isFile()) {
56
+ throw new Error(`Skill files must be regular files without symlinks: ${file}.`);
57
+ }
58
+ await access(file, constants.R_OK);
59
+ }
60
+ if ((await lstat(versionPath)).size > 128)
61
+ throw new Error("VERSION exceeds 128 bytes.");
62
+ const version = (await readFile(versionPath, { encoding: "utf8", signal })).trim();
63
+ if (!/^[1-9][0-9]{0,8}$/u.test(version))
64
+ throw new Error("VERSION must contain a positive internal integer.");
65
+ skills.push({ id, path, version, versionPath });
66
+ }
67
+ catch (error) {
68
+ throw new Error(`Current Skill ${id} is missing, unreadable or invalid at ${path}. `
69
+ + `${error instanceof Error ? error.message : String(error)}`, { cause: error });
70
+ }
71
+ }
72
+ return skills;
73
+ }
44
74
  export const SKILL_CHECK_COMMAND = "yui session skills --json";
45
75
  export const SKILL_REUSE_RULE = "The managed Host checks current Skill versions/availability before each input; "
46
- + "do not repeat an explicit Host check. Without that notice, check through the Manifest's command. "
76
+ + "unchanged inputs carry no notice or marker, and do not require a duplicate check. "
77
+ + "For unmediated clients or uncertain current input routing, check through the Manifest's command; conversation reuse alone does not establish Host mediation. Keep routine checks internal, not user-facing progress. "
47
78
  + "Reuse unchanged full instructions in context; messages alone require no reload. "
48
79
  + "Read affected entries/references fully on change or localized loss. On first entry, recovery or lost context, "
49
80
  + "rebuild the current working set from applicable Skill entry points and action-relevant references, not all past procedures. "
@@ -1,9 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { chmodSync, readFileSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
3
4
  import { join, resolve } from "node:path";
4
5
  import { writeTextFileAtomically } from "../storage/durableFile.js";
5
6
  import { validateSkillPackageRef } from "./skillPackage.js";
6
- import { checkCurrentSkills, SKILL_CHECK_COMMAND, SKILL_REUSE_RULE } from "./currentSkills.js";
7
+ import { checkCurrentSkills, checkCurrentSkillsAsync, SKILL_CHECK_COMMAND, SKILL_REUSE_RULE } from "./currentSkills.js";
7
8
  import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
8
9
  export { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
9
10
  /** Portable read pointers accompany every managed input, including the first
@@ -22,11 +23,11 @@ export function withSessionContextPointer(text, environment) {
22
23
  * Unmediated/native clients retain the explicit diagnostic fallback. */
23
24
  export function sessionContextInstructions(manifestPath, digest) {
24
25
  return `Yui Session Manifest: ${manifestPath}${digest === undefined ? "" : ` (digest ${digest})`}. `
25
- + "For inputs carrying the Yui Host Skill-check notice, engineering has checked current versions; do not repeat that check. "
26
- + `Otherwise run its contextProtocol.skillCheckCommand (fallback: ${SKILL_CHECK_COMMAND}). `
26
+ + "The managed Host checks Skills internally and injects guidance only when loading is needed; absence of a notice does not require another check. "
27
+ + `For unmediated clients or uncertain current input routing, run its contextProtocol.skillCheckCommand (fallback: ${SKILL_CHECK_COMMAND}); conversation reuse alone does not establish Host mediation. `
27
28
  + "On first entry or lost instructions, read the current applicable Skill entry points, then only references needed for the current action. "
28
29
  + "On change reload affected instructions. Compaction does not require restoring every previously used procedure. "
29
- + "Preserve the Manifest Role profile; metadata never proves loading or memory.";
30
+ + "Preserve the Manifest Role profile; metadata never proves loading or memory. Keep routine role, version and empty-inbox checks internal.";
30
31
  }
31
32
  /** The caller must already pass the ordinary managed Session/CLI/Home checks.
32
33
  * Old manifests remain immutable audit records, while this read resolves their
@@ -40,6 +41,15 @@ export function checkSessionSkills(home, manifestPath) {
40
41
  historicalPackagesAreAuditOnly: true
41
42
  };
42
43
  }
44
+ export async function checkSessionSkillsAsync(home, manifestPath, signal) {
45
+ const manifest = validateSessionBootstrapManifest(JSON.parse(await readFile(resolve(manifestPath), { encoding: "utf8", signal })));
46
+ return {
47
+ skills: await checkCurrentSkillsAsync(home, manifest.skills.map(skill => skill.id), signal),
48
+ roleProfileRef: manifest.roleProfileRef,
49
+ rule: SKILL_REUSE_RULE,
50
+ historicalPackagesAreAuditOnly: true
51
+ };
52
+ }
43
53
  /** Read back one immutable Session Manifest and verify its content digest. */
44
54
  export function readSessionBootstrapManifest(path) {
45
55
  const source = resolve(path);
@@ -50,6 +60,9 @@ export function readSessionBootstrapManifest(path) {
50
60
  catch (error) {
51
61
  throw new Error(`Session Manifest is unreadable: ${source}.`, { cause: error });
52
62
  }
63
+ return validateSessionBootstrapManifest(parsed);
64
+ }
65
+ function validateSessionBootstrapManifest(parsed) {
53
66
  if (parsed === null || typeof parsed !== "object") {
54
67
  throw new Error("Session Manifest is invalid.");
55
68
  }
@@ -1,4 +1,26 @@
1
- import { checkSessionSkills } from "./sessionBootstrapManifest.js";
1
+ import { checkSessionSkills, checkSessionSkillsAsync } from "./sessionBootstrapManifest.js";
2
+ /** Early advisory checks do not commit notices or assert model memory. The
3
+ * next input still validates its exact metadata and emits only its own delta.
4
+ * Invalid metadata may reject that input, never a Session state transition. */
5
+ export function createSessionSkillLifecycleCheck(home, environment) {
6
+ return {
7
+ id: "session-skills",
8
+ run: async (transition, signal) => {
9
+ if (!["connected", "interactive-connected", "settled"].includes(transition.event.type))
10
+ return;
11
+ const manifest = environment().YUI_SESSION_MANIFEST;
12
+ if (!manifest || signal.aborted)
13
+ return;
14
+ try {
15
+ await checkSessionSkillsAsync(home, manifest, signal);
16
+ }
17
+ catch (error) {
18
+ if (!signal.aborted)
19
+ throw error;
20
+ }
21
+ }
22
+ };
23
+ }
2
24
  /** Host-local last accepted metadata, never a model loading/memory receipt.
3
25
  * A new Host/attachment starts cold. No durable schema or input replay is added. */
4
26
  export function createSessionSkillNotice(home) {
@@ -10,9 +32,7 @@ export function createSessionSkillNotice(home) {
10
32
  prepare(text, environment, sessionKey, options = {}) {
11
33
  const manifest = environment.YUI_SESSION_MANIFEST;
12
34
  if (manifest === undefined)
13
- return { text, commit() { } };
14
- if (options.recovery)
15
- invalidate();
35
+ return { text, inputText: text, commit() { } };
16
36
  let current;
17
37
  try {
18
38
  current = checkSessionSkills(home, manifest);
@@ -21,7 +41,7 @@ export function createSessionSkillNotice(home) {
21
41
  invalidate();
22
42
  throw error; // Do not submit business input with an invalid current installation.
23
43
  }
24
- const key = JSON.stringify([sessionKey, manifest, current.roleProfileRef]);
44
+ const key = JSON.stringify([sessionKey, manifest, current.roleProfileRef, options.contextEpoch]);
25
45
  const skills = new Map(current.skills.map(skill => [skill.id, JSON.stringify({
26
46
  id: skill.id, path: skill.path, version: skill.version
27
47
  })]));
@@ -30,17 +50,18 @@ export function createSessionSkillNotice(home) {
30
50
  .map(([, value]) => value);
31
51
  const removed = !cold && [...previous.skills.keys()].some(id => !skills.has(id));
32
52
  // Every input is checked, but only a lifecycle/metadata event needs an
33
- // instruction-loading notice. This marker distinguishes Host-mediated
34
- // input from an unmediated client without restating the operating manual.
35
- const lines = ["Yui Host checked current Skills; no Agent check needed."];
53
+ // instruction-loading notice. Unchanged input is passed through exactly.
54
+ const lines = [];
36
55
  if (cold || changed.length > 0 || removed) {
37
- lines.push(`Yui Session Manifest: ${manifest}`, cold
38
- ? `First input or recovered attachment: read the exact Role profile at ${current.roleProfileRef.path} and the full current listed Skill entry points. Follow references only for the action you are taking, not to restore every previously used procedure. A Task role-profile projection is not this profile.`
39
- : "Skill metadata changed: load affected entries and applicable references; retain other complete instructions.", `Current Skill pointers: [${(removed ? [...skills.values()] : changed).join(",")}]`, "Metadata is not a loading receipt. Read each required file through EOF; if output is truncated, finish the missing ranges. Recover missing instructions for the current task, not the whole manual.");
56
+ lines.push(`Yui Session Manifest: ${manifest}`, "Host-managed Skill checks are active; unchanged input has no notice. Detached/unmediated clients must use the Manifest check fallback.", cold
57
+ ? `Read the exact Role profile at ${current.roleProfileRef.path} (not a Task projection) and all current Skill entry points below. Follow references only for the current action.`
58
+ : "Skill metadata changed: load affected entries and applicable references; retain other complete instructions.", `Current Skill pointers: [${(removed ? [...skills.values()] : changed).join(",")}]`, "Read required files through EOF. Metadata is not loaded instructions. Keep routine checks internal.");
40
59
  }
41
60
  const preparedGeneration = generation;
42
61
  return {
43
- text: `${lines.join("\n")}\n\n${text}`,
62
+ text: lines.length === 0 ? text : `${lines.join("\n")}\n\n${text}`,
63
+ inputText: text,
64
+ ...(lines.length === 0 ? {} : { context: lines.join("\n") }),
44
65
  commit() {
45
66
  if (generation !== preparedGeneration)
46
67
  return;
@@ -1111,7 +1111,7 @@ export async function main() {
1111
1111
  if (action === "list") {
1112
1112
  if (resolved.length !== 4)
1113
1113
  throw usageError(usage);
1114
- data = await listArtifactsCapability(home, taskId);
1114
+ data = await listArtifactsCapability(home, taskId, () => store.getConfig());
1115
1115
  }
1116
1116
  else if (action === "read") {
1117
1117
  const relativePath = resolved[4];
@@ -1120,7 +1120,7 @@ export async function main() {
1120
1120
  throw usageError(usage);
1121
1121
  data = await readArtifactCapability(home, taskId, {
1122
1122
  relativePath, ...(commit === undefined ? {} : { commit })
1123
- });
1123
+ }, () => store.getConfig());
1124
1124
  }
1125
1125
  else {
1126
1126
  // save: a delivery-authoritative action; a managed Task caller must be the current Leader.
@@ -1156,7 +1156,7 @@ export async function main() {
1156
1156
  relativePath, content, ...(encoding === undefined ? {} : { encoding }),
1157
1157
  ...(message === undefined ? {} : { message }),
1158
1158
  ...(expectedHead === undefined ? {} : { expectedHead })
1159
- });
1159
+ }, () => store.getConfig());
1160
1160
  }
1161
1161
  emit(JSON.stringify(data, null, 2), false, data);
1162
1162
  return;
@@ -2515,7 +2515,7 @@ async function deltaRecheckPreflightForTaskCommand(args, store, actualTaskReview
2515
2515
  repositoryPaths,
2516
2516
  previousRound: previous,
2517
2517
  candidate: actualTaskReviewCandidate,
2518
- git: new NodeGitWorkspace()
2518
+ git: new NodeGitWorkspace(() => store.getConfig())
2519
2519
  });
2520
2520
  if (assessment.kind === "ineligible") {
2521
2521
  throw usageError(`Delta-recheck is technically unavailable: ${assessment.reason}`);
@@ -59,21 +59,31 @@ export async function assertFileTaskControllerStorageCompatible(home, options =
59
59
  export async function ensureFileTaskController(home, options = {}) {
60
60
  const call = options.call ?? callController;
61
61
  await waitForForeignHandover(home, options);
62
- try {
63
- const status = await call(home, "controller.status", {});
64
- assertCompatibleControllerStatus(status, options.expectedIdentity);
65
- return status;
66
- }
67
- catch (error) {
68
- if (!isUnavailable(error))
69
- throw error;
62
+ const timeoutMs = positive(options.startupTimeoutMs, STARTUP_TIMEOUT_MS, "startupTimeoutMs");
63
+ const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
64
+ const probeDeadline = Date.now() + timeoutMs;
65
+ for (;;) {
66
+ try {
67
+ const status = await call(home, "controller.status", {});
68
+ assertCompatibleControllerStatus(status, options.expectedIdentity);
69
+ return status;
70
+ }
71
+ catch (error) {
72
+ if (isUnavailable(error))
73
+ break;
74
+ // A closing endpoint may reset a read even after its owner exited.
75
+ // Retry only this observation: ambiguity never authorizes a spawn.
76
+ if (!(error instanceof ControllerClientError)
77
+ || error.code !== "CONTROLLER_DELIVERY_UNKNOWN"
78
+ || Date.now() >= probeDeadline)
79
+ throw error;
80
+ }
81
+ await delay(pollMs);
70
82
  }
71
83
  // A handover may have started after the first lock check and before the
72
84
  // failed discovery/socket call. Re-check before spawning so a managed
73
85
  // Session can never resurrect the old Controller during an update.
74
86
  await waitForForeignHandover(home, options);
75
- const timeoutMs = positive(options.startupTimeoutMs, STARTUP_TIMEOUT_MS, "startupTimeoutMs");
76
- const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
77
87
  const spawnController = options.spawnController ?? spawnDetachedFileTaskController;
78
88
  // A readiness timeout is not exit evidence. Reuse the observable startup
79
89
  // process for this exact Home rather than multiplying unready Controllers.
@@ -213,17 +223,26 @@ async function waitForControllerProcessExit(controller, options) {
213
223
  const timeoutMs = positive(options.shutdownTimeoutMs, CONTROLLER_SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
214
224
  const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
215
225
  const deadline = Date.now() + timeoutMs;
216
- // This check is tri-state: unreadable OS identity throws rather than
217
- // collapsing an unknown/live process into the absence of a Controller match.
218
- while (processGenerationIsLive(controller.pid, controller.processStartIdentity)) {
219
- if (process.platform === "linux") {
220
- const observed = readLinuxProcessIdentity(controller.pid);
221
- // Preserve the existing physical-stop boundary: an exact zombie has
222
- // exited, even if its parent has not reaped the process table entry.
223
- if (observed?.startIdentity === controller.processStartIdentity && observed.state === "Z")
226
+ for (;;) {
227
+ let observationError;
228
+ try {
229
+ if (!processGenerationIsLive(controller.pid, controller.processStartIdentity))
224
230
  return;
231
+ if (process.platform === "linux") {
232
+ const observed = readLinuxProcessIdentity(controller.pid);
233
+ // An exact zombie has exited even before its parent reaps it.
234
+ if (observed?.startIdentity === controller.processStartIdentity && observed.state === "Z")
235
+ return;
236
+ }
237
+ }
238
+ catch (error) {
239
+ // OS identity can disappear before kill(pid, 0) reports absence.
240
+ // Keep observing within the existing stop budget, never infer exit.
241
+ observationError = error;
225
242
  }
226
243
  if (Date.now() >= deadline) {
244
+ if (observationError !== undefined)
245
+ throw observationError;
227
246
  throw new ControllerClientError("CONTROLLER_TIMEOUT", `Controller process ${controller.pid} did not exit within ${timeoutMs} ms.`);
228
247
  }
229
248
  await delay(pollMs);
@@ -14,7 +14,7 @@ import { KeyedWorkQueue } from "../coordination/keyedWorkQueue.js";
14
14
  import { MailboxScheduler } from "../coordination/mailboxScheduler.js";
15
15
  import { nearestDeadlineBatch } from "../coordination/deadlineScheduler.js";
16
16
  import { mailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js";
17
- import { hasRuntimeCleanupObligation } from "../runtime/lifecycleReservation.js";
17
+ import { hasRuntimeCleanupObligation, runtimeCleanupDisposition } from "../runtime/lifecycleReservation.js";
18
18
  import { formatTaskRecordReference } from "../task/taskRecordReference.js";
19
19
  import { parseDurableJobAcknowledgeParams, parseDurableJobCancelParams, parseDurableJobRefParams, parseDurableJobStartParams } from "./jobControl.js";
20
20
  const DEFAULT_RECONCILIATION_INTERVAL_MS = reconciliationIntervalMilliseconds();
@@ -211,6 +211,12 @@ async function processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost
211
211
  const owner = runtimeOwner(target);
212
212
  const batchId = runtimeLifecycleBatchIdentity(target, mailbox);
213
213
  if (hasRuntimeCleanupObligation(mailbox)) {
214
+ // Upgrade is a graceful, exact-identity operation, never the destructive
215
+ // cleanup path. Its coordinator owns drain/restore; retain backpressure.
216
+ if (runtimeCleanupDisposition(mailbox) === "upgrade-host") {
217
+ markFailedRuntimeTarget(failedRoles, target);
218
+ continue;
219
+ }
214
220
  try {
215
221
  if (lifecycleHost === undefined) {
216
222
  throw new Error("Role runtime cleanup host is unavailable.");
@@ -698,6 +704,7 @@ export class FileTaskController {
698
704
  #providerRetry;
699
705
  #continuationReconciler;
700
706
  #hostTerminalRecovery;
707
+ #lifecycleChecks;
701
708
  #persistenceMetrics;
702
709
  #diagnosticMetrics;
703
710
  #current;
@@ -754,6 +761,7 @@ export class FileTaskController {
754
761
  this.#schedules = options.schedules;
755
762
  this.#continuationReconciler = options.continuationReconciler;
756
763
  this.#hostTerminalRecovery = options.hostTerminalRecovery;
764
+ this.#lifecycleChecks = options.lifecycleChecks;
757
765
  this.#globalInputDelivery = options.globalInputDelivery;
758
766
  this.#messageHealth = options.messageHealth;
759
767
  this.#providerRetry = options.providerRetry;
@@ -842,6 +850,7 @@ export class FileTaskController {
842
850
  return;
843
851
  if (this.#stopped)
844
852
  throw new Error("Controller runtime is stopped.");
853
+ this.#lifecycleChecks?.notify("startup");
845
854
  this.#requestBackgroundPump();
846
855
  this.#requestRuntimeObservation();
847
856
  // Arm the Operator lane once for work that was already pending when the
@@ -862,6 +871,7 @@ export class FileTaskController {
862
871
  }
863
872
  stop() {
864
873
  this.#stopped = true;
874
+ this.#lifecycleChecks?.close();
865
875
  this.#workspaceWaitAbort.abort(new Error("Controller stopped while waiting for Project maintenance."));
866
876
  this.#signalScheduler.stop();
867
877
  this.#operatorSignalScheduler.stop();
@@ -905,6 +915,7 @@ export class FileTaskController {
905
915
  if (this.#stopped)
906
916
  throw new Error("Controller runtime is stopped.");
907
917
  parseMailboxKey(key);
918
+ this.#lifecycleChecks?.notify("signal");
908
919
  if (key === "operator") {
909
920
  // Main lane owns durable Hook folding; the Operator lane is the sole
910
921
  // consumer of the Operator delivery mailbox.
@@ -1017,6 +1028,8 @@ export class FileTaskController {
1017
1028
  }
1018
1029
  }
1019
1030
  const firstRuntimeDrain = await this.#phases.observe("runtime-drain", "home", () => this.#drainRuntimeEvents());
1031
+ if (scope.kind === "full")
1032
+ this.#lifecycleChecks?.notify("periodic");
1020
1033
  if (scope.kind === "full" && this.#hostTerminalRecovery !== undefined) {
1021
1034
  await this.#observeMaintenance("Host terminal recovery", this.#hostTerminalRecovery);
1022
1035
  }
@@ -1258,6 +1271,8 @@ export class FileTaskController {
1258
1271
  ? await processor.drainAsync(this.#now())
1259
1272
  : processor.drain(this.#now());
1260
1273
  this.#lastRuntimeDrain = result;
1274
+ if (result.acknowledgedEventIds.length > 0)
1275
+ this.#lifecycleChecks?.notify("events-persisted");
1261
1276
  this.#runtimeDrainPasses += 1;
1262
1277
  const metrics = result.metrics ?? ZERO_DRAIN_METRICS;
1263
1278
  this.#runtimeListedEvents += metrics.listedEventCount;
@@ -1723,6 +1723,8 @@ export class FileSchedulerStoreAdapter {
1723
1723
  const disposition = runtimeCleanupDisposition(mailbox);
1724
1724
  if (mailbox === null || disposition === null)
1725
1725
  return false;
1726
+ if (disposition === "upgrade-host")
1727
+ return false;
1726
1728
  if (mailbox.processing !== null) {
1727
1729
  if (disposition !== "replace-session" && !mailbox.processing.batch.reasons.every(isRuntimeCleanupReason)) {
1728
1730
  return false;