@code-yeongyu/senpi 2026.11.10-1 → 2026.11.10-2

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 (129) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/bundle/chunks/{chunk-GPXZTSEX.js → chunk-2M54EAAY.js} +3 -3
  3. package/dist/bundle/chunks/{chunk-YPAQXRP3.js → chunk-47T6YMI2.js} +2 -2
  4. package/dist/bundle/chunks/{chunk-I5ASHSLZ.js → chunk-4JMGWVRH.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-HI5JSLG5.js → chunk-74IB3JPP.js} +2 -2
  6. package/dist/bundle/chunks/{chunk-NH7SYIGN.js → chunk-DLPV4B3R.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-Y7Z7XPUF.js → chunk-DZQA5LVN.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-2CNSCMYC.js → chunk-E5O4SP5I.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-3IYWH6RO.js → chunk-IKJCA5SH.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-H33A33M4.js → chunk-M2VJVJMK.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-L6NRDUWJ.js → chunk-OF3XVGG4.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-THYEZBUG.js → chunk-Q4G5DECT.js} +2 -2
  13. package/dist/bundle/chunks/{chunk-ARUV3US4.js → chunk-QATU6BBC.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-FAFIGBUD.js → chunk-TUEZNIBK.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-BELNOPAI.js → chunk-UOZDDYBN.js} +2 -2
  16. package/dist/bundle/chunks/{chunk-DBQBJTMF.js → chunk-VUY4BZDI.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-TGW4R7SI.js → chunk-XH24X5B2.js} +1 -1
  18. package/dist/bundle/chunks/chunk-XI6THO4F.js +2 -0
  19. package/dist/bundle/chunks/{chunk-X4XWAB3J.js → chunk-YCPURLPU.js} +38 -38
  20. package/dist/bundle/chunks/{chunk-LHEP3ACI.js → chunk-YNGEZBTP.js} +1 -1
  21. package/dist/bundle/chunks/{cli-main-VG3C2EXI.js → cli-main-Y6ALA5OI.js} +1 -1
  22. package/dist/bundle/chunks/{hand-entry-3LQKUTYS.js → hand-entry-IPDAIOGI.js} +1 -1
  23. package/dist/bundle/chunks/{host-command-CCASS7W4.js → host-command-2326GCUP.js} +1 -1
  24. package/dist/bundle/chunks/{host-lifecycle-WWC7HXQ6.js → host-lifecycle-TNHASSFJ.js} +1 -1
  25. package/dist/bundle/chunks/host-lifecycle.js +1 -1
  26. package/dist/bundle/chunks/{host-session-command-YJB2FLKU.js → host-session-command-BZYBDOIE.js} +1 -1
  27. package/dist/bundle/chunks/{host-socket-request-DMYXC56U.js → host-socket-request-QN4Q2P5E.js} +1 -1
  28. package/dist/bundle/chunks/{interactive-mode-CUN3WMUF.js → interactive-mode-LBQB7DDR.js} +1 -1
  29. package/dist/bundle/chunks/{main-P7XRC2OI.js → main-FFRFP34W.js} +1 -1
  30. package/dist/bundle/chunks/{multi-session-host-244X3P6L.js → multi-session-host-NDIHHJ2L.js} +11 -8
  31. package/dist/bundle/chunks/{package-manager-cli-UVWJOQA3.js → package-manager-cli-XCPAMOYI.js} +1 -1
  32. package/dist/bundle/chunks/{rpc-mode-G4I3HHPB.js → rpc-mode-TKNZM4PZ.js} +1 -1
  33. package/dist/bundle/chunks/{rpc-monitor-source-C6VQ3BC4.js → rpc-monitor-source-PGTDSGWP.js} +1 -1
  34. package/dist/bundle/chunks/{run-attached-tui-R3RB5P7G.js → run-attached-tui-JDUKXDI2.js} +1 -1
  35. package/dist/bundle/chunks/{schedule-command-5FG2OKSR.js → schedule-command-HF2DHABB.js} +1 -1
  36. package/dist/bundle/chunks/{session-control-endpoint-XWCFLJYC.js → session-control-endpoint-A6R6W2JQ.js} +1 -1
  37. package/dist/bundle/chunks/{session-picker-2FZSNFS7.js → session-picker-GWA4S2PO.js} +1 -1
  38. package/dist/bundle/chunks/session-worker.js +119 -116
  39. package/dist/bundle/cli.js +2 -2
  40. package/dist/bundle/host-supervisor.js +1 -1
  41. package/dist/bundle/index.js +1 -1
  42. package/dist/bundle/rpc-entry.js +1 -1
  43. package/dist/bundle/runtime-manifest.json +1 -1
  44. package/dist/core/agent-session-runtime.d.ts +2 -0
  45. package/dist/core/agent-session-runtime.js +5 -0
  46. package/dist/core/agent-session.d.ts +1 -1
  47. package/dist/core/agent-session.js +30 -10
  48. package/dist/core/extensions/builtin/config-reload/change-groups.js +6 -2
  49. package/dist/core/extensions/builtin/mcp/auth/catalog-identity.js +32 -8
  50. package/dist/core/extensions/builtin/mcp/auth/oauth-grant.d.ts +43 -0
  51. package/dist/core/extensions/builtin/mcp/auth/oauth-grant.js +196 -0
  52. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.d.ts +4 -17
  53. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +64 -75
  54. package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.d.ts +1 -1
  55. package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +25 -4
  56. package/dist/core/extensions/builtin/mcp/auth/oauth.js +10 -7
  57. package/dist/core/extensions/builtin/mcp/auth/token-store.d.ts +18 -0
  58. package/dist/core/extensions/builtin/mcp/auth/token-store.js +3 -0
  59. package/dist/core/extensions/builtin/mcp/catalog-cache.js +7 -6
  60. package/dist/core/extensions/builtin/mcp/catalog.js +3 -2
  61. package/dist/core/extensions/builtin/mcp/invocation.js +6 -0
  62. package/dist/core/extensions/builtin/mcp/service-register.js +16 -2
  63. package/dist/core/extensions/builtin/mcp/service-tools-changed.js +61 -54
  64. package/dist/core/extensions/builtin/mcp/startup-race.d.ts +2 -0
  65. package/dist/core/extensions/builtin/mcp/startup-race.js +25 -7
  66. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-2.js +6 -6
  67. package/dist/core/open-timing.d.ts +2 -0
  68. package/dist/core/open-timing.js +7 -1
  69. package/dist/core/session-manager.d.ts +2 -0
  70. package/dist/core/session-manager.js +15 -1
  71. package/dist/modes/interactive/session-control-rpc-commands.d.ts +1 -1
  72. package/dist/modes/interactive/session-control-rpc-commands.js +5 -0
  73. package/dist/modes/rpc/compact-admission-validation.js +4 -1
  74. package/dist/modes/rpc/connection-handler.d.ts +2 -0
  75. package/dist/modes/rpc/connection-handler.js +10 -2
  76. package/dist/modes/rpc/custom-capability.d.ts +2 -0
  77. package/dist/modes/rpc/custom-capability.js +2 -0
  78. package/dist/modes/rpc/effect-commands.d.ts +21 -0
  79. package/dist/modes/rpc/host-capabilities.d.ts +1 -0
  80. package/dist/modes/rpc/host-capabilities.js +3 -1
  81. package/dist/modes/rpc/host-observe-request.d.ts +2 -0
  82. package/dist/modes/rpc/host-observe-request.js +6 -1
  83. package/dist/modes/rpc/host-park-ledger.d.ts +19 -0
  84. package/dist/modes/rpc/host-park-ledger.js +110 -0
  85. package/dist/modes/rpc/lifecycle-delivery.js +4 -1
  86. package/dist/modes/rpc/media-placeholders.js +1 -0
  87. package/dist/modes/rpc/method-classes-schema-host.d.ts +42 -1
  88. package/dist/modes/rpc/method-classes-schema-host.js +24 -21
  89. package/dist/modes/rpc/method-classes-schema.d.ts +42 -1
  90. package/dist/modes/rpc/method-classes-schema.js +5 -2
  91. package/dist/modes/rpc/method-classes.d.ts +2 -0
  92. package/dist/modes/rpc/method-classes.js +3 -0
  93. package/dist/modes/rpc/multi-session-host.js +1 -1
  94. package/dist/modes/rpc/rpc-host-command-types.d.ts +14 -1
  95. package/dist/modes/rpc/rpc-host-response-types.d.ts +21 -0
  96. package/dist/modes/rpc/session-attachment-gate.js +2 -0
  97. package/dist/modes/rpc/session-binding.d.ts +1 -0
  98. package/dist/modes/rpc/session-binding.js +6 -1
  99. package/dist/modes/rpc/session-connection-context.d.ts +2 -0
  100. package/dist/modes/rpc/session-connection-context.js +4 -0
  101. package/dist/modes/rpc/session-registry-attach.d.ts +2 -0
  102. package/dist/modes/rpc/session-registry-attach.js +15 -11
  103. package/dist/modes/rpc/session-registry-claims.js +2 -0
  104. package/dist/modes/rpc/session-registry-open.js +13 -7
  105. package/dist/modes/rpc/session-registry-opening.js +2 -1
  106. package/dist/modes/rpc/session-registry-types.d.ts +4 -0
  107. package/dist/modes/rpc/session-registry.d.ts +3 -0
  108. package/dist/modes/rpc/session-registry.js +27 -3
  109. package/dist/modes/rpc/session-router-close.js +1 -0
  110. package/dist/modes/rpc/session-router-context.d.ts +4 -2
  111. package/dist/modes/rpc/session-router-context.js +3 -0
  112. package/dist/modes/rpc/session-router-host-reads.js +1 -0
  113. package/dist/modes/rpc/session-router-occupancy.js +10 -1
  114. package/dist/modes/rpc/session-router-open-result.d.ts +12 -0
  115. package/dist/modes/rpc/session-router-open-result.js +63 -0
  116. package/dist/modes/rpc/session-router-open.d.ts +3 -0
  117. package/dist/modes/rpc/session-router-open.js +15 -63
  118. package/dist/modes/rpc/session-router-park.d.ts +15 -0
  119. package/dist/modes/rpc/session-router-park.js +176 -0
  120. package/dist/modes/rpc/session-router-routing.d.ts +1 -1
  121. package/dist/modes/rpc/session-router-routing.js +7 -0
  122. package/dist/modes/rpc/session-status-source.js +2 -2
  123. package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
  124. package/docs/engine-socket/fixtures/rpc-inbound-samples.json +22 -2
  125. package/docs/rpc.md +50 -0
  126. package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
  127. package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
  128. package/package.json +6 -6
  129. package/dist/bundle/chunks/chunk-436DNN4Z.js +0 -2
