@gajae-code/coding-agent 0.12.5 → 0.12.6

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 (127) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +4 -1
  3. package/dist/types/eval/backend.d.ts +2 -2
  4. package/dist/types/exec/bash-executor.d.ts +10 -8
  5. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  6. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  7. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  8. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  9. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  10. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  11. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  12. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  13. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  14. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  15. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  16. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
  17. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
  18. package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
  19. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  20. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  21. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  22. package/dist/types/sdk/broker/broker.d.ts +18 -0
  23. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  24. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  25. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -3
  26. package/dist/types/sdk/bus/index.d.ts +4 -0
  27. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -2
  28. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  29. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  30. package/dist/types/sdk/host/host.d.ts +23 -2
  31. package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
  32. package/dist/types/session/agent-session.d.ts +5 -4
  33. package/dist/types/session/artifacts.d.ts +8 -5
  34. package/dist/types/session/attempt-record-store.d.ts +19 -0
  35. package/dist/types/session/client-bridge.d.ts +14 -0
  36. package/dist/types/session/internal/managed-session-storage.d.ts +5 -2
  37. package/dist/types/session/messages.d.ts +4 -1
  38. package/dist/types/session/session-manager.d.ts +6 -4
  39. package/dist/types/session/session-storage.d.ts +8 -0
  40. package/dist/types/session/streaming-output.d.ts +32 -0
  41. package/dist/types/task/receipt.d.ts +2 -0
  42. package/dist/types/task/types.d.ts +178 -0
  43. package/dist/types/tools/bash.d.ts +1 -1
  44. package/dist/types/tools/fetch.d.ts +3 -0
  45. package/dist/types/tools/output-meta.d.ts +37 -5
  46. package/dist/types/tools/tool-result.d.ts +1 -0
  47. package/dist/types/utils/artifact-id.d.ts +2 -0
  48. package/package.json +8 -8
  49. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  50. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  51. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  52. package/src/config/model-profiles.ts +8 -0
  53. package/src/config/settings-schema.ts +17 -3
  54. package/src/cursor.ts +20 -13
  55. package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
  56. package/src/eval/backend.ts +2 -2
  57. package/src/exec/bash-executor.ts +425 -151
  58. package/src/extensibility/extensions/runner.ts +80 -6
  59. package/src/extensibility/extensions/wrapper.ts +22 -15
  60. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  61. package/src/gjc-runtime/launch-tmux.ts +309 -293
  62. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  63. package/src/gjc-runtime/psmux-detect.ts +52 -6
  64. package/src/gjc-runtime/team-launch.ts +50 -2
  65. package/src/gjc-runtime/team-runtime.ts +716 -251
  66. package/src/gjc-runtime/team-store.ts +25 -7
  67. package/src/gjc-runtime/team-workers.ts +161 -1
  68. package/src/gjc-runtime/tmux-common.ts +16 -4
  69. package/src/gjc-runtime/tmux-gc.ts +37 -5
  70. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  71. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  72. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  73. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  74. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  75. package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
  76. package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
  77. package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
  78. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  79. package/src/internal-urls/agent-protocol.ts +98 -38
  80. package/src/internal-urls/artifact-protocol.ts +28 -17
  81. package/src/internal-urls/docs-index.generated.ts +5 -5
  82. package/src/internal-urls/local-protocol.ts +39 -18
  83. package/src/internal-urls/registry-helpers.ts +62 -0
  84. package/src/modes/acp/acp-agent.ts +51 -13
  85. package/src/modes/acp/acp-client-bridge.ts +154 -11
  86. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  87. package/src/modes/components/plugin-settings.ts +5 -0
  88. package/src/modes/components/settings-selector.ts +4 -0
  89. package/src/modes/controllers/selector-controller.ts +1 -0
  90. package/src/prompts/tools/bash.md +2 -2
  91. package/src/prompts/tools/monitor.md +4 -4
  92. package/src/sdk/acp/adapter.ts +200 -9
  93. package/src/sdk/broker/broker.ts +93 -10
  94. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  95. package/src/sdk/broker/lifecycle.ts +691 -92
  96. package/src/sdk/bus/chat-daemon-control.ts +32 -6
  97. package/src/sdk/bus/index.ts +347 -14
  98. package/src/sdk/bus/telegram-daemon-contract.ts +4 -2
  99. package/src/sdk/bus/telegram-daemon.ts +283 -44
  100. package/src/sdk/bus/topic-registry.ts +133 -10
  101. package/src/sdk/host/host.ts +187 -9
  102. package/src/sdk/host/reverse-leases.ts +249 -42
  103. package/src/sdk/session.ts +7 -6
  104. package/src/session/agent-session.ts +930 -354
  105. package/src/session/artifacts.ts +211 -74
  106. package/src/session/attempt-record-store.ts +106 -0
  107. package/src/session/client-bridge.ts +15 -0
  108. package/src/session/internal/managed-session-scope.ts +252 -67
  109. package/src/session/internal/managed-session-storage.ts +105 -5
  110. package/src/session/messages.ts +32 -7
  111. package/src/session/session-manager.ts +19 -21
  112. package/src/session/session-storage.ts +146 -24
  113. package/src/session/streaming-output.ts +198 -40
  114. package/src/task/index.ts +45 -0
  115. package/src/task/provider-retry-status.ts +4 -1
  116. package/src/task/receipt.ts +4 -0
  117. package/src/task/render.ts +30 -0
  118. package/src/task/types.ts +34 -0
  119. package/src/tools/bash.ts +818 -239
  120. package/src/tools/eval.ts +99 -4
  121. package/src/tools/fetch.ts +77 -15
  122. package/src/tools/find.ts +7 -3
  123. package/src/tools/monitor.ts +7 -1
  124. package/src/tools/output-meta.ts +706 -116
  125. package/src/tools/search.ts +9 -4
  126. package/src/tools/tool-result.ts +1 -0
  127. package/src/utils/artifact-id.ts +6 -0
