@gajae-code/coding-agent 0.11.7 → 0.11.9

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 (213) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/dist/types/cli/args.d.ts +0 -2
  3. package/dist/types/cli.d.ts +1 -0
  4. package/dist/types/config/keybindings.d.ts +33 -3
  5. package/dist/types/config/model-profile-activation.d.ts +4 -3
  6. package/dist/types/config/model-profile-contract.d.ts +38 -0
  7. package/dist/types/config/model-profiles.d.ts +2 -3
  8. package/dist/types/config/settings-schema.d.ts +15 -2
  9. package/dist/types/edit/modes/patch.d.ts +13 -0
  10. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  11. package/dist/types/eval/py/executor.d.ts +1 -1
  12. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  13. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
  14. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
  15. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
  16. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
  17. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
  18. package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
  19. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  20. package/dist/types/gjc-runtime/state-writer.d.ts +44 -0
  21. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
  22. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
  23. package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
  24. package/dist/types/main.d.ts +8 -0
  25. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  26. package/dist/types/modes/components/tool-status-header.d.ts +3 -2
  27. package/dist/types/modes/components/welcome.d.ts +4 -0
  28. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  29. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  30. package/dist/types/modes/interactive-mode.d.ts +5 -2
  31. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  32. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  33. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  34. package/dist/types/modes/types.d.ts +3 -0
  35. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  36. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  37. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  38. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  39. package/dist/types/runtime-mcp/redaction.d.ts +2 -0
  40. package/dist/types/sdk/broker/broker.d.ts +2 -0
  41. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
  43. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  44. package/dist/types/sdk/bus/config.d.ts +2 -0
  45. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  46. package/dist/types/sdk/bus/index.d.ts +25 -0
  47. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  48. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  49. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +15 -3
  50. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  51. package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
  52. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  53. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  54. package/dist/types/sdk/host/host.d.ts +1 -1
  55. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  56. package/dist/types/sdk/index.d.ts +2 -0
  57. package/dist/types/sdk/prompt-status.d.ts +88 -0
  58. package/dist/types/sdk/session-directory.d.ts +1 -1
  59. package/dist/types/sdk/startup-capability.d.ts +3 -0
  60. package/dist/types/session/agent-session.d.ts +2 -0
  61. package/dist/types/session/internal/managed-session-scope.d.ts +22 -3
  62. package/dist/types/session/internal/managed-session-storage.d.ts +24 -4
  63. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  64. package/dist/types/session/session-manager.d.ts +5 -1
  65. package/dist/types/session/session-storage.d.ts +31 -0
  66. package/dist/types/skill-state/active-state.d.ts +4 -0
  67. package/dist/types/task/index.d.ts +3 -1
  68. package/dist/types/task/receipt.d.ts +2 -0
  69. package/dist/types/task/types.d.ts +94 -0
  70. package/dist/types/tools/ask.d.ts +454 -174
  71. package/dist/types/tools/image-gen.d.ts +47 -1
  72. package/dist/types/tools/index.d.ts +4 -0
  73. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  74. package/package.json +8 -8
  75. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  76. package/scripts/dogfood-repository-binding.ts +209 -0
  77. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  78. package/scripts/run-test-manifest.ts +4 -4
  79. package/src/cli/args.ts +2 -92
  80. package/src/cli/mcp-cli.ts +4 -21
  81. package/src/cli/notify-cli.ts +5 -38
  82. package/src/cli.ts +50 -0
  83. package/src/commands/deep-interview.ts +9 -4
  84. package/src/commands/sdk.ts +2 -2
  85. package/src/commit/agentic/index.ts +3 -3
  86. package/src/commit/map-reduce/index.ts +2 -2
  87. package/src/config/keybindings.ts +157 -24
  88. package/src/config/model-profile-activation.ts +7 -18
  89. package/src/config/model-profile-contract.ts +161 -0
  90. package/src/config/model-profiles.ts +2 -3
  91. package/src/config/model-registry.ts +11 -1
  92. package/src/config/settings-schema.ts +17 -2
  93. package/src/coordinator-mcp/model-preset.ts +21 -74
  94. package/src/defaults/gjc/skills/deep-interview/SKILL.md +80 -165
  95. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  96. package/src/edit/index.ts +3 -6
  97. package/src/edit/modes/patch.ts +38 -1
  98. package/src/edit/modes/replace.ts +42 -0
  99. package/src/edit/path-mutation-lock.ts +112 -0
  100. package/src/eval/py/executor.ts +30 -11
  101. package/src/eval/py/prelude.py +2 -2
  102. package/src/eval/py/tool-bridge.ts +34 -18
  103. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  104. package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
  105. package/src/gjc-runtime/deep-interview-draft.ts +842 -0
  106. package/src/gjc-runtime/deep-interview-payload.ts +262 -0
  107. package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
  108. package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
  109. package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
  110. package/src/gjc-runtime/deep-interview-state.ts +916 -4
  111. package/src/gjc-runtime/launch-tmux.ts +1 -1
  112. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  113. package/src/gjc-runtime/ralplan-runtime.ts +78 -4
  114. package/src/gjc-runtime/repository-binding.ts +267 -0
  115. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  116. package/src/gjc-runtime/state-writer.ts +166 -2
  117. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  118. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  119. package/src/gjc-runtime/ultragoal-guard.ts +5 -5
  120. package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
  121. package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
  122. package/src/gjc-runtime/workflow-manifest.ts +191 -5
  123. package/src/internal-urls/docs-index.generated.ts +8 -6
  124. package/src/internal-urls/local-protocol.ts +33 -16
  125. package/src/internal-urls/mcp-protocol.ts +73 -11
  126. package/src/main.ts +25 -10
  127. package/src/modes/DESIGN.md +5 -0
  128. package/src/modes/acp/acp-agent.ts +50 -16
  129. package/src/modes/components/model-selector.ts +42 -8
  130. package/src/modes/components/plan-preview-overlay.ts +1 -1
  131. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  132. package/src/modes/components/tool-status-header.ts +20 -4
  133. package/src/modes/components/welcome.ts +32 -20
  134. package/src/modes/controllers/command-controller.ts +97 -54
  135. package/src/modes/controllers/event-controller.ts +10 -0
  136. package/src/modes/controllers/input-controller.ts +7 -2
  137. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  138. package/src/modes/controllers/selector-controller.ts +46 -54
  139. package/src/modes/interactive-mode.ts +70 -36
  140. package/src/modes/prompt-action-autocomplete.ts +32 -13
  141. package/src/modes/prompt-suggestion-controller.ts +96 -0
  142. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  143. package/src/modes/types.ts +3 -0
  144. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  145. package/src/modes/utils/ui-helpers.ts +5 -3
  146. package/src/prompts/agents/architect.md +0 -1
  147. package/src/prompts/agents/critic.md +0 -1
  148. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  149. package/src/prompts/system/system-prompt.md +7 -4
  150. package/src/prompts/tools/cron.md +3 -1
  151. package/src/runtime-mcp/json-rpc.ts +7 -4
  152. package/src/runtime-mcp/manager.ts +51 -53
  153. package/src/runtime-mcp/oauth-flow.ts +0 -72
  154. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  155. package/src/runtime-mcp/redaction.ts +30 -0
  156. package/src/runtime-mcp/transports/http.ts +13 -6
  157. package/src/sdk/broker/broker.ts +34 -20
  158. package/src/sdk/broker/lifecycle.ts +185 -36
  159. package/src/sdk/broker/session-index.ts +11 -1
  160. package/src/sdk/bus/chat-daemon-control.ts +20 -5
  161. package/src/sdk/bus/config-commands.ts +2 -2
  162. package/src/sdk/bus/config.ts +9 -1
  163. package/src/sdk/bus/conversation-store.ts +40 -6
  164. package/src/sdk/bus/index.ts +240 -65
  165. package/src/sdk/bus/lifecycle-commands.ts +47 -3
  166. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  167. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  168. package/src/sdk/bus/notification-orchestration.ts +12 -0
  169. package/src/sdk/bus/notification-service.ts +27 -2
  170. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  171. package/src/sdk/bus/recent-activity.ts +90 -26
  172. package/src/sdk/bus/telegram-daemon-contract.ts +15 -3
  173. package/src/sdk/bus/telegram-daemon-control.ts +145 -33
  174. package/src/sdk/bus/telegram-daemon.ts +853 -112
  175. package/src/sdk/bus/telegram-reference.ts +20 -2
  176. package/src/sdk/cli/session-cli.ts +15 -2
  177. package/src/sdk/host/control/dispatch.ts +1 -1
  178. package/src/sdk/host/control/operations.ts +1 -1
  179. package/src/sdk/host/host.ts +2 -6
  180. package/src/sdk/host/query/handlers.ts +70 -5
  181. package/src/sdk/index.ts +12 -0
  182. package/src/sdk/prompt-status.ts +97 -0
  183. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  184. package/src/sdk/protocol/operation-registry.ts +18 -4
  185. package/src/sdk/session-directory.ts +5 -1
  186. package/src/sdk/session.ts +2 -0
  187. package/src/sdk/startup-capability.ts +9 -1
  188. package/src/session/agent-session.ts +138 -5
  189. package/src/session/blob-store.ts +7 -2
  190. package/src/session/internal/managed-session-scope.ts +998 -133
  191. package/src/session/internal/managed-session-storage.ts +268 -203
  192. package/src/session/internal/native-publish-outcome.ts +266 -0
  193. package/src/session/session-manager.ts +406 -360
  194. package/src/session/session-storage.ts +151 -11
  195. package/src/skill-state/active-state.ts +10 -0
  196. package/src/task/executor.ts +13 -5
  197. package/src/task/gjc-command.ts +2 -2
  198. package/src/task/index.ts +121 -15
  199. package/src/task/receipt.ts +3 -0
  200. package/src/task/types.ts +34 -3
  201. package/src/tools/ask.ts +199 -88
  202. package/src/tools/ast-edit.ts +2 -2
  203. package/src/tools/bash-pty-selection.ts +2 -2
  204. package/src/tools/browser/screenshot-format.ts +6 -1
  205. package/src/tools/cron.ts +1 -1
  206. package/src/tools/image-gen.ts +178 -6
  207. package/src/tools/index.ts +4 -0
  208. package/src/tools/render-utils.ts +7 -1
  209. package/src/utils/edit-mode.ts +2 -2
  210. package/src/utils/prompt-suggestion.ts +265 -0
  211. package/src/utils/sixel.ts +3 -3
  212. package/src/web/insane/url-guard.ts +3 -1
  213. package/src/web/scrapers/docs-rs.ts +30 -5