@@ -0,0 +1,110 @@
1
+ /** Speculation is bounded independently of the unlimited in-process session registry. */
2
+ const MAX_PARKS = 4;
3
+ const PARK_TTL_MS = 60_000;
4
+ /** Owns both in-flight builds and ready parks, including the races with claim, expiry and shutdown. */
5
+ export class HostParkLedger {
6
+ constructor(close) {
7
+ this.parks = new Map();
8
+ this.replacements = new Map();
9
+ this.stopped = false;
10
+ this.close = close;
11
+ }
12
+ get size() {
13
+ return this.parks.size;
14
+ }
15
+ async park(key, build) {
16
+ const existing = this.parks.get(key);
17
+ if (existing && !existing.claim && !existing.closing) {
18
+ await existing.ready;
19
+ return "already_parked";
20
+ }
21
+ // A park being claimed or closed is on its way out; the replacement scheduled after it builds the next.
22
+ if (existing)
23
+ return "full";
24
+ const replacement = this.replacements.get(key);
25
+ if (replacement !== undefined) {
26
+ clearTimeout(replacement);
27
+ this.replacements.delete(key);
28
+ }
29
+ if (this.stopped)
30
+ throw new Error("host_draining");
31
+ if (this.parks.size + this.replacements.size >= MAX_PARKS)
32
+ return "full";
33
+ const ready = Promise.withResolvers();
34
+ ready.promise.catch(() => { });
35
+ const park = { ready: ready.promise };
36
+ // Reserved before build's first await: a second request joins, never starts another runtime.
37
+ this.parks.set(key, park);
38
+ try {
39
+ park.opened = await build();
40
+ if (!this.stopped) {
41
+ park.expiry = setTimeout(() => void this.retire(key, park), PARK_TTL_MS);
42
+ park.expiry.unref?.();
43
+ }
44
+ ready.resolve();
45
+ if (this.stopped)
46
+ await this.retire(key, park);
47
+ return "parked";
48
+ }
49
+ catch (cause) {
50
+ this.parks.delete(key);
51
+ ready.reject(cause);
52
+ throw cause;
53
+ }
54
+ }
55
+ /** A claim never waits for a build, and consumes the ready park synchronously. */
56
+ take(key) {
57
+ const park = this.parks.get(key);
58
+ if (this.stopped || !park?.opened || park.claim || park.closing)
59
+ return undefined;
60
+ clearTimeout(park.expiry);
61
+ park.claim = Promise.withResolvers();
62
+ return park.opened;
63
+ }
64
+ finish(key) {
65
+ const park = this.parks.get(key);
66
+ this.parks.delete(key);
67
+ park?.claim?.resolve();
68
+ }
69
+ replace(key, build) {
70
+ if (this.stopped || this.replacements.has(key))
71
+ return;
72
+ const timer = setTimeout(() => {
73
+ this.replacements.delete(key);
74
+ void this.park(key, build).catch((cause) => {
75
+ process.stderr.write(`senpi rpc replacement park failed: ${String(cause)}\n`);
76
+ });
77
+ }, 0);
78
+ timer.unref?.();
79
+ this.replacements.set(key, timer);
80
+ }
81
+ /** Fences new builds synchronously; every build already accepted is closed when it finishes. */
82
+ async stop() {
83
+ this.stopped = true;
84
+ for (const timer of this.replacements.values())
85
+ clearTimeout(timer);
86
+ this.replacements.clear();
87
+ await Promise.all([...this.parks].map(async ([key, park]) => {
88
+ await park.ready.catch(() => { });
89
+ if (park.claim)
90
+ await park.claim.promise;
91
+ else
92
+ await this.retire(key, park);
93
+ }));
94
+ }
95
+ retire(key, park) {
96
+ clearTimeout(park.expiry);
97
+ park.closing ??= (async () => {
98
+ try {
99
+ if (park.opened)
100
+ await this.close(park.opened);
101
+ }
102
+ finally {
103
+ if (this.parks.get(key) === park)
104
+ this.parks.delete(key);
105
+ }
106
+ })();
107
+ return park.closing;
108
+ }
109
+ }
110
+ //# sourceMappingURL=host-park-ledger.js.map
@@ -15,7 +15,10 @@ export function observeLifecycleDelivery(record) {
15
15
  return record;
16
16
  if (record.command === "get_state")
17
17
  return { ...record, data: observeState(data) };
18
- if (record.command === "open_session" && "state" in data && typeof data.state === "object" && data.state !== null)
18
+ if ((record.command === "open_session" || record.command === "claim_session") &&
19
+ "state" in data &&
20
+ typeof data.state === "object" &&
21
+ data.state !== null)
19
22
  return { ...record, data: { ...data, state: observeState(data.state) } };
20
23
  return record;
21
24
  }
