@zq-silk/yui 2.20.0 → 2.22.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 (142) hide show
  1. package/README.md +5 -0
  2. package/dist/cli/commandCatalog.js +88 -28
  3. package/dist/cli/dynamicCompletion.js +4 -1
  4. package/dist/cli/helpRenderer.js +12 -1
  5. package/dist/cli/interactionPolicy.js +2 -0
  6. package/dist/cli/invocationAuthority.js +2 -6
  7. package/dist/cli/invocationRouter.js +5 -5
  8. package/dist/cli/managedDiagnostics.js +24 -16
  9. package/dist/cli/roleWizard.js +5 -1
  10. package/dist/commands/durableJobCommands.js +2 -3
  11. package/dist/commands/globalRoleCommands.js +33 -8
  12. package/dist/commands/inboxCommands.js +39 -12
  13. package/dist/commands/operatorCommands.js +2 -5
  14. package/dist/commands/releaseCommands.js +16 -2
  15. package/dist/commands/roleConfiguration.js +25 -2
  16. package/dist/commands/sessionCommands.js +5 -2
  17. package/dist/commands/taskCommands.js +225 -99
  18. package/dist/commands/taskContextCommand.js +26 -4
  19. package/dist/commands/taskFactCommands.js +20 -7
  20. package/dist/commands/taskIntegrationCommands.js +3 -3
  21. package/dist/commands/taskRoleRuntimeStatus.js +15 -5
  22. package/dist/commands/taskUsageCommand.js +2 -6
  23. package/dist/commands/textInput.js +2 -2
  24. package/dist/context/contextFamilies.js +10 -0
  25. package/dist/context/currentSkills.js +6 -4
  26. package/dist/context/runContextPack.js +44 -5
  27. package/dist/context/runInputContract.js +1 -1
  28. package/dist/context/sessionBootstrapManifest.js +7 -5
  29. package/dist/context/sessionSkillNotice.js +53 -0
  30. package/dist/context/taskCatalog.js +4 -13
  31. package/dist/context/taskContext.js +135 -103
  32. package/dist/context/taskSearch.js +3 -7
  33. package/dist/controlPlaneCli.js +23 -25
  34. package/dist/controller/agentRuntimeObserver.js +57 -12
  35. package/dist/controller/clientRuntime.js +37 -8
  36. package/dist/controller/controller.js +3 -1
  37. package/dist/controller/diagnosticJournal.js +10 -2
  38. package/dist/controller/fileSchedulerStoreAdapter.js +72 -24
  39. package/dist/controller/jobClient.js +2 -2
  40. package/dist/controller/jobControl.js +2 -2
  41. package/dist/controller/jobDefinitionControl.js +5 -1
  42. package/dist/controller/messageHealth.js +4 -3
  43. package/dist/controller/providerRetryAdmission.js +15 -1
  44. package/dist/controller/providerRetryDelivery.js +18 -2
  45. package/dist/controller/runtime.js +15 -5
  46. package/dist/controller/runtimeEventInbox.js +107 -13
  47. package/dist/controller/runtimeEventProcessor.js +3 -3
  48. package/dist/controller/taskRunRecovery.js +153 -0
  49. package/dist/core/controllerClient.js +16 -2
  50. package/dist/core/controllerServer.js +14 -5
  51. package/dist/core/controllerTelemetry.js +51 -5
  52. package/dist/errors/cliFailure.js +14 -5
  53. package/dist/executor/agentAdapter.js +32 -4
  54. package/dist/executor/agentConfigurationFields.js +2 -0
  55. package/dist/executor/taskAgentCapabilities.js +2 -2
  56. package/dist/integration/gitIntegrationService.js +3 -2
  57. package/dist/integration/integrationSourceApplication.js +12 -1
  58. package/dist/interaction/notificationPresentation.js +11 -16
  59. package/dist/interaction/operatorPresentation.js +1 -1
  60. package/dist/message/messageContinuation.js +3 -1
  61. package/dist/output/boundedRead.js +36 -7
  62. package/dist/output/rolePresentation.js +3 -1
  63. package/dist/runtime/agentHost.js +12 -3
  64. package/dist/runtime/codexAppServerRuntime.js +110 -24
  65. package/dist/runtime/codexInteractiveHost.js +46 -0
  66. package/dist/runtime/providerRetry.js +5 -2
  67. package/dist/runtime/providerRuntimeIdentity.js +1 -1
  68. package/dist/runtime/structuredProviderHost.js +32 -34
  69. package/dist/scheduler/wakeupQueue.js +10 -4
  70. package/dist/storage/sqliteSchema.js +9 -0
  71. package/dist/storage/storageVersions.js +1 -1
  72. package/dist/task/taskAuthority.js +7 -3
  73. package/dist/tmux/tmuxManager.js +4 -2
  74. package/dist/web/webSettings.js +2 -0
  75. package/docs/authorization.md +18 -3
  76. package/docs/cli-information-contract.md +245 -6
  77. package/docs/cli-information-contract.zh-CN.md +107 -5
  78. package/docs/managed-turn-and-session-runtime.md +82 -21
  79. package/docs/managed-turn-and-session-runtime.zh-CN.md +56 -13
  80. package/docs/provider-retry.md +11 -0
  81. package/docs/release-workflow.md +62 -0
  82. package/docs/release-workflow.zh-CN.md +46 -0
  83. package/docs/roles-and-configuration.md +28 -0
  84. package/docs/roles-and-configuration.zh-CN.md +21 -0
  85. package/docs/storage-baseline.md +4 -1
  86. package/docs/storage-baseline.zh-CN.md +1 -1
  87. package/docs/task-delivery.md +7 -0
  88. package/docs/task-delivery.zh-CN.md +5 -0
  89. package/docs/task-discovery.md +5 -5
  90. package/docs/task-discovery.zh-CN.md +4 -4
  91. package/docs/testing/review-evaluation.md +7 -7
  92. package/docs/testing/review-evaluation.zh-CN.md +7 -7
  93. package/docs/testing/verification-levels.md +45 -2
  94. package/docs/testing/verification-levels.zh-CN.md +21 -2
  95. package/i18n/README.zh-CN.md +6 -0
  96. package/package.json +1 -1
  97. package/skills/yui-leader/SKILL.md +98 -189
  98. package/skills/yui-leader/VERSION +1 -1
  99. package/skills/yui-leader/references/artifacts.md +41 -0
  100. package/skills/yui-leader/references/assignment-execution.md +140 -0
  101. package/skills/yui-leader/references/completion.md +79 -0
  102. package/skills/yui-leader/references/delivery-scope.md +26 -0
  103. package/skills/yui-leader/references/execution.md +16 -487
  104. package/skills/yui-leader/references/implementation.md +81 -0
  105. package/skills/yui-leader/references/planning.md +7 -45
  106. package/skills/yui-leader/references/review.md +90 -0
  107. package/skills/yui-leader/references/task-progress.md +23 -0
  108. package/skills/yui-leader/references/work-allocation.md +118 -0
  109. package/skills/yui-operator/SKILL.md +77 -331
  110. package/skills/yui-operator/VERSION +1 -1
  111. package/skills/yui-operator/references/body-transport.md +62 -0
  112. package/skills/yui-operator/references/configuration.md +47 -0
  113. package/skills/yui-operator/references/lifecycle.md +41 -0
  114. package/skills/yui-operator/references/progress.md +39 -0
  115. package/skills/yui-operator/references/routing.md +57 -0
  116. package/skills/yui-operator/references/submission.md +56 -0
  117. package/skills/yui-operator/references/task-delivery.md +5 -2
  118. package/skills/yui-operator/references/web-lifecycle.md +19 -0
  119. package/skills/yui-operator/scripts/verify-submission.mjs +37 -0
  120. package/skills/yui-reviewer/SKILL.md +4 -2
  121. package/skills/yui-reviewer/VERSION +1 -1
  122. package/skills/yui-runtime/SKILL.md +177 -505
  123. package/skills/yui-runtime/VERSION +1 -1
  124. package/skills/yui-runtime/references/archive.md +51 -0
  125. package/skills/yui-runtime/references/command-discovery.md +41 -0
  126. package/skills/yui-runtime/references/completed-followup.md +65 -0
  127. package/skills/yui-runtime/references/context-ordering.md +3 -34
  128. package/skills/yui-runtime/references/context-reads.md +61 -0
  129. package/skills/yui-runtime/references/dispatched-run.md +61 -0
  130. package/skills/yui-runtime/references/execution-failure.md +105 -0
  131. package/skills/yui-runtime/references/global-context.md +18 -0
  132. package/skills/yui-runtime/references/global-input-controls.md +44 -0
  133. package/skills/yui-runtime/references/host-custody.md +30 -0
  134. package/skills/yui-runtime/references/inbox-recovery.md +77 -0
  135. package/skills/yui-runtime/references/incremental-context.md +40 -0
  136. package/skills/yui-runtime/references/publication.md +4 -104
  137. package/skills/yui-runtime/references/recovery.md +32 -192
  138. package/skills/yui-runtime/references/session-identity.md +18 -0
  139. package/skills/yui-runtime/references/session-replacement.md +45 -0
  140. package/skills/yui-runtime/references/uncertain-execution.md +29 -0
  141. package/skills/yui-worker/SKILL.md +7 -4
  142. package/skills/yui-worker/VERSION +1 -1