@@ -25,9 +25,11 @@ import type {
25
25
  import { OWNERSHIP_MISMATCH_MESSAGE, ownershipMismatchRecovery } from "../../daemon/operator-contract";
26
26
  import { resolveGjcRuntimeSpawnInfo } from "../../daemon/runtime";
27
27
  import { isProcessIncarnation } from "../broker/process-incarnation";
28
+
28
29
  import { getNotificationConfig, isTelegramConfigured, tokenFingerprint } from "./config";
29
30
  import { exactUnlinkNotificationFile, readNotificationEndpointFile } from "./notification-service";
30
31
  import {
32
+ type AttestedLegacyDaemonOwner,
31
33
  confirmTelegramDaemonSpawn,
32
34
  type DaemonState,
33
35
  daemonPaths,
@@ -35,6 +37,7 @@ import {
35
37
  isCurrentCompatibleOwner,
36
38
  isFreshLiveOwner,
37
39
  isSignalableMatchingOwner,
40
+ readAttestedLegacyDaemonOwner,
38
41
  readDaemonRoots,
39
42
  readDaemonState,
40
43
  spawnTelegramDaemonOwner,
@@ -109,16 +112,19 @@ export async function clearTelegramControlRequest(
109
112
 
110
113
  export interface DaemonProcessReference {
111
114
  incarnation: string;
115
+ /** Whether the platform delivers the requested signal cooperatively or as a hard termination. */
116
+ termination: "cooperative" | "hard";
112
117
  signalRoot(signal: NodeJS.Signals): void;
113
118
  }
114
119
 
115
- function defaultProcessReference(pid: number, platform = os.platform()): DaemonProcessReference | undefined {
120
+ export function defaultProcessReference(pid: number, platform = os.platform()): DaemonProcessReference | undefined {
116
121
  try {
117
122
  const processRef = Process.fromPid(pid);
118
123
  if (!processRef || !isProcessIncarnation(processRef.incarnation)) return undefined;
119
124
  const incarnation = processRef.incarnation;
120
125
  return {
121
126
  incarnation,
127
+ termination: platform === "win32" ? "hard" : "cooperative",
122
128
  signalRoot: signal => {
123
129
  const nativeSignal = os.constants.signals[signal];
124
130
  if (nativeSignal === undefined) throw new Error(`Unsupported signal: ${signal}`);
@@ -190,7 +196,6 @@ export class TelegramDaemonController implements BuiltInDaemonController {
190
196
  private readonly now: () => number;
191
197
  private readonly processReference: (pid: number) => DaemonProcessReference | undefined;
192
198
  private readonly waitStepMs: number;
193
-
194
199
  constructor(
195
200
  private readonly settings: Settings,
196
201
  private readonly deps: TelegramDaemonControlDeps = {},
@@ -315,31 +320,28 @@ export class TelegramDaemonController implements BuiltInDaemonController {
315
320
  tokenFingerprint: string,
316
321
  chatId: string,
317
322
  signal: NodeJS.Signals,
318
- ): Promise<"signaled" | "already_gone" | "ownership_changed"> {
323
+ ): Promise<"signaled" | "already_gone" | "ownership_changed" | "hard_termination"> {
319
324
  const current = await readDaemonState(this.settings, this.fsImpl);
320
- // The signal is a privileged action: immediately before sending it, prove the
321
- // complete state record is still the exact captured owner. A malformed record,
322
- // configuration mutation, or successor using a reused PID is never signalable.
323
- if (
324
- !hasSafeDaemonStateShape(current) ||
325
- current.ownerId !== captured.ownerId ||
326
- current.acquisitionId !== captured.acquisitionId ||
327
- current.pid !== captured.pid ||
328
- current.generation !== captured.generation ||
329
- current.incarnation !== captured.incarnation ||
330
- current.tokenFingerprint !== captured.tokenFingerprint ||
331
- current.chatId !== captured.chatId ||
332
- current.tokenFingerprint !== tokenFingerprint ||
333
- current.chatId !== chatId
334
- )
335
- return "ownership_changed";
336
- // A matching captured owner that exited between the request and recheck has
337
- // completed the handoff. A live owner with changed or unavailable provenance
338
- // remains ambiguous and must not be treated as stopped.
325
+
326
+ const modernCurrentMatches =
327
+ hasSafeDaemonStateShape(current) &&
328
+ current.ownerId === captured.ownerId &&
329
+ current.acquisitionId === captured.acquisitionId &&
330
+ current.pid === captured.pid &&
331
+ current.generation === captured.generation &&
332
+ current.incarnation === captured.incarnation &&
333
+ current.tokenFingerprint === captured.tokenFingerprint &&
334
+ current.chatId === captured.chatId &&
335
+ current.tokenFingerprint === tokenFingerprint &&
336
+ current.chatId === chatId;
337
+ // The signal is privileged: the persisted record must still be the exact
338
+ // modern owner. Legacy generation-3 records remain migration evidence only;
339
+ // this controller never upgrades or signals them.
340
+ if (!modernCurrentMatches) return "ownership_changed";
339
341
  if (!this.pidAlive(captured.pid)) return "already_gone";
340
342
  if (
341
343
  !isSignalableMatchingOwner({
342
- state: current,
344
+ state: modernCurrentMatches ? current : captured,
343
345
  tokenFingerprint,
344
346
  chatId,
345
347
  pidAlive: this.pidAlive,
@@ -352,6 +354,69 @@ export class TelegramDaemonController implements BuiltInDaemonController {
352
354
  // the exit-and-reuse window between ordinary provenance checks and signaling.
353
355
  // Its identity must still be the exact persisted incarnation before use.
354
356
  if (!processRef || processRef.incarnation !== captured.incarnation) return "ownership_changed";
357
+ if (signal === "SIGTERM" && processRef.termination === "hard") return "hard_termination";
358
+ try {
359
+ processRef.signalRoot(signal);
360
+ } catch {
361
+ return "already_gone";
362
+ }
363
+ return "signaled";
364
+ }
365
+
366
+ #currentConfigurationMatches(tokenFingerprintValue: string, chatId: string): boolean {
367
+ const current = getNotificationConfig(this.settings);
368
+ return (
369
+ isTelegramConfigured(current) &&
370
+ tokenFingerprint(current.botToken) === tokenFingerprintValue &&
371
+ current.chatId === chatId
372
+ );
373
+ }
374
+
375
+ async #signalAttestedLegacyOwner(
376
+ captured: AttestedLegacyDaemonOwner,
377
+ tokenFingerprint: string,
378
+ chatId: string,
379
+ signal: NodeJS.Signals,
380
+ ): Promise<"signaled" | "already_gone" | "ownership_changed" | "hard_termination"> {
381
+ if (!this.#currentConfigurationMatches(tokenFingerprint, chatId)) return "ownership_changed";
382
+ const current = await readAttestedLegacyDaemonOwner({
383
+ settings: this.settings,
384
+ fs: this.fsImpl,
385
+ now: this.now,
386
+ pidIncarnation: this.deps.pidIncarnation,
387
+ tokenFingerprint,
388
+ chatId,
389
+ });
390
+ if (
391
+ !current ||
392
+ current.state.pid !== captured.state.pid ||
393
+ current.state.ownerId !== captured.state.ownerId ||
394
+ current.state.startedAt !== captured.state.startedAt ||
395
+ current.incarnation !== captured.incarnation
396
+ )
397
+ return "ownership_changed";
398
+ if (!this.pidAlive(captured.state.pid)) return "already_gone";
399
+ const processRef = this.processReference(captured.state.pid);
400
+ if (!processRef || processRef.incarnation !== captured.incarnation) return "ownership_changed";
401
+ const rechecked = await readAttestedLegacyDaemonOwner({
402
+ settings: this.settings,
403
+ fs: this.fsImpl,
404
+ now: this.now,
405
+ pidIncarnation: this.deps.pidIncarnation,
406
+ tokenFingerprint,
407
+ chatId,
408
+ });
409
+ if (
410
+ !rechecked ||
411
+ rechecked.state.pid !== current.state.pid ||
412
+ rechecked.state.ownerId !== current.state.ownerId ||
413
+ rechecked.state.startedAt !== current.state.startedAt ||
414
+ rechecked.incarnation !== current.incarnation ||
415
+ !this.#currentConfigurationMatches(tokenFingerprint, chatId) ||
416
+ !this.pidAlive(captured.state.pid)
417
+ )
418
+ return "ownership_changed";
419
+ if (signal === "SIGTERM" && processRef.termination === "hard") return "hard_termination";
355
420
  try {
356
421
  processRef.signalRoot(signal);
357
422
  } catch {
@@ -376,7 +441,10 @@ export class TelegramDaemonController implements BuiltInDaemonController {
376
441
  return this.stopOrReload("reload", opts);
377
442
  }
378
443
 
379
- async reloadForGenerationUpgrade(opts: DaemonOperationOptions = {}): Promise<TelegramGenerationReloadResult> {
444
+ async reloadForGenerationUpgrade(
445
+ opts: DaemonOperationOptions = {},
446
+ attestedLegacyUpgrade = false,
447
+ ): Promise<TelegramGenerationReloadResult> {
380
448
  // A generation upgrade MUST replace an incompatible older-generation owner to
381
449
  // avoid a permanent single-poller deadlock. Unlike a manual `gjc daemon
382
450
  // reload`, this automatic path force-escalates to SIGKILL when the old owner
@@ -384,7 +452,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
384
452
  // self-recovers instead of failing closed and asking the operator to rerun
385
453
  // with --force. The SIGKILL remains fenced to the still-live, still-matching
386
454
  // captured owner (same ownerId + pid), so a fresh replacement is never killed.
387
- const operation = await this.stopOrReload("reload", { ...opts, force: true });
455
+ const operation = await this.stopOrReload("reload", { ...opts, force: true }, attestedLegacyUpgrade);
388
456
  if (!operation.ok) return { outcome: "failed", operation };
389
457
  const after = await this.status();
390
458
  return after.health === "running" ? { outcome: "ready", operation } : { outcome: "failed", operation };
@@ -394,7 +462,11 @@ export class TelegramDaemonController implements BuiltInDaemonController {
394
462
  return this.stopOrReload("stop", opts);
395
463
  }
396
464
 
397
- private async stopOrReload(action: "stop" | "reload", opts: DaemonOperationOptions): Promise<DaemonOperationResult> {
465
+ private async stopOrReload(
466
+ action: "stop" | "reload",
467
+ opts: DaemonOperationOptions,
468
+ attestedLegacyUpgrade = false,
469
+ ): Promise<DaemonOperationResult> {
398
470
  const before = await this.status();
399
471
  const warnings: string[] = [];
400
472
  if (before.runtime.warning) warnings.push(before.runtime.warning);
@@ -409,6 +481,16 @@ export class TelegramDaemonController implements BuiltInDaemonController {
409
481
  const killTimeoutMs = opts.killTimeoutMs ?? DEFAULT_KILL_TIMEOUT_MS;
410
482
 
411
483
  const state = await readDaemonState(this.settings, this.fsImpl);
484
+ const attestedLegacyOwner = attestedLegacyUpgrade
485
+ ? await readAttestedLegacyDaemonOwner({
486
+ settings: this.settings,
487
+ fs: this.fsImpl,
488
+ now: this.now,
489
+ pidIncarnation: this.deps.pidIncarnation,
490
+ tokenFingerprint: fp,
491
+ chatId,
492
+ })
493
+ : undefined;
412
494
  const replaceableLiveOwner =
413
495
  (action === "reload" &&
414
496
  state !== undefined &&
@@ -427,6 +509,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
427
509
  pidAlive: this.pidAlive,
428
510
  pidIncarnation: this.deps.pidIncarnation,
429
511
  })) ||
512
+ (action === "reload" && attestedLegacyOwner !== undefined && this.pidAlive(attestedLegacyOwner.state.pid)) ||
430
513
  // A physically-live matching owner whose heartbeat is stale (hung) may be
431
514
  // past-TTL yet still holding the poller. Autostart/generation-upgrade reloads
432
515
  // stay conservative and refuse it, but an explicit `reload --force` must be
@@ -483,16 +566,18 @@ export class TelegramDaemonController implements BuiltInDaemonController {
483
566
  }
484
567
 
485
568
  // Running owner: capture identity, request cooperative stop, signal, wait.
486
- if (
487
- !hasSafeDaemonStateShape(state) ||
488
- !isSignalableMatchingOwner({
569
+ const modernCapturedOwner =
570
+ hasSafeDaemonStateShape(state) &&
571
+ isSignalableMatchingOwner({
489
572
  state,
490
573
  tokenFingerprint: fp,
491
574
  chatId,
492
575
  pidAlive: this.pidAlive,
493
576
  pidIncarnation: this.deps.pidIncarnation,
494
577
  })
495
- ) {
578
+ ? state
579
+ : undefined;
580
+ if (!modernCapturedOwner && !attestedLegacyOwner) {
496
581
  return this.result(
497
582
  action,
498
583
  false,
@@ -502,7 +587,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
502
587
  warnings,
503
588
  );
504
589
  }
505
- const capturedOwner = state;
590
+ const capturedOwner = modernCapturedOwner ?? attestedLegacyOwner!.state;
506
591
  const oldOwnerId = capturedOwner.ownerId;
507
592
  const oldPid = capturedOwner.pid;
508
593
  const requestId = this.deps.randomId?.() ?? `${this.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
@@ -511,7 +596,21 @@ export class TelegramDaemonController implements BuiltInDaemonController {
511
596
  { version: 1, requestId, action, ownerId: oldOwnerId, pid: oldPid, createdAt: this.now() },
512
597
  this.fsImpl,
513
598
  );
514
- if ((await this.signalCapturedOwner(capturedOwner, fp, chatId, "SIGTERM")) === "ownership_changed") {
599
+ const signalResult = attestedLegacyOwner
600
+ ? await this.#signalAttestedLegacyOwner(attestedLegacyOwner, fp, chatId, "SIGTERM")
601
+ : await this.signalCapturedOwner(modernCapturedOwner!, fp, chatId, "SIGTERM");
602
+ if (signalResult === "hard_termination") {
603
+ await this.clearOwnRequest(requestId);
604
+ return this.result(
605
+ action,
606
+ false,
607
+ "telegram daemon has hard process authority; refusing cooperative SIGTERM",
608
+ before,
609
+ await this.status(),
610
+ warnings,
611
+ );
612
+ }
613
+ if (signalResult === "ownership_changed") {
515
614
  await this.clearOwnRequest(requestId);
516
615
  return this.result(
517
616
  action,
@@ -559,7 +658,9 @@ export class TelegramDaemonController implements BuiltInDaemonController {
559
658
  // the captured owner/pid still matches, so we never kill a different owner.
560
659
  const stillSameOwner = current !== undefined && current.ownerId === oldOwnerId && current.pid === oldPid;
561
660
  if (opts.force && stillSameOwner) {
562
- const killResult = await this.signalCapturedOwner(capturedOwner, fp, chatId, "SIGKILL");
661
+ const killResult = attestedLegacyOwner
662
+ ? await this.#signalAttestedLegacyOwner(attestedLegacyOwner, fp, chatId, "SIGKILL")
663
+ : await this.signalCapturedOwner(modernCapturedOwner!, fp, chatId, "SIGKILL");
563
664
  // Kill only a still-live matching owner; an owner that exited between the
564
665
  // graceful timeout and this recheck ("already_gone") is confirmed dead by
565
666
  // waitForPidDeath, while a real ownership change stays fenced.
@@ -585,6 +686,17 @@ export class TelegramDaemonController implements BuiltInDaemonController {
585
686
  return this.result(action, true, "stopped telegram daemon", before, after, warnings);
586
687
  }
587
688
 
689
+ if (!this.#currentConfigurationMatches(fp, chatId)) {
690
+ const after = await this.status();
691
+ return this.result(
692
+ action,
693
+ false,
694
+ "telegram notification configuration changed during reload; refusing to spawn",
695
+ before,
696
+ after,
697
+ warnings,
698
+ );
699
+ }
588
700
  const { spawned, ready } = await this.spawnAndWait(roots, fp, chatId);
589
701
  warnings.push(...spawned.warnings);
590
702
  const after = await this.status();