@@ -39,6 +39,7 @@ const MEDIA_BEARING_RESPONSE_COMMANDS = new Set([
39
39
  "get_tree",
40
40
  "get_state",
41
41
  "open_session",
42
+ "claim_session",
42
43
  ]);
43
44
  /**
44
45
  * Decoded byte length of a base64 string, derived from its length alone.
@@ -15,6 +15,38 @@ export declare const HOST_RECORD_SCHEMAS: {
15
15
  id: Type.TOptional<Type.TString>;
16
16
  admissionId: Type.TOptional<Type.TString>;
17
17
  type: Type.TLiteral<"open_session">;
18
+ } & {
19
+ retry: Type.TOptional<Type.TLiteral<true>>;
20
+ attended: Type.TOptional<Type.TLiteral<false>>;
21
+ spawnedBy: Type.TOptional<Type.TObject<{
22
+ parentSessionId: Type.TString;
23
+ rootSessionId: Type.TString;
24
+ taskId: Type.TString;
25
+ }>>;
26
+ expectedHostInstance: Type.TOptional<Type.TString>;
27
+ sessionPath: Type.TOptional<Type.TString>;
28
+ cwd: Type.TOptional<Type.TString>;
29
+ provider: Type.TOptional<Type.TString>;
30
+ modelId: Type.TOptional<Type.TString>;
31
+ thinkingLevel: Type.TOptional<Type.TUnion<[Type.TLiteral<string>, ...Type.TLiteral<string>[]]>>;
32
+ permissionPreset: Type.TOptional<Type.TString>;
33
+ retain_on_disconnect: Type.TOptional<Type.TBoolean>;
34
+ kind: Type.TOptional<Type.TUnion<[Type.TLiteral<string>, ...Type.TLiteral<string>[]]>>;
35
+ context: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
36
+ auto_title: Type.TOptional<Type.TBoolean>;
37
+ durableSessionId: Type.TOptional<Type.TString>;
38
+ promptSurface: Type.TOptional<Type.TUnion<[Type.TLiteral<string>, ...Type.TLiteral<string>[]]>>;
39
+ browserEngine: Type.TOptional<Type.TUnion<[Type.TLiteral<string>, ...Type.TLiteral<string>[]]>>;
40
+ retryFallback: Type.TOptional<Type.TObject<{
41
+ modelFallback: Type.TBoolean;
42
+ fallbackChains: Type.TRecord<"^.*$", Type.TImmutable<Type.TArray<Type.TString>>>;
43
+ }>>;
44
+ env: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
45
+ }>;
46
+ claim_session: Type.TObject<{
47
+ id: Type.TOptional<Type.TString>;
48
+ admissionId: Type.TOptional<Type.TString>;
49
+ type: Type.TLiteral<"claim_session">;
18
50
  } & {
19
51
  readonly retry: Type.TOptional<Type.TLiteral<true>>;
20
52
  readonly attended: Type.TOptional<Type.TLiteral<false>>;
@@ -25,7 +57,6 @@ export declare const HOST_RECORD_SCHEMAS: {
25
57
  }>>;
26
58
  readonly expectedHostInstance: Type.TOptional<Type.TString>;
27
59
  readonly sessionPath: Type.TOptional<Type.TString>;
28
- readonly cwd: Type.TOptional<Type.TString>;
29
60
  readonly provider: Type.TOptional<Type.TString>;
30
61
  readonly modelId: Type.TOptional<Type.TString>;
31
62
  readonly thinkingLevel: Type.TOptional<Type.TUnion<[Type.TLiteral<string>, ...Type.TLiteral<string>[]]>>;
@@ -42,6 +73,16 @@ export declare const HOST_RECORD_SCHEMAS: {
42
73
  fallbackChains: Type.TRecord<"^.*$", Type.TImmutable<Type.TArray<Type.TString>>>;
43
74
  }>>;
44
75
  readonly env: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
76
+ readonly cwd: Type.TString;
77
+ }>;
78
+ park_session: Type.TObject<{
79
+ id: Type.TOptional<Type.TString>;
80
+ admissionId: Type.TOptional<Type.TString>;
81
+ type: Type.TLiteral<"park_session">;
82
+ } & {
83
+ readonly cwd: Type.TString;
84
+ readonly kind: Type.TOptional<Type.TUnion<[Type.TLiteral<string>, ...Type.TLiteral<string>[]]>>;
85
+ readonly context: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
45
86
  }>;
46
87
  close_session: Type.TObject<{
47
88
  id: Type.TOptional<Type.TString>;
@@ -9,32 +9,35 @@ const uiResponseFrame = {
9
9
  uiRequestId: OptId,
10
10
  answerId: OptId,
11
11
  };
12
+ const openProperties = {
13
+ retry: Opt(Type.Literal(true)),
14
+ attended: Opt(Type.Literal(false)),
15
+ spawnedBy: Opt(Type.Object({ parentSessionId: Id, rootSessionId: Id, taskId: Id })),
16
+ expectedHostInstance: OptId,
17
+ sessionPath: OptPath,
18
+ cwd: OptPath,
19
+ provider: OptId,
20
+ modelId: OptId,
21
+ thinkingLevel: Opt(ThinkingLevel),
22
+ permissionPreset: OptId,
23
+ retain_on_disconnect: OptBool,
24
+ kind: Opt(SessionKind),
25
+ context: Opt(SessionContext),
26
+ auto_title: OptBool,
27
+ durableSessionId: OptId,
28
+ promptSurface: Opt(literals("terminal", "app", "chat")),
29
+ browserEngine: Opt(literals("connected", "builtin", "none")),
30
+ retryFallback: Opt(RetryFallback),
31
+ env: Opt(SessionEnv),
32
+ };
12
33
  export const HOST_RECORD_SCHEMAS = {
13
34
  get_protocol_info: command("get_protocol_info", {
14
35
  auth: Opt(Type.Object({ client_nonce: Id })),
15
36
  observe: OptBool,
16
37
  }),
17
- open_session: command("open_session", {
18
- retry: Opt(Type.Literal(true)),
19
- attended: Opt(Type.Literal(false)),
20
- spawnedBy: Opt(Type.Object({ parentSessionId: Id, rootSessionId: Id, taskId: Id })),
21
- expectedHostInstance: OptId,
22
- sessionPath: OptPath,
23
- cwd: OptPath,
24
- provider: OptId,
25
- modelId: OptId,
26
- thinkingLevel: Opt(ThinkingLevel),
27
- permissionPreset: OptId,
28
- retain_on_disconnect: OptBool,
29
- kind: Opt(SessionKind),
30
- context: Opt(SessionContext),
31
- auto_title: OptBool,
32
- durableSessionId: OptId,
33
- promptSurface: Opt(literals("terminal", "app", "chat")),
34
- browserEngine: Opt(literals("connected", "builtin", "none")),
35
- retryFallback: Opt(RetryFallback),
36
- env: Opt(SessionEnv),
37
- }),
38
+ open_session: command("open_session", openProperties),
39
+ claim_session: command("claim_session", { ...openProperties, cwd: PathStr }),
40
+ park_session: command("park_session", { cwd: PathStr, kind: Opt(SessionKind), context: Opt(SessionContext) }),
38
41
  close_session: command("close_session", { sessionId: Id }),
39
42
  move_session: Type.Union([
40
43
  move({ op: Type.Literal("prepare"), moveId: Id }),
@@ -70,6 +70,38 @@ export declare const RPC_INBOUND_SCHEMAS: {
70
70
  id: import("typebox").TOptional<import("typebox").TString>;
71
71
  admissionId: import("typebox").TOptional<import("typebox").TString>;
72
72
  type: import("typebox").TLiteral<"open_session">;
73
+ } & {
74
+ retry: import("typebox").TOptional<import("typebox").TLiteral<true>>;
75
+ attended: import("typebox").TOptional<import("typebox").TLiteral<false>>;
76
+ spawnedBy: import("typebox").TOptional<import("typebox").TObject<{
77
+ parentSessionId: import("typebox").TString;
78
+ rootSessionId: import("typebox").TString;
79
+ taskId: import("typebox").TString;
80
+ }>>;
81
+ expectedHostInstance: import("typebox").TOptional<import("typebox").TString>;
82
+ sessionPath: import("typebox").TOptional<import("typebox").TString>;
83
+ cwd: import("typebox").TOptional<import("typebox").TString>;
84
+ provider: import("typebox").TOptional<import("typebox").TString>;
85
+ modelId: import("typebox").TOptional<import("typebox").TString>;
86
+ thinkingLevel: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TLiteral<string>, ...import("typebox").TLiteral<string>[]]>>;
87
+ permissionPreset: import("typebox").TOptional<import("typebox").TString>;
88
+ retain_on_disconnect: import("typebox").TOptional<import("typebox").TBoolean>;
89
+ kind: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TLiteral<string>, ...import("typebox").TLiteral<string>[]]>>;
90
+ context: import("typebox").TOptional<import("typebox").TRecord<"^.*$", import("typebox").TString>>;
91
+ auto_title: import("typebox").TOptional<import("typebox").TBoolean>;
92
+ durableSessionId: import("typebox").TOptional<import("typebox").TString>;
93
+ promptSurface: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TLiteral<string>, ...import("typebox").TLiteral<string>[]]>>;
94
+ browserEngine: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TLiteral<string>, ...import("typebox").TLiteral<string>[]]>>;
95
+ retryFallback: import("typebox").TOptional<import("typebox").TObject<{
96
+ modelFallback: import("typebox").TBoolean;
97
+ fallbackChains: import("typebox").TRecord<"^.*$", import("typebox").TImmutable<import("typebox").TArray<import("typebox").TString>>>;
98
+ }>>;
99
+ env: import("typebox").TOptional<import("typebox").TRecord<"^.*$", import("typebox").TString>>;
100
+ }>;
101
+ readonly claim_session: import("typebox").TObject<{
102
+ id: import("typebox").TOptional<import("typebox").TString>;
103
+ admissionId: import("typebox").TOptional<import("typebox").TString>;
104
+ type: import("typebox").TLiteral<"claim_session">;
73
105
  } & {
74
106
  readonly retry: import("typebox").TOptional<import("typebox").TLiteral<true>>;
75
107
  readonly attended: import("typebox").TOptional<import("typebox").TLiteral<false>>;
@@ -80,7 +112,6 @@ export declare const RPC_INBOUND_SCHEMAS: {
80
112
  }>>;
81
113
  readonly expectedHostInstance: import("typebox").TOptional<import("typebox").TString>;
82
114
  readonly sessionPath: import("typebox").TOptional<import("typebox").TString>;
83
- readonly cwd: import("typebox").TOptional<import("typebox").TString>;
84
115
  readonly provider: import("typebox").TOptional<import("typebox").TString>;
85
116
  readonly modelId: import("typebox").TOptional<import("typebox").TString>;
86
117
  readonly thinkingLevel: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TLiteral<string>, ...import("typebox").TLiteral<string>[]]>>;
@@ -97,6 +128,16 @@ export declare const RPC_INBOUND_SCHEMAS: {
97
128
  fallbackChains: import("typebox").TRecord<"^.*$", import("typebox").TImmutable<import("typebox").TArray<import("typebox").TString>>>;
98
129
  }>>;
99
130
  readonly env: import("typebox").TOptional<import("typebox").TRecord<"^.*$", import("typebox").TString>>;
131
+ readonly cwd: import("typebox").TString;
132
+ }>;
133
+ readonly park_session: import("typebox").TObject<{
134
+ id: import("typebox").TOptional<import("typebox").TString>;
135
+ admissionId: import("typebox").TOptional<import("typebox").TString>;
136
+ type: import("typebox").TLiteral<"park_session">;
137
+ } & {
138
+ readonly cwd: import("typebox").TString;
139
+ readonly kind: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TLiteral<string>, ...import("typebox").TLiteral<string>[]]>>;
140
+ readonly context: import("typebox").TOptional<import("typebox").TRecord<"^.*$", import("typebox").TString>>;
100
141
  }>;
101
142
  readonly close_session: import("typebox").TObject<{
102
143
  id: import("typebox").TOptional<import("typebox").TString>;
@@ -37,12 +37,15 @@ function fieldBoundsError(record) {
37
37
  const payload = rpcCommandPayloadError(record) ?? rpcMessageLengthError(record);
38
38
  if (payload !== undefined)
39
39
  return payload;
40
- if (record.type === "open_session" || record.type === "warm") {
40
+ if (record.type === "open_session" ||
41
+ record.type === "warm" ||
42
+ record.type === "park_session" ||
43
+ record.type === "claim_session") {
41
44
  const context = sessionContextError(record.context);
42
45
  if (context !== undefined)
43
46
  return context;
44
47
  }
45
- if (record.type === "open_session")
48
+ if (record.type === "open_session" || record.type === "claim_session")
46
49
  return sessionRetryFallbackError(record.retryFallback) ?? sessionEnvShapeError(record.env);
47
50
  if (record.type === "set_retry_fallback")
48
51
  return sessionRetryFallbackError(record.retryFallback);
@@ -163,6 +163,8 @@ export declare const RPC_METHOD_CLASS: {
163
163
  readonly classify: typeof openSessionClass;
164
164
  readonly version: 2;
165
165
  };
166
+ readonly claim_session: "device_admin";
167
+ readonly park_session: "device_admin";
166
168
  readonly resume_session: "thread";
167
169
  readonly fork: "thread";
168
170
  readonly clone: "thread";
@@ -207,6 +207,9 @@ export const RPC_METHOD_CLASS = {
207
207
  // thread by id; device_admin when the record names a host path, a permission preset or a tool environment
208
208
  new_session: { classify: newSessionClass, version: 1 },
209
209
  open_session: { classify: openSessionClass, version: 2 },
210
+ // Both commands require a cwd; claim has the same device authority as a cwd-naming open.
211
+ claim_session: "device_admin",
212
+ park_session: "device_admin",
210
213
  resume_session: "thread",
211
214
  fork: "thread",
212
215
  clone: "thread",
@@ -177,7 +177,7 @@ export function createHostCore(options, writer, capabilities = parseClientCapabi
177
177
  return;
178
178
  }
179
179
  const command = parsed;
180
- const response = await gate.dispatch(command.type === "open_session" &&
180
+ const response = await gate.dispatch((command.type === "open_session" || command.type === "claim_session") &&
181
181
  (delegated
182
182
  ? command.retain_on_disconnect !== true
183
183
  : machineOwned && command.retain_on_disconnect === undefined)
@@ -255,7 +255,20 @@ type RpcCommandBody = RpcAttachmentCommand | (RpcSessionCommand & {
255
255
  context?: Record<string, string>;
256
256
  };
257
257
  /** Compact callers may identify their own admission on any correlated session request. */