@@ -9,6 +9,8 @@ import { nonReplayableMessageWake, isLeaderSteer, pinMessageRevisions, releasedN
9
9
  import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
10
10
  import { isTaskRecordRetired, retiredTaskRecordIds } from "../task/taskRecordRetirement.js";
11
11
  import { blocksInboxOrder, inboxEntryCategory, inboxSettled } from "../message/inboxTurn.js";
12
+ import { recoveredMessageContinuation } from "../controller/taskRunRecovery.js";
13
+ import { taskWakeInspectionCommand } from "./taskCommands.js";
12
14
  const target = (address) => ({ kind: "role", taskId: address.taskId, roleName: address.roleName });
13
15
  const source = (address) => `inbox:${address.scope}:${address.taskId ?? ""}:${address.roleName}`;
14
16
  const pendingId = (pending) => `pending-${readDigest(pending)}`;
@@ -38,6 +40,16 @@ function currentTurn(store, address) {
38
40
  return binding?.conversations.find(c => c.epoch === binding.currentConversationEpoch)?.conversationId === address.nativeSessionId
39
41
  ? binding?.run : undefined;
40
42
  }
43
+ function isRecoveredMessage(store, address, entry) {
44
+ if (address.scope !== "task" || address.runId === undefined || isWake(entry))
45
+ return false;
46
+ const run = store.getRun(address.taskId, address.runId);
47
+ return run !== null && recoveredMessageContinuation(store, entry, run);
48
+ }
49
+ function entryCategory(store, address, entry) {
50
+ return isRecoveredMessage(store, address, entry) ? "current"
51
+ : inboxEntryCategory(entry, address, currentTurn(store, address));
52
+ }
41
53
  function ownsMessage(store, address, message) {
42
54
  if (address.roleName === "leader" && message.taskId === address.taskId && isLeaderSteer(message))
43
55
  return true;
@@ -138,7 +150,8 @@ function blocker(store, address, entry) {
138
150
  }
139
151
  else {
140
152
  const message = entry;
141
- if (message.continuation?.runId !== undefined && message.continuation.runId !== address.runId) {
153
+ if (message.continuation?.runId !== undefined && message.continuation.runId !== address.runId
154
+ && !isRecoveredMessage(store, address, message)) {
142
155
  return "continuation-already-dispatched";
143
156
  }
144
157
  if (!accepted && message.inputControl?.expectedSessionId !== undefined
@@ -163,7 +176,7 @@ function checkMutation(store, address) {
163
176
  }
164
177
  function summary(store, address, entry) {
165
178
  return { id: entry.id, kind: isWake(entry) ? "notification-window" : "message",
166
- category: inboxEntryCategory(entry, address, currentTurn(store, address)),
179
+ category: entryCategory(store, address, entry),
167
180
  createdAt: entry.createdAt, summary: summaryText(isWake(entry) ? entry.reasons.join(", ") : entry.body),
168
181
  blocked: blocker(store, address, entry) ?? null, inbox: summaryMetadata(entry.inbox ?? null),
169
182
  handling: inboxHandlingState(entry.inbox),
@@ -178,9 +191,14 @@ function summary(store, address, entry) {
178
191
  read: `yui inbox show ${entry.id}`, ...(isWake(entry)
179
192
  ? { originals: `yui task wake show ${entry.taskId} ${entry.id} --json` } : {}) };
180
193
  }
181
- function receipt(entry) {
194
+ function receipt(entry, requestedId) {
195
+ // Pending-window inspection has no fixed originals yet. An existing wake
196
+ // can already have been read completely through inbox show; offer refresh
197
+ // without implying that claim invalidated those bodies or proved a read.
198
+ const originalRead = isWake(entry)
199
+ ? { [requestedId.startsWith("pending-") ? "originals" : "refreshOriginals"]: `yui task wake show ${entry.taskId} ${entry.id} --json` } : {};
182
200
  return { id: entry.id, inbox: entry.inbox,
183
- ...(isWake(entry) ? { originals: `yui task wake show ${entry.taskId} ${entry.id} --json` } : {}) };
201
+ ...originalRead };
184
202
  }
185
203
  /** Own-address command: there is deliberately no --role, --task or --session.
186
204
  * Reads do not claim. Exact claim precedes semantic handling; exact ack follows
@@ -198,8 +216,7 @@ export function runInboxCommand(args, store, env, now = new Date()) {
198
216
  const selected = address.scope === "global" ? pendingGlobalInboxMessages(store.listGlobalRoleMessages(address.roleName))
199
217
  : entries(store, address).filter(entry => (isWake(entry) || !retired.has(entry.id))
200
218
  && !inboxSettled(entry.inbox));
201
- const turn = currentTurn(store, address);
202
- const categories = selected.map(entry => ({ entry, category: inboxEntryCategory(entry, address, turn) }));
219
+ const categories = selected.map(entry => ({ entry, category: entryCategory(store, address, entry) }));
203
220
  const rank = { current: 0, available: 1, history: 2 };
204
221
  categories.sort((a, b) => rank[a.category] - rank[b.category]);
205
222
  const items = categories.filter(item => view === "all"
@@ -230,7 +247,17 @@ export function runInboxCommand(args, store, env, now = new Date()) {
230
247
  note: "Claim this exact window before processing; claim returns its durable wake and original-read command." }, `${source(address)}/${id}`, parsed.cursor);
231
248
  }
232
249
  const entry = getEntry(store, address, id);
233
- const value = !isWake(entry) && address.scope === "task"
250
+ if (isWake(entry)) {
251
+ // Own-inbox authorization above remains the boundary. Reuse the existing
252
+ // bounded focused originals rather than reconstructing a second view or
253
+ // making a claim mutation depend on a subsequent read succeeding.
254
+ const focused = taskWakeInspectionCommand(["show", entry.taskId, entry.id], store);
255
+ if (focused.kind !== "output")
256
+ throw new Error("Wake inspection did not return a read result.");
257
+ const { wake: _wake, ...originalContext } = focused.data;
258
+ return boundedDocument({ ...entry, originalContext }, `${source(address)}/${id}`, parsed.cursor);
259
+ }
260
+ const value = address.scope === "task"
234
261
  ? expandTaskMessageResult(entry, (task, run) => store.getRun(task, run)) : entry;
235
262
  return boundedDocument(value, `${source(address)}/${id}`, parsed.cursor);
236
263
  }
@@ -262,7 +289,7 @@ export function runInboxCommand(args, store, env, now = new Date()) {
262
289
  if (existing.inboxPendingId !== id) {
263
290
  throw new Error("Inbox request id was reused for a different window.");
264
291
  }
265
- return receipt(existing);
292
+ return receipt(existing, id);
266
293
  }
267
294
  const mailbox = tx.getWorkMailbox(target(current));
268
295
  if (!mailbox?.pending || pendingId(mailbox.pending) !== id)
@@ -310,7 +337,7 @@ export function runInboxCommand(args, store, env, now = new Date()) {
310
337
  if (saved.requestId !== requestId || saved.reason !== reason || !sameSession(saved.actor, current)) {
311
338
  throw new Error("Conflicting retirement; original disposition retained.");
312
339
  }
313
- return receipt(entry);
340
+ return receipt(entry, id);
314
341
  }
315
342
  // A retained attempt in an older conversation is still evidence of
316
343
  // submission; changing the active Session cannot erase that boundary.
@@ -336,7 +363,7 @@ export function runInboxCommand(args, store, env, now = new Date()) {
336
363
  throw new Error("Claim belongs to a previous Session; read the original and prior effects, then explicitly acknowledge only after finishing its handling.");
337
364
  if (entry.inbox.claim.requestId !== requestId)
338
365
  throw new Error("Entry already claimed; retain the original request id.");
339
- return receipt(entry);
366
+ return receipt(entry, id);
340
367
  }
341
368
  const blocked = blocker(tx, current, entry);
342
369
  if (blocked !== undefined)
@@ -398,7 +425,7 @@ export function runInboxCommand(args, store, env, now = new Date()) {
398
425
  if (entry.inbox.ack.requestId !== requestId || entry.inbox.ack.reason !== reason
399
426
  || !sameSession(entry.inbox.ack.actor, current))
400
427
  throw new Error("Conflicting acknowledgement; original disposition retained.");
401
- return receipt(entry);
428
+ return receipt(entry, id);
402
429
  }
403
430
  const inbox = { ...entry.inbox,
404
431
  ack: { requestId, actor: current, at: now.toISOString(), reason: reason } };
@@ -415,7 +442,7 @@ export function runInboxCommand(args, store, env, now = new Date()) {
415
442
  }
416
443
  }
417
444
  save(tx, current, entry);
418
- return receipt(entry);
445
+ return receipt(entry, id);
419
446
  });
420
447
  if (ids.length === 1)
421
448
  return apply(ids[0]);
@@ -136,11 +136,8 @@ function submit(rest, store, options) {
136
136
  if (requestId !== undefined && requestId.length === 0) {
137
137
  throw usageError("--request-id is required.", usage);
138
138
  }
139
- const body = readCommandText(positionals[0], bodyFile, "--body", usage);
140
- return {
141
- kind: "output",
142
- output: submitOperatorMessage(body, taskId, store, options, intent, requestId)
143
- };
139
+ const body = readCommandText(positionals[0], bodyFile, "--body", usage, { preserveFileWhitespace: true });
140
+ return submitOperatorMessage(body, taskId, store, options, intent, requestId);
144
141
  }
145
142
  function listSessions(args, store, options) {
146
143
  if (args.length !== 0)
@@ -5,6 +5,7 @@
5
5
  import { spawn, spawnSync } from "node:child_process";
6
6
  import { chmodSync, cpSync, existsSync, lstatSync, mkdirSync, readdirSync, rmSync, } from "node:fs";
7
7
  import { join, resolve } from "node:path";
8
+ import { pathToFileURL } from "node:url";
8
9
  import { callController, ControllerClientError } from "../core/controllerClient.js";
9
10
  import { processGenerationIsLive } from "../core/fileLockOwner.js";
10
11
  import { activateRelease, ControllerStartUnconfirmedError } from "../release/releaseHandover.js";
@@ -346,10 +347,23 @@ export function createReleaseActivatePorts(overrides = {}) {
346
347
  }
347
348
  }),
348
349
  runPreflight: overrides.runPreflight ?? ((releaseDir, home) => {
349
- const cli = join(releaseDir, "dist", "cli.js");
350
+ assertReleaseIsNotWorktreeOrLinked(releaseDir);
351
+ const manifest = verifyReleaseIntegrity(releaseDir);
352
+ if (resolve(releaseDir) !== resolve(releaseDirectoryFor(home, manifest))
353
+ || !hasMatchingSmokeReceipt(releaseDir, manifest)) {
354
+ throw new Error("Release preflight requires this Home's verified installed target and matching smoke receipt.");
355
+ }
350
356
  // The target owns current-record and database integrity validation.
351
357
  // Activation never migrates; a valid but older Home still blocks here.
352
- const result = spawnSync(process.execPath, [cli, "--json", "upgrade", "--update-preflight"], {
358
+ // Invoke its read-only primitive, not an ordinary inactive CLI command:
359
+ // the Home's CLI fence must remain closed until handover completes.
360
+ const module = pathToFileURL(join(releaseDir, "dist", "storage", "upgrade", "upgradeOrchestrator.js")).href;
361
+ const result = spawnSync(process.execPath, ["--input-type=module", "-e", `
362
+ const { runStorageUpgrade } = await import(process.argv[1]);
363
+ const data = await runStorageUpgrade({ home: process.argv[2], mode: "update-preflight" });
364
+ process.stdout.write(JSON.stringify({ ok: true, data }));
365
+ process.exitCode = data.outcome === "update-preflight" ? 0 : 5;
366
+ `, module, home], {
353
367
  env: { ...process.env, YUI_HOME: home, NO_COLOR: "1" },
354
368
  encoding: "utf8",
355
369
  timeout: 60_000
@@ -24,6 +24,8 @@ const AGENT_VALUE_OPTIONS = [
24
24
  ["--permission-strategy", "value"],
25
25
  ["--sandbox", "value"],
26
26
  ["--approval", "value"],
27
+ ["--exclude-tmpdir-env-var", "value"],
28
+ ["--exclude-slash-tmp", "value"],
27
29
  ["--permission-mode", "value"],
28
30
  ["--search", "value"]
29
31
  ];
@@ -37,6 +39,8 @@ const AGENT_CLEAR_OPTIONS = [
37
39
  ["--clear-allowed-tools", "flag"],
38
40
  ["--clear-disallowed-tools", "flag"],
39
41
  ["--clear-search", "flag"],
42
+ ["--clear-exclude-tmpdir-env-var", "flag"],
43
+ ["--clear-exclude-slash-tmp", "flag"],
40
44
  ["--clear-agent-config", "flag"]
41
45
  ];
42
46
  const AGENT_CONFIG_OPTIONS = new Set([
@@ -196,6 +200,8 @@ function permissionPatch(binding, parsed) {
196
200
  : undefined;
197
201
  const nativeOptions = [
198
202
  "--sandbox", "--approval", "--permission-mode",
203
+ "--exclude-tmpdir-env-var", "--exclude-slash-tmp",
204
+ "--clear-exclude-tmpdir-env-var", "--clear-exclude-slash-tmp",
199
205
  "--allowed-tool", "--clear-allowed-tools",
200
206
  "--disallowed-tool", "--clear-disallowed-tools"
201
207
  ].some((option) => parsed.has(option));
@@ -219,6 +225,19 @@ function permissionPatch(binding, parsed) {
219
225
  permission.sandbox = requiredText(parsed.one("--sandbox"));
220
226
  if (parsed.has("--approval"))
221
227
  permission.approval = requiredText(parsed.one("--approval"));
228
+ for (const [option, key] of [
229
+ ["exclude-tmpdir-env-var", "excludeTmpdirEnvVar"],
230
+ ["exclude-slash-tmp", "excludeSlashTmp"]
231
+ ]) {
232
+ if (parsed.has(`--${option}`)) {
233
+ const value = parsed.one(`--${option}`);
234
+ if (value !== "true" && value !== "false")
235
+ throw usageError(`--${option} requires true or false.`);
236
+ permission[key] = value === "true";
237
+ }
238
+ if (parsed.has(`--clear-${option}`))
239
+ delete permission[key];
240
+ }
222
241
  }
223
242
  else if (binding.adapterId === "acp") {
224
243
  // An ACP Session's configured permission is exactly one mode id, matched
@@ -263,7 +282,9 @@ function assertAgentOptionConflicts(parsed) {
263
282
  ["--effort", "--clear-effort"],
264
283
  ["--allowed-tool", "--clear-allowed-tools"],
265
284
  ["--disallowed-tool", "--clear-disallowed-tools"],
266
- ["--search", "--clear-search"]
285
+ ["--search", "--clear-search"],
286
+ ["--exclude-tmpdir-env-var", "--clear-exclude-tmpdir-env-var"],
287
+ ["--exclude-slash-tmp", "--clear-exclude-slash-tmp"]
267
288
  ]);
268
289
  if (parsed.has("--clear-agent-config")
269
290
  && [...parsed.seen].some((option) => option !== "--clear-agent-config" && AGENT_CONFIG_OPTIONS.has(option))) {
@@ -271,7 +292,9 @@ function assertAgentOptionConflicts(parsed) {
271
292
  }
272
293
  }
273
294
  function assertAdapterOptions(adapterId, parsed) {
274
- const codex = ["--sandbox", "--approval", "--search", "--clear-search"];
295
+ const codex = ["--sandbox", "--approval", "--search", "--clear-search",
296
+ "--exclude-tmpdir-env-var", "--exclude-slash-tmp",
297
+ "--clear-exclude-tmpdir-env-var", "--clear-exclude-slash-tmp"];
275
298
  const tools = [
276
299
  "--allowed-tool", "--clear-allowed-tools",
277
300
  "--disallowed-tool", "--clear-disallowed-tools"
@@ -27,7 +27,10 @@ export async function runSessionStopCommand(input) {
27
27
  // Leader dispatch. The explicit full pass settles pending runtime inbox and
28
28
  // lifecycle work that may exist without a current Session candidate.
29
29
  await input.runtime.drainController();
30
- await input.runtime.stopController();
30
+ const stoppedController = await input.runtime.stopController();
31
+ if (stoppedController.stopped !== true && stoppedController.alreadyStopped !== true) {
32
+ throw new Error("Controller stop was not confirmed; no Session was stopped.");
33
+ }
31
34
  // Re-read the authoritative facts after the drain because a AgentRun that was
32
35
  // already inside a pass may have changed state meanwhile.
33
36
  const settled = input.runtime.snapshot();
@@ -80,7 +83,7 @@ function blockedSessions(input) {
80
83
  });
81
84
  }
82
85
  function blockedStopResult(blocked) {
83
- const details = blocked.map(({ session }) => (`- ${renderSessionOwner(session)} (an active AgentRun or lifecycle operation is still pending)`));
86
+ const details = blocked.map(({ session }) => (`- ${renderSessionOwner(session)} (native execution, an active AgentRun or lifecycle operation is still pending)`));
84
87
  return {
85
88
  output: [
86
89
  `Cannot stop managed Sessions: ${blocked.length} Session(s) are still busy.`,