@@ -39,13 +39,14 @@ export type ChatDaemonAction = "stop" | "reload";
39
39
  * applies the current notification configuration directly when starting chat
40
40
  * daemon transports. Generation 16 applies Telegram sound-policy configuration
41
41
  * through shared notification parsing. Generation 17 bound managed-session
42
- * replacement to exact native filesystem authority; generation 18 retires that
43
- * binding (revert of #3489, which stalled POSIX artifact cleanup) while keeping
44
- * the lifecycle contract moving strictly forward.
42
+ * replacement to exact native filesystem authority; generation 18 retired that
43
+ * binding, generation 19 bound exact cleanup to parent/link-count authority,
44
+ * generation 20 refreshed retained cleanup semantics, and generation 21 hardens
45
+ * exact Bash process-tree ownership shared by chat daemon cleanup.
45
46
  */
46
47
  export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
47
- discord: 18,
48
- slack: 18,
48
+ discord: 21,
49
+ slack: 21,
49
50
  };
50
51
 
51
52
  export function chatDaemonGeneration(kind: ChatDaemonKind): number {
@@ -245,6 +246,9 @@ interface ChatDaemonOwnerLockLease {
245
246
  ino: bigint;
246
247
  size: bigint;
247
248
  mtimeNs: bigint;
249
+ nlink: bigint;
250
+ parentDev: bigint;
251
+ parentIno: bigint;
248
252
  sha256: string;
249
253
  }
250
254
 
@@ -892,6 +896,7 @@ async function createChatDaemonOwnerLock(
892
896
  if (isAlreadyExists(error)) return undefined;
893
897
  throw error;
894
898
  }
899
+ await fs.promises.unlink(temporary);
895
900
  return await captureChatDaemonOwnerLockLease(lock);
896
901
  } finally {
897
902
  await fs.promises.unlink(temporary).catch(() => undefined);
@@ -903,14 +908,22 @@ async function captureChatDaemonOwnerLockLease(lock: string): Promise<ChatDaemon
903
908
  const handle = await fs.promises.open(lock, "r");
904
909
  try {
905
910
  const before = await handle.stat({ bigint: true });
911
+ const parentBefore = await fs.promises.lstat(path.dirname(lock), { bigint: true });
906
912
  if (!before.isFile()) return undefined;
907
913
  const content = await handle.readFile({ encoding: "utf8" });
908
914
  const after = await handle.stat({ bigint: true });
909
915
  const pathname = await fs.promises.lstat(lock, { bigint: true });
916
+ const parentAfter = await fs.promises.lstat(path.dirname(lock), { bigint: true });
910
917
  if (
911
918
  !after.isFile() ||
912
919
  !pathname.isFile() ||
913
920
  pathname.isSymbolicLink() ||
921
+ before.nlink !== 1n ||
922
+ pathname.nlink !== 1n ||
923
+ !parentBefore.isDirectory() ||
924
+ parentBefore.isSymbolicLink() ||
925
+ parentBefore.dev !== parentAfter.dev ||
926
+ parentBefore.ino !== parentAfter.ino ||
914
927
  before.dev !== after.dev ||
915
928
  before.ino !== after.ino ||
916
929
  before.size !== after.size ||
@@ -927,6 +940,9 @@ async function captureChatDaemonOwnerLockLease(lock: string): Promise<ChatDaemon
927
940
  ino: before.ino,
928
941
  size: before.size,
929
942
  mtimeNs: before.mtimeNs,
943
+ nlink: before.nlink,
944
+ parentDev: parentBefore.dev,
945
+ parentIno: parentBefore.ino,
930
946
  sha256: crypto.createHash("sha256").update(content).digest("hex"),
931
947
  };
932
948
  } finally {
@@ -944,6 +960,9 @@ async function ownsChatDaemonOwnerLock(lock: string, lease: ChatDaemonOwnerLockL
944
960
  current.ino === lease.ino &&
945
961
  current.size === lease.size &&
946
962
  current.mtimeNs === lease.mtimeNs &&
963
+ current.nlink === lease.nlink &&
964
+ current.parentDev === lease.parentDev &&
965
+ current.parentIno === lease.parentIno &&
947
966
  current.content === lease.content
948
967
  );
949
968
  }
@@ -952,7 +971,14 @@ async function ownsChatDaemonOwnerLock(lock: string, lease: ChatDaemonOwnerLockL
952
971
  function unlinkExactChatDaemonOwnerLock(lock: string, lease: ChatDaemonOwnerLockLease): boolean {
953
972
  try {
954
973
  const removed = native.exactUnlink(lock, {
955
- ...lease,
974
+ dev: lease.dev,
975
+ ino: lease.ino,
976
+ size: lease.size,
977
+ mtimeNs: lease.mtimeNs,
978
+ nlink: lease.nlink,
979
+ parentDev: lease.parentDev,
980
+ parentIno: lease.parentIno,
981
+ sha256: lease.sha256,
956
982
  quarantineName: `.gjc-delete-chat-daemon-lock-${crypto.randomUUID()}`,
957
983
  });
958
984
  if (removed.ok) return true;
@@ -27,7 +27,7 @@ import * as fs from "node:fs";
27
27
  import * as os from "node:os";
28
28
  import * as path from "node:path";
29
29
  import { promisify } from "node:util";
30
- import { ThinkingLevel } from "@gajae-code/agent-core";
30
+ import { type RunSettlementProof, ThinkingLevel } from "@gajae-code/agent-core";
31
31
  import type { ImageContent, TextContent, Tool } from "@gajae-code/ai";
32
32
  import { NotificationServer, nativeBuildInfo } from "@gajae-code/natives";
33
33
  import { $credentialEnv, logger, postmortem, VERSION } from "@gajae-code/utils";
@@ -61,7 +61,7 @@ import { registerAskAnswerSource, registerWorkflowGateEmitterListener } from "..
61
61
  import { acpFinalTextFromMessage } from "../acp/final-text";
62
62
  import { ensureBroker } from "../broker/ensure";
63
63
  import { SessionIndex } from "../broker/session-index";
64
- import { SessionSdkHost, shouldHostSdk } from "../host";
64
+ import { ReverseLeaseError, SessionSdkHost, shouldHostSdk } from "../host";
65
65
  import { type ControlSurface, dispatchControl } from "../host/control";
66
66
  import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "../host/query";
67
67
  import { projectQ10Models } from "../models.js";
@@ -116,6 +116,25 @@ export {
116
116
  runIdentityControlTerminalPath,
117
117
  } from "./control-drain-lease";
118
118
 
119
+ const PROMPT_SETTLEMENT_DIAGNOSTIC_ENTRY_LIMIT = 8;
120
+ const PROMPT_SETTLEMENT_DIAGNOSTIC_MAX_AGE_MS = 86_400_000;
121
+
122
+ export function formatPromptSettlementDiagnostic(
123
+ proof: Extract<RunSettlementProof, { status: "unfenced" }>,
124
+ now = Date.now(),
125
+ ): string {
126
+ const pending = proof.pending.slice(0, PROMPT_SETTLEMENT_DIAGNOSTIC_ENTRY_LIMIT).map(entry => ({
127
+ kind: entry.kind,
128
+ labelHash: crypto.createHash("sha256").update(entry.label).digest("hex").slice(0, 16),
129
+ ageMs: Math.max(0, Math.min(PROMPT_SETTLEMENT_DIAGNOSTIC_MAX_AGE_MS, now - entry.registeredAt)),
130
+ }));
131
+ return JSON.stringify({
132
+ reason: proof.reason,
133
+ pending,
134
+ omitted: Math.max(0, proof.pending.length - pending.length),
135
+ });
136
+ }
137
+
119
138
  // ===========================================================================
120
139
  // Session lifecycle control protocol (TypeScript mirror of the Rust wire
121
140
  // contract in `crates/gjc-sdk/src/lifecycle.rs`).
@@ -3239,6 +3258,8 @@ export function createNotificationsExtension(
3239
3258
  awaitStartup: boolean;
3240
3259
  }
3241
3260
  | undefined;
3261
+ const MAX_SYNTHETIC_SEAM_ADMISSIONS = 64;
3262
+ let syntheticSeamAdmissions = 0;
3242
3263
  let extensionShuttingDown = false;
3243
3264
 
3244
3265
  async function ensureTelegramOwner(
@@ -3548,8 +3569,64 @@ export function createNotificationsExtension(
3548
3569
  const revisions = new RevisionStore(id, Date.now, { storageDir: stateRoot });
3549
3570
  let host: SessionSdkHost | undefined;
3550
3571
  let disposeUiAnswerSource: (() => void) | undefined;
3551
- const installProviderDefinitions = (capability: string, definitions: unknown) => {
3572
+ let fsClientBridge: ClientBridge | undefined;
3573
+ let terminalClientBridge: ClientBridge | undefined;
3574
+ let terminalPublicationFailedLeaseId: string | undefined;
3575
+ const MAX_PENDING_TERMINAL_CREATIONS = 32;
3576
+ let pendingTerminalCreations = 0;
3577
+ let terminalCreationTail = Promise.resolve();
3578
+ const withTerminalCreationAdmission = async <T>(operation: () => Promise<T>): Promise<T> => {
3579
+ if (pendingTerminalCreations >= MAX_PENDING_TERMINAL_CREATIONS)
3580
+ throw new Error("terminal creation admission limit reached");
3581
+ pendingTerminalCreations++;
3582
+ const previous = terminalCreationTail;
3583
+ const release = Promise.withResolvers<void>();
3584
+ terminalCreationTail = previous.then(() => release.promise);
3585
+ try {
3586
+ await previous;
3587
+ return await operation();
3588
+ } finally {
3589
+ pendingTerminalCreations--;
3590
+ release.resolve();
3591
+ }
3592
+ };
3593
+ const providerLeaseIdentities = new Map<
3594
+ string,
3595
+ { connectionId: string; connectionGeneration: number; fence: string }
3596
+ >();
3597
+ const refreshSdkClientBridge = () => {
3598
+ if (!fsClientBridge && !terminalClientBridge) {
3599
+ ctx.setSdkClientBridge?.(undefined);
3600
+ return;
3601
+ }
3602
+ const bridge: ClientBridge = {
3603
+ capabilities: {
3604
+ readTextFile: fsClientBridge?.capabilities.readTextFile,
3605
+ writeTextFile: fsClientBridge?.capabilities.writeTextFile,
3606
+ terminal: terminalClientBridge?.capabilities.terminal,
3607
+ },
3608
+ deferAgentInitiatedTurns: true,
3609
+ };
3610
+ if (fsClientBridge?.readTextFile) bridge.readTextFile = fsClientBridge.readTextFile;
3611
+ if (fsClientBridge?.writeTextFile) bridge.writeTextFile = fsClientBridge.writeTextFile;
3612
+ if (terminalClientBridge?.quarantineProviderLease)
3613
+ bridge.quarantineProviderLease = terminalClientBridge.quarantineProviderLease;
3614
+ if (terminalClientBridge?.createTerminal) bridge.createTerminal = terminalClientBridge.createTerminal;
3615
+ ctx.setSdkClientBridge?.(bridge);
3616
+ };
3617
+ const installProviderDefinitions = (
3618
+ capability: string,
3619
+ definitions: unknown,
3620
+ leaseId: string,
3621
+ connectionId: string,
3622
+ connectionGeneration: number,
3623
+ ) => {
3552
3624
  validateProviderDefinitions(capability, definitions);
3625
+ providerLeaseIdentities.set(leaseId, {
3626
+ connectionId,
3627
+ connectionGeneration,
3628
+ fence: `${connectionId}:${connectionGeneration}:${leaseId}`,
3629
+ });
3553
3630
  if (capability === "permission") {
3554
3631
  ctx.setSdkPermissionProvider?.(async (toolCall, permissionOptions, signal) => {
3555
3632
  const result = await host!.reverse.request(
@@ -3587,6 +3664,179 @@ export function createNotificationsExtension(
3587
3664
  );
3588
3665
  return;
3589
3666
  }
3667
+ if (capability === "terminal") {
3668
+ const terminalLeaseId = leaseId;
3669
+ if (!terminalLeaseId) throw new Error("terminal provider lease is unavailable");
3670
+ const terminalProviderIdentity = providerLeaseIdentities.get(terminalLeaseId);
3671
+ if (!terminalProviderIdentity) throw new Error("terminal provider generation is unavailable");
3672
+ if (terminalPublicationFailedLeaseId !== terminalLeaseId) terminalPublicationFailedLeaseId = undefined;
3673
+ const requestTerminal = (
3674
+ method: string,
3675
+ payload: Record<string, unknown>,
3676
+ signal?: AbortSignal,
3677
+ cleanup = false,
3678
+ ) =>
3679
+ host!.reverse.request(
3680
+ "terminal",
3681
+ method,
3682
+ payload,
3683
+ signal,
3684
+ terminalLeaseId,
3685
+ cleanup,
3686
+ terminalProviderIdentity.connectionId,
3687
+ );
3688
+ const settleTerminalCleanup = async (
3689
+ method: "terminal.kill" | "terminal.release",
3690
+ terminalId: string,
3691
+ ): Promise<boolean> =>
3692
+ await Promise.race([
3693
+ Promise.resolve()
3694
+ .then(() => requestTerminal(method, { terminalId }, AbortSignal.timeout(500), true))
3695
+ .then(
3696
+ () => true,
3697
+ () => false,
3698
+ ),
3699
+ Bun.sleep(500).then(() => false),
3700
+ ]);
3701
+ terminalClientBridge = {
3702
+ capabilities: { terminal: true },
3703
+ deferAgentInitiatedTurns: true,
3704
+ async quarantineProviderLease(identity, _reason) {
3705
+ if (typeof identity === "string") throw new Error("terminal provider generation is required");
3706
+ if (
3707
+ identity.leaseId !== terminalLeaseId ||
3708
+ identity.connectionId !== terminalProviderIdentity.connectionId ||
3709
+ identity.connectionGeneration !== terminalProviderIdentity.connectionGeneration ||
3710
+ identity.fence !== terminalProviderIdentity.fence
3711
+ )
3712
+ throw new Error("terminal provider lease mismatch");
3713
+ terminalPublicationFailedLeaseId = terminalLeaseId;
3714
+ host!.quarantineProviderLease(identity, "terminal cleanup ownership uncertain");
3715
+ },
3716
+ async createTerminal(params) {
3717
+ return withTerminalCreationAdmission(async () => {
3718
+ if (terminalPublicationFailedLeaseId === terminalLeaseId)
3719
+ throw new Error("terminal provider quarantined after publication failure");
3720
+ if (params.signal?.aborted) throw new Error("terminal creation aborted");
3721
+ const creationAbort = new AbortController();
3722
+ const creationSignal = params.signal
3723
+ ? AbortSignal.any([params.signal, creationAbort.signal])
3724
+ : creationAbort.signal;
3725
+ let created: unknown;
3726
+ let creationTimedOut = false;
3727
+ try {
3728
+ created = await Promise.race([
3729
+ requestTerminal(
3730
+ "terminal.create",
3731
+ {
3732
+ command: params.command,
3733
+ ...(params.args ? { args: params.args } : {}),
3734
+ ...(params.env ? { env: params.env } : {}),
3735
+ ...(params.cwd ? { cwd: params.cwd } : {}),
3736
+ ...(typeof params.outputByteLimit === "number"
3737
+ ? { outputByteLimit: params.outputByteLimit }
3738
+ : {}),
3739
+ },
3740
+ creationSignal,
3741
+ ),
3742
+ Bun.sleep(500).then(() => {
3743
+ creationTimedOut = true;
3744
+ creationAbort.abort();
3745
+ throw new Error("terminal creation did not settle within 500ms");
3746
+ }),
3747
+ ]);
3748
+ } catch (error) {
3749
+ if (
3750
+ creationSignal.aborted ||
3751
+ (error instanceof ReverseLeaseError && error.code === "payload_too_large")
3752
+ )
3753
+ terminalPublicationFailedLeaseId = terminalLeaseId;
3754
+ if (creationTimedOut) throw new Error("terminal creation did not settle within 500ms");
3755
+ throw error;
3756
+ } finally {
3757
+ creationAbort.abort();
3758
+ }
3759
+ const terminalId =
3760
+ created &&
3761
+ typeof created === "object" &&
3762
+ typeof (created as { terminalId?: unknown }).terminalId === "string"
3763
+ ? (created as { terminalId: string }).terminalId
3764
+ : undefined;
3765
+ if (!terminalId) {
3766
+ terminalPublicationFailedLeaseId = terminalLeaseId;
3767
+ throw new Error("terminal provider returned an invalid create response");
3768
+ }
3769
+ const publicationAbort = new AbortController();
3770
+ const publicationSignal = params.signal
3771
+ ? AbortSignal.any([params.signal, publicationAbort.signal])
3772
+ : publicationAbort.signal;
3773
+ try {
3774
+ await Promise.race([
3775
+ requestTerminal(
3776
+ "terminal.publish",
3777
+ { toolCallId: params.toolCallId, terminalId },
3778
+ publicationSignal,
3779
+ ),
3780
+ Bun.sleep(500).then(() => {
3781
+ publicationAbort.abort();
3782
+ throw new Error("terminal publication did not settle within 500ms");
3783
+ }),
3784
+ ]);
3785
+ } catch (error) {
3786
+ const killed = await settleTerminalCleanup("terminal.kill", terminalId);
3787
+ const released = await settleTerminalCleanup("terminal.release", terminalId);
3788
+ if (!params.signal?.aborted || !killed || !released)
3789
+ terminalPublicationFailedLeaseId = terminalLeaseId;
3790
+ throw error;
3791
+ } finally {
3792
+ publicationAbort.abort();
3793
+ }
3794
+ return {
3795
+ terminalId,
3796
+ providerLeaseId: terminalLeaseId,
3797
+ providerConnectionId: terminalProviderIdentity.connectionId,
3798
+ providerConnectionGeneration: terminalProviderIdentity.connectionGeneration,
3799
+ providerFence: terminalProviderIdentity.fence,
3800
+ async currentOutput() {
3801
+ const output = await requestTerminal("terminal.output", { terminalId });
3802
+ if (
3803
+ !output ||
3804
+ typeof output !== "object" ||
3805
+ typeof (output as { output?: unknown }).output !== "string" ||
3806
+ typeof (output as { truncated?: unknown }).truncated !== "boolean"
3807
+ ) {
3808
+ throw new Error("terminal provider returned an invalid output response");
3809
+ }
3810
+ const response = output as { output: string; truncated: boolean; exitStatus?: unknown };
3811
+ return {
3812
+ output: response.output,
3813
+ truncated: response.truncated,
3814
+ exitStatus:
3815
+ response.exitStatus && typeof response.exitStatus === "object"
3816
+ ? (response.exitStatus as { exitCode?: number | null; signal?: string | null })
3817
+ : null,
3818
+ };
3819
+ },
3820
+ async waitForExit() {
3821
+ const status = await requestTerminal("terminal.waitForExit", { terminalId });
3822
+ if (!status || typeof status !== "object")
3823
+ throw new Error("terminal provider returned an invalid exit response");
3824
+ const response = status as { exitCode?: number | null; signal?: string | null };
3825
+ return { exitCode: response.exitCode ?? null, signal: response.signal ?? null };
3826
+ },
3827
+ async kill() {
3828
+ await requestTerminal("terminal.kill", { terminalId }, undefined, true);
3829
+ },
3830
+ async release() {
3831
+ await requestTerminal("terminal.release", { terminalId }, undefined, true);
3832
+ },
3833
+ };
3834
+ });
3835
+ },
3836
+ };
3837
+ refreshSdkClientBridge();
3838
+ return;
3839
+ }
3590
3840
  if (capability !== "fs") return;
3591
3841
  // Only advertise the methods the client actually declared; a read-only client
3592
3842
  // must keep using the local write path instead of failing against the bridge.
@@ -3618,11 +3868,17 @@ export function createNotificationsExtension(
3618
3868
  };
3619
3869
  if (!canRead) delete bridge.readTextFile;
3620
3870
  if (!canWrite) delete bridge.writeTextFile;
3621
- ctx.setSdkClientBridge?.(bridge);
3871
+ fsClientBridge = bridge;
3872
+ refreshSdkClientBridge();
3622
3873
  };
3623
3874
  const removeProviderDefinitions = (capability: string) => {
3624
3875
  if (capability === "permission") ctx.setSdkPermissionProvider?.(undefined);
3625
- if (capability === "fs") ctx.setSdkClientBridge?.(undefined);
3876
+ if (capability === "fs") fsClientBridge = undefined;
3877
+ if (capability === "terminal") {
3878
+ terminalClientBridge = undefined;
3879
+ terminalPublicationFailedLeaseId = undefined;
3880
+ }
3881
+ if (capability === "fs" || capability === "terminal") refreshSdkClientBridge();
3626
3882
  if (capability === "ui") {
3627
3883
  disposeUiAnswerSource?.();
3628
3884
  disposeUiAnswerSource = undefined;
@@ -3630,6 +3886,12 @@ export function createNotificationsExtension(
3630
3886
  };
3631
3887
 
3632
3888
  const hostCapCache = new Map<string, ReadonlySet<string>>();
3889
+ const MAX_PENDING_PROVIDER_RECLAIMS = 64;
3890
+ const pendingProviderReclaims = new Map<
3891
+ string,
3892
+ Array<{ connectionId: string; connectionGeneration: number; frame: Record<string, unknown> }>
3893
+ >();
3894
+ let pendingProviderReclaimCount = 0;
3633
3895
 
3634
3896
  const configOverrides = new Map<string, unknown>();
3635
3897
  const configRevision = { current: 0 };
@@ -3990,10 +4252,7 @@ export function createNotificationsExtension(
3990
4252
  submission.phase = "terminalizing";
3991
4253
  if (options.fence) {
3992
4254
  const seam = ctx as typeof ctx & {
3993
- abortPromptAndWait?: (
3994
- handle: string,
3995
- options: { graceMs: number },
3996
- ) => Promise<{ status: "settled" } | { status: "unfenced" }>;
4255
+ abortPromptAndWait?: (handle: string, options: { graceMs: number }) => Promise<RunSettlementProof>;
3997
4256
  };
3998
4257
  // Only the handle captured for this correlation may be fenced; a later run
3999
4258
  // must never be aborted by an older prompt's cleanup.
@@ -4006,7 +4265,7 @@ export function createNotificationsExtension(
4006
4265
  );
4007
4266
  return;
4008
4267
  }
4009
- let proof: { status: "settled" } | { status: "unfenced" };
4268
+ let proof: RunSettlementProof;
4010
4269
  try {
4011
4270
  proof = await seam.abortPromptAndWait(submission.executionHandle, {
4012
4271
  graceMs: PROMPT_TERMINALIZATION_GRACE_MS,
@@ -4022,6 +4281,7 @@ export function createNotificationsExtension(
4022
4281
  return;
4023
4282
  }
4024
4283
  if (proof?.status !== "settled") {
4284
+ logger.warn(`sdk: prompt resource settlement unfenced: ${formatPromptSettlementDiagnostic(proof)}`);
4025
4285
  // No settlement proof: never publish a normal terminal. The durable
4026
4286
  // pending claim stays active so restart recovery finalizes it.
4027
4287
  failPromptClosed(
@@ -4185,7 +4445,8 @@ export function createNotificationsExtension(
4185
4445
  };
4186
4446
 
4187
4447
  const sendSdkFrame = (connectionId: string, frame: Record<string, unknown>) => {
4188
- if (extensionShuttingDown || runtime?.stopping || runtimes.get(id) !== runtime) {
4448
+ const cancellation = frame.type === "reverse_cancel";
4449
+ if (!cancellation && (extensionShuttingDown || runtime?.stopping || runtimes.get(id) !== runtime)) {
4189
4450
  abandonPromptResponse(connectionId, frame);
4190
4451
  return;
4191
4452
  }
@@ -4204,13 +4465,15 @@ export function createNotificationsExtension(
4204
4465
  abandonPromptResponse(connectionId, frame);
4205
4466
  throw error;
4206
4467
  }
4468
+ syntheticSeamAdmissions = Math.max(0, syntheticSeamAdmissions - 1);
4469
+ host?.handleDisconnect(connectionId);
4207
4470
  return;
4208
4471
  }
4209
4472
  try {
4210
4473
  server.sendTo(connectionId, json);
4211
4474
  } catch (error) {
4212
- logger.warn(`sdk: directed response delivery failed for ${connectionId}: ${String(error)}`);
4213
4475
  abandonPromptResponse(connectionId, frame);
4476
+ logger.warn(`sdk: directed response delivery rejected for ${connectionId}: ${String(error)}`);
4214
4477
  throw error;
4215
4478
  }
4216
4479
  };
@@ -4223,6 +4486,13 @@ export function createNotificationsExtension(
4223
4486
  connectionCapabilities: connectionId => hostCapCache.get(connectionId),
4224
4487
  installProviderDefinitions,
4225
4488
  onProviderDefinitionsRemoved: removeProviderDefinitions,
4489
+ onProviderLeaseRegistered: (lease, connectionGeneration) => {
4490
+ providerLeaseIdentities.set(lease.leaseId, {
4491
+ connectionId: lease.connectionId,
4492
+ connectionGeneration,
4493
+ fence: `${lease.connectionId}:${connectionGeneration}:${lease.leaseId}`,
4494
+ });
4495
+ },
4226
4496
  onFrame: handler => {
4227
4497
  inboundSdkFrame = handler;
4228
4498
  return () => {
@@ -4230,6 +4500,11 @@ export function createNotificationsExtension(
4230
4500
  };
4231
4501
  },
4232
4502
  onRequest: options.onSdkRequest,
4503
+ onFrameOverflow: connectionId => {
4504
+ fencedConnections.add(connectionId);
4505
+ controlSurface.cancelPendingPreflightsForConnection(connectionId);
4506
+ host?.handleDisconnect(connectionId);
4507
+ },
4233
4508
  beforeControlResponse: async (_connectionId, request, response, sendTerminal) => {
4234
4509
  if (typeof request.operation !== "string" || !identityControlOperations.has(request.operation)) return;
4235
4510
  const pending = deferredIdentityRotation;
@@ -4521,6 +4796,47 @@ export function createNotificationsExtension(
4521
4796
  server.sendTo(connectionId, JSON.stringify({ type: responseType, id, ok: false, error }));
4522
4797
  } catch {}
4523
4798
  };
4799
+ const queueExpectedProviderReclaim = (connectionId: string, frame: Record<string, unknown>): boolean => {
4800
+ if (frame.type !== "register_provider") return false;
4801
+ const capability = typeof frame.capability === "string" ? frame.capability : undefined;
4802
+ const expectedLeaseId = typeof frame.expectedLeaseId === "string" ? frame.expectedLeaseId : undefined;
4803
+ if (!capability || !expectedLeaseId) return false;
4804
+ const lease = host?.reverse.getLease(capability);
4805
+ if (!lease || lease.connectionId === connectionId || lease.leaseId !== expectedLeaseId) return false;
4806
+ if (pendingProviderReclaimCount >= MAX_PENDING_PROVIDER_RECLAIMS) return false;
4807
+ const queued = pendingProviderReclaims.get(lease.connectionId) ?? [];
4808
+ if (typeof frame.id === "string" && queued.some(entry => entry.frame.id === frame.id)) return true;
4809
+ const currentGeneration = host?.connectionGeneration(connectionId) ?? 0;
4810
+ if (currentGeneration <= 0) return false;
4811
+ queued.push({ connectionId, connectionGeneration: currentGeneration, frame });
4812
+ pendingProviderReclaims.set(lease.connectionId, queued);
4813
+ pendingProviderReclaimCount++;
4814
+ return true;
4815
+ };
4816
+ const purgeProviderReclaimsForClaimant = (connectionId: string): void => {
4817
+ for (const [incumbentId, queued] of pendingProviderReclaims) {
4818
+ const retained = queued.filter(entry => entry.connectionId !== connectionId);
4819
+ pendingProviderReclaimCount = Math.max(0, pendingProviderReclaimCount - (queued.length - retained.length));
4820
+ if (retained.length === 0) pendingProviderReclaims.delete(incumbentId);
4821
+ else pendingProviderReclaims.set(incumbentId, retained);
4822
+ }
4823
+ };
4824
+ const drainProviderReclaims = (closedConnectionId: string): void => {
4825
+ const queued = pendingProviderReclaims.get(closedConnectionId);
4826
+ if (!queued) return;
4827
+ pendingProviderReclaims.delete(closedConnectionId);
4828
+ pendingProviderReclaimCount = Math.max(0, pendingProviderReclaimCount - queued.length);
4829
+ for (const entry of queued) {
4830
+ if (
4831
+ initializedRuntime.inboundFenced ||
4832
+ initializedRuntime.stopping ||
4833
+ runtimes.get(id) !== initializedRuntime
4834
+ )
4835
+ continue;
4836
+ if (!host?.isConnectionOpen(entry.connectionId, entry.connectionGeneration)) continue;
4837
+ inboundSdkFrame?.(entry.connectionId, entry.frame);
4838
+ }
4839
+ };
4524
4840
  try {
4525
4841
  server.onSdkFrame((err, inbound) => {
4526
4842
  if (err || !inbound) return;
@@ -4528,6 +4844,13 @@ export function createNotificationsExtension(
4528
4844
  const frame = JSON.parse(inbound.json) as unknown;
4529
4845
  if (!frame || typeof frame !== "object") return;
4530
4846
  const typedFrame = frame as Record<string, unknown>;
4847
+ if (!host.isConnectionOpen(inbound.connectionId)) {
4848
+ if (host.connectionGeneration(inbound.connectionId) > 0) {
4849
+ sendEndpointStale(inbound.connectionId, typedFrame);
4850
+ return;
4851
+ }
4852
+ host.handleConnect(inbound.connectionId);
4853
+ }
4531
4854
  if (inbound.connectionId && fencedConnections.has(inbound.connectionId)) {
4532
4855
  sendEndpointStale(inbound.connectionId, typedFrame);
4533
4856
  return;
@@ -4536,6 +4859,7 @@ export function createNotificationsExtension(
4536
4859
  sendEndpointStale(inbound.connectionId, typedFrame);
4537
4860
  return;
4538
4861
  }
4862
+ if (queueExpectedProviderReclaim(inbound.connectionId, typedFrame)) return;
4539
4863
  if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
4540
4864
  if (typedFrame.type === "event_replay") {
4541
4865
  const capabilities = Array.isArray(typedFrame.capabilities) ? typedFrame.capabilities : [];
@@ -4563,11 +4887,16 @@ export function createNotificationsExtension(
4563
4887
  server.onConnectionClose((_err, connectionId) => {
4564
4888
  if (!connectionId) return;
4565
4889
  controlSurface.cancelPendingPreflightsForConnection(connectionId);
4566
- host.handleDisconnect(connectionId);
4890
+ try {
4891
+ host.handleDisconnect(connectionId);
4892
+ } finally {
4893
+ drainProviderReclaims(connectionId);
4894
+ }
4567
4895
  hostCapCache.delete(connectionId);
4568
4896
  // The socket is gone, so its fence has nothing left to refuse. Dropping the
4569
4897
  // entry keeps the set bounded by live connections instead of growing forever.
4570
4898
  fencedConnections.delete(connectionId);
4899
+ purgeProviderReclaimsForClaimant(connectionId);
4571
4900
  // Deliberate deviation from the plan's "claim prompt_failed on old-owner
4572
4901
  // disconnect": that would break the shipped Q26 reconnect contract, where a
4573
4902
  // client may drop its socket and reconcile the still-running prompt without
@@ -4848,7 +5177,9 @@ export function createNotificationsExtension(
4848
5177
  !suspendedRuntime.stopping &&
4849
5178
  !suspendedRuntime.policySuspended
4850
5179
  )
4851
- inboundSdkFrame?.(`seam:${inbound.requestId ?? "notification"}`, frame);
5180
+ if (syntheticSeamAdmissions >= MAX_SYNTHETIC_SEAM_ADMISSIONS) return;
5181
+ syntheticSeamAdmissions++;
5182
+ inboundSdkFrame?.(`seam:${inbound.requestId ?? "notification"}`, frame);
4852
5183
  });
4853
5184
  }
4854
5185
  }
@@ -4857,6 +5188,8 @@ export function createNotificationsExtension(
4857
5188
  if (inbound.kind === "control_command") {
4858
5189
  const frame = sdkInboundFrame(inbound.commandJson);
4859
5190
  if (frame) {
5191
+ if (syntheticSeamAdmissions >= MAX_SYNTHETIC_SEAM_ADMISSIONS) return;
5192
+ syntheticSeamAdmissions++;
4860
5193
  inboundSdkFrame?.(`seam:${inbound.requestId ?? "notification"}`, frame);
4861
5194
  return;
4862
5195
  }
@@ -49,9 +49,11 @@ export const NOTIFICATION_PROTOCOL_VERSION = 3;
49
49
  * user-created topic adoption (forum-topic folder picker). Generation 36 bound
50
50
  * managed-session replacement to exact native filesystem authority; generation
51
51
  * 37 retires that binding (revert of #3489, which stalled POSIX artifact
52
- * cleanup) while keeping the lifecycle contract moving strictly forward.
52
+ * cleanup); generation 38 binds exact cleanup to parent and link-count authority;
53
+ * generation 39 hardens exact Bash process-tree ownership, settlement, and
54
+ * descendant cleanup authority.
53
55
  */
54
- export const DAEMON_GENERATION = 37;
56
+ export const DAEMON_GENERATION = 39;
55
57
 
56
58
  /**
57
59
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 1 covers