258
- export type RpcCommand = RpcCommandBody & {
258
+ export type RpcCommand = (RpcCommandBody | {
259
+ id?: string;
260
+ /** Build one invisible session for later claim; never an attachment. Requires `park_claim`. */
261
+ type: "park_session";
262
+ cwd: string;
263
+ kind?: SessionKind;
264
+ context?: Record<string, string>;
265
+ } | (Omit<Extract<RpcCommandBody, {
266
+ type: "open_session";
267
+ }>, "type" | "cwd"> & {
268
+ /** Consume one ready park. A miss returns `claimed: false` and never builds a session. */
269
+ type: "claim_session";
270
+ cwd: string;
271
+ })) & {
259
272
  readonly admissionId?: string;
260
273
  };
261
274
  /** Stdin records: session/host commands plus extension-UI replies and progress, and host-request answers. */
@@ -19,6 +19,27 @@ export interface RpcSessionPresence {
19
19
  readonly held: number;
20
20
  }
21
21
  export type RpcHostCommandResponse = {
22
+ id?: string;
23
+ type: "response";
24
+ command: "park_session";
25
+ success: true;
26
+ data: {
27
+ state: "parked" | "already_parked" | "full";
28
+ };
29
+ } | {
30
+ id?: string;
31
+ type: "response";
32
+ command: "claim_session";
33
+ success: true;
34
+ data: {
35
+ claimed: false;
36
+ } | {
37
+ sessionId: string;
38
+ state: RpcSessionState;
39
+ attached?: boolean;
40
+ presence?: RpcSessionPresence;
41
+ };
42
+ } | {
22
43
  id?: string;
23
44
  type: "response";
24
45
  command: "list_hands";
@@ -8,6 +8,8 @@ export const ATTACH_GATE_EXEMPT = new Set([
8
8
  "unsubscribe_sessions",
9
9
  "get_subscription_limits",
10
10
  "warm",
11
+ "park_session",
12
+ "claim_session",
11
13
  "detach_connection",
12
14
  "open_session",
13
15
  "attach_session",
@@ -4,6 +4,7 @@ import type { RpcSessionEntry } from "./session-registry.ts";
4
4
  /** A session-owned adapter around the classic command and extension-UI wiring. */
5
5
  export interface RpcSessionBinding {
6
6
  handle(command: object): Promise<void>;
7
+ setClientCapabilities?(capabilities: readonly string[]): void;
7
8
  cancelPendingExtensionUiRequests?(): void;
8
9
  /** A connection of the shared host left this session (`close_session`, a dropped or detached connection). */
9
10
  connectionDetached?(connectionId: string): void;
@@ -40,6 +40,8 @@ export async function createRpcSessionBinding(sessionId, entry, writer, requestC
40
40
  return carrier && record && firstOrdinal !== undefined ? { record, carrier, ordinal, firstOrdinal } : undefined;
41
41
  };
42
42
  const enqueueRecord = (record) => {
43
+ if (entry.speculative)
44
+ return;
43
45
  toolSpans.observe(record);
44
46
  const canonical = pairing?.take(record);
45
47
  const refused = pairing?.refused(record) === true;
@@ -103,6 +105,8 @@ export async function createRpcSessionBinding(sessionId, entry, writer, requestC
103
105
  writeInScope(record);
104
106
  };
105
107
  const writeStructuredInScope = bindToProviderScope((record) => {
108
+ if (entry.speculative)
109
+ return;
106
110
  writer.structuredLanes.accept(sessionId, record);
107
111
  const control = pairing?.structured(record);
108
112
  if (control && lane)
@@ -151,7 +155,7 @@ export async function createRpcSessionBinding(sessionId, entry, writer, requestC
151
155
  rpcOwnersByConnection.clear();
152
156
  },
153
157
  publishLoadedSurfacesChanged: (previous, digest) => {
154
- if (entry.scope.state !== "closed")
158
+ if (!entry.speculative && entry.scope.state !== "closed")
155
159
  writer.publishLoadedSurfacesChanged(sessionId, previous, digest);
156
160
  },
157
161
  ...options,
@@ -171,6 +175,7 @@ export async function createRpcSessionBinding(sessionId, entry, writer, requestC
171
175
  });
172
176
  await handler.ready;
173
177
  return {
178
+ setClientCapabilities: (capabilities) => runWithProviderScope(entry.scope, () => handler.setClientCapabilities(capabilities)),
174
179
  handle: (command) => {
175
180
  if (writer.handleMcpForm(command))
176
181
  return Promise.resolve();
@@ -7,6 +7,8 @@ export declare class SessionConnectionContext {
7
7
  private readonly delegationOf;
8
8
  constructor(ownerOf: (id: string) => ExtensionTransportOwner | undefined, delegationOf?: (id: string) => DelegationDescriptor | undefined);
9
9
  run<T>(id: string, task: () => T): T;
10
+ /** Host-owned background construction has no requesting transport or inherited delegation. */
11
+ outside<T>(task: () => T): T;
10
12
  /** The delegation the running line captured at its start, or undefined for a connection that never declared. */
11
13
  get delegation(): DelegationDescriptor | undefined;
12
14
  /**
@@ -10,6 +10,10 @@ export class SessionConnectionContext {
10
10
  const delegation = this.delegationOf(id);
11
11
  return this.storage.run({ id, owner: this.ownerOf(id), ...(delegation === undefined ? {} : { delegation }) }, task);
12
12
  }
13
+ /** Host-owned background construction has no requesting transport or inherited delegation. */
14
+ outside(task) {
15
+ return this.storage.exit(task);
16
+ }
13
17
  /** The delegation the running line captured at its start, or undefined for a connection that never declared. */
14
18
  get delegation() {
15
19
  return this.storage.getStore()?.delegation;
@@ -1,4 +1,6 @@
1
1
  import type { SessionPathReservations } from "./host-reservations.ts";
2
2
  import { type OpenRpcSession, type RpcSessionEntry, type RpcSessionLaunchProfile, type RpcSessionOpenOptions } from "./session-registry-types.ts";
3
3
  export declare function attachToOpenSession(entries: ReadonlyMap<string, RpcSessionEntry>, sessionPath: string, profile: RpcSessionLaunchProfile, options: RpcSessionOpenOptions | undefined, pathReservations: SessionPathReservations | undefined, now: number): Promise<OpenRpcSession>;
4
+ /** The live launch choices shared by attach-on-open and claiming an already-built session. */
5
+ export declare function applyAttachedProfile(entry: RpcSessionEntry, profile: RpcSessionLaunchProfile): void;
4
6
  //# sourceMappingURL=session-registry-attach.d.ts.map
@@ -102,6 +102,21 @@ export async function attachToOpenSession(entries, sessionPath, profile, options
102
102
  entry.retainOnDisconnect = true;
103
103
  if (!entry.durableSessionId)
104
104
  throw new RpcSessionRegistryError("session_path_in_use");
105
+ applyAttachedProfile(entry, profile);
106
+ entry.lastCommandAt = now;
107
+ if (wasParked) {
108
+ entry.lifecycleMutex = entry.lifecycleMutex.then(() => entry.runtime?.emitAttachmentEvent("session_resumed"));
109
+ await entry.lifecycleMutex;
110
+ }
111
+ return {
112
+ sessionId: handle,
113
+ durableSessionId: entry.durableSessionId,
114
+ sessionPath: entry.sessionPath,
115
+ attached: true,
116
+ };
117
+ }
118
+ /** The live launch choices shared by attach-on-open and claiming an already-built session. */
119
+ export function applyAttachedProfile(entry, profile) {
105
120
  // The surface, browser engine and permission preset follow the client: an attach that names one
106
121
  // moves the live session to it; an attach without one keeps what the session has.
107
122
  if (profile.promptSurface !== undefined && profile.promptSurface !== entry.profile.promptSurface) {
@@ -121,16 +136,5 @@ export async function attachToOpenSession(entries, sessionPath, profile, options
121
136
  entry.profile = frozenProfile(withClientPreset(entry.profile, namedPreset));
122
137
  entry.runtime?.setPermissionPreset(namedPreset);
123
138
  }
124
- entry.lastCommandAt = now;
125
- if (wasParked) {
126
- entry.lifecycleMutex = entry.lifecycleMutex.then(() => entry.runtime?.emitAttachmentEvent("session_resumed"));
127
- await entry.lifecycleMutex;
128
- }
129
- return {
130
- sessionId: handle,
131
- durableSessionId: entry.durableSessionId,
132
- sessionPath: entry.sessionPath,
133
- attached: true,
134
- };
135
139
  }
136
140
  //# sourceMappingURL=session-registry-attach.js.map
@@ -70,6 +70,8 @@ export async function settlePendingClaim(pending, sessionPath, graceMs) {
70
70
  /** Reconcile path and durable identity after runtime replacement. */
71
71
  export function syncRuntimeMetadata(entries, reservations, pathReservations) {
72
72
  for (const entry of entries) {
73
+ if (entry.speculative)
74
+ continue;
73
75
  const manager = entry.runtime?.session.sessionManager;
74
76
  if (!manager)
75
77
  continue;
@@ -28,7 +28,7 @@ export async function openRegistrySession(host, requested, options) {
28
28
  const requestedDurableId = profile.durableSessionId;
29
29
  if (requestedDurableId !== undefined) {
30
30
  for (const entry of host.entries.values()) {
31
- if (entry.state === "closed")
31
+ if (entry.state === "closed" || entry.speculative)
32
32
  continue;
33
33
  if (entry.durableSessionId !== requestedDurableId)
34
34
  continue;
@@ -101,7 +101,8 @@ export async function openRegistrySession(host, requested, options) {
101
101
  const { entry, storedProfile, runtimeProfile, sessionStartEvent, settleOpen } = opening;
102
102
  host.entries.set(handle, entry);
103
103
  claimSettled?.();
104
- host.options.onSizeChange?.(host.entries.size);
104
+ if (!entry.speculative)
105
+ host.options.onSizeChange?.([...host.entries.values()].filter((item) => !item.speculative).length);
105
106
  const timer = startOpenTimer(`registry-open:${handle}`);
106
107
  try {
107
108
  const newSessionOptions = {
@@ -112,7 +113,9 @@ export async function openRegistrySession(host, requested, options) {
112
113
  ? host.options.sessionFileLoad === "blocking"
113
114
  ? SessionManager.open(sessionPath, undefined, storedProfile.cwd, newSessionOptions)
114
115
  : await SessionManager.openAsync(sessionPath, undefined, storedProfile.cwd, newSessionOptions, host.options.createSlicer?.())
115
- : SessionManager.create(storedProfile.cwd, undefined, newSessionOptions);
116
+ : options?.speculative
117
+ ? SessionManager.inMemory(storedProfile.cwd, newSessionOptions)
118
+ : SessionManager.create(storedProfile.cwd, undefined, newSessionOptions);
116
119
  timer.mark("session_manager");
117
120
  entry.runtime = await runWithProviderScope(entry.scope, () => createAgentSessionRuntime(host.options.createRuntime, {
118
121
  cwd: manager.getCwd(),
@@ -125,19 +128,21 @@ export async function openRegistrySession(host, requested, options) {
125
128
  assertRequestedModelResolved(runtimeProfile, entry.runtime.diagnostics);
126
129
  ClientAdmissions.sealQueuedOnReplace(entry.runtime);
127
130
  EffectLedger.sealOnReplace(entry.runtime);
128
- entry.durableSessionId = manager.getSessionId();
131
+ const durableSessionId = manager.getSessionId();
132
+ if (!entry.speculative)
133
+ entry.durableSessionId = durableSessionId;
129
134
  entry.sessionPath ??= manager.getSessionFile();
130
135
  // The accepted baseline precedes the open, so router admission and its reply come after it (R3.C1.1).
131
136
  // Gated like a worker incarnation's lane: with the flag off no session owns one (RULING-112).
132
137
  if (host.options.workerIsolate !== true && structuredLaneEnabled()) {
133
138
  entry.acceptance = new SessionAcceptanceLane(undefined, undefined, host.options.agentDir);
134
- await entry.acceptance.generations.bind(entry.durableSessionId);
139
+ await entry.acceptance.generations.bind(durableSessionId);
135
140
  entry.acceptance.bootstrap({ transcript: manager.beginTranscriptJournal() });
136
141
  setSessionRecoveryEpoch(entry.runtime.session, entry.acceptance.recoveryEpoch);
137
142
  }
138
143
  entry.state = "open";
139
144
  settleOpen.resolve();
140
- return { sessionId: handle, durableSessionId: entry.durableSessionId, sessionPath: entry.sessionPath };
145
+ return { sessionId: handle, durableSessionId, sessionPath: entry.sessionPath };
141
146
  }
142
147
  catch (error) {
143
148
  // Runtime construction may have started extensions, watchers, and provider
@@ -156,7 +161,8 @@ export async function openRegistrySession(host, requested, options) {
156
161
  }
157
162
  finally {
158
163
  host.entries.delete(handle);
159
- host.options.onSizeChange?.(host.entries.size);
164
+ if (!entry.speculative)
165
+ host.options.onSizeChange?.([...host.entries.values()].filter((item) => !item.speculative).length);
160
166
  if (sessionPath)
161
167
  await releaseWithinGrace(host.teardownHost, handle, sessionPath);
162
168
  }
@@ -38,13 +38,14 @@ export function buildOpeningEntry(input) {
38
38
  openCompletion.catch(() => { });
39
39
  const entry = {
40
40
  state: "opening",
41
+ ...(options?.speculative ? { speculative: true } : {}),
41
42
  scope: new ProviderScope(),
42
43
  profile: storedProfile,
43
44
  ...sessionIdentity(storedProfile),
44
45
  sessionPath,
45
46
  reservationKey: sessionPath,
46
47
  cwd: storedProfile.cwd,
47
- attachments: 1,
48
+ attachments: options?.speculative ? 0 : 1,
48
49
  retainOnDisconnect: options?.retainOnDisconnect === true,
49
50
  lastCommandAt: input.now,
50
51
  lifecycleMutex: Promise.resolve(),