@gajae-code/coding-agent 0.12.6 → 0.12.8

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 (150) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/types/async/job-manager.d.ts +36 -1
  3. package/dist/types/commands/notify.d.ts +4 -0
  4. package/dist/types/config/file-lock.d.ts +3 -0
  5. package/dist/types/config/model-discovery-manager.d.ts +6 -1
  6. package/dist/types/config/model-registry.d.ts +2 -0
  7. package/dist/types/config/resolve-config-value.d.ts +2 -1
  8. package/dist/types/config/settings-schema.d.ts +17 -1
  9. package/dist/types/eval/backend.d.ts +2 -2
  10. package/dist/types/exec/bash-executor.d.ts +8 -10
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
  12. package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
  13. package/dist/types/main.d.ts +1 -0
  14. package/dist/types/modes/components/hook-selector.d.ts +20 -0
  15. package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
  16. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  17. package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
  18. package/dist/types/sdk/broker/broker.d.ts +5 -1
  19. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  20. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -3
  21. package/dist/types/sdk/bus/config.d.ts +75 -113
  22. package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
  23. package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
  24. package/dist/types/sdk/bus/index.d.ts +1 -1
  25. package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
  26. package/dist/types/sdk/bus/notification-service.d.ts +31 -4
  27. package/dist/types/sdk/bus/session-control.d.ts +8 -3
  28. package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
  29. package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
  30. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  31. package/dist/types/sdk/client/discovery.d.ts +2 -1
  32. package/dist/types/sdk/host/host.d.ts +2 -23
  33. package/dist/types/sdk/host/query/handlers.d.ts +2 -0
  34. package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
  35. package/dist/types/sdk/index.d.ts +1 -0
  36. package/dist/types/sdk/providers.d.ts +22 -0
  37. package/dist/types/sdk/session-directory.d.ts +1 -1
  38. package/dist/types/session/artifacts.d.ts +5 -8
  39. package/dist/types/session/client-bridge.d.ts +0 -14
  40. package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
  41. package/dist/types/session/internal/managed-session-storage.d.ts +11 -4
  42. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
  43. package/dist/types/session/internal/session-open-errors.d.ts +5 -0
  44. package/dist/types/session/messages.d.ts +1 -4
  45. package/dist/types/session/session-manager.d.ts +7 -6
  46. package/dist/types/session/session-storage.d.ts +2 -0
  47. package/dist/types/session/streaming-output.d.ts +0 -32
  48. package/dist/types/task/receipt.d.ts +1 -2
  49. package/dist/types/task/types.d.ts +43 -178
  50. package/dist/types/tools/bash.d.ts +1 -1
  51. package/dist/types/tools/fetch.d.ts +0 -3
  52. package/dist/types/tools/output-meta.d.ts +5 -37
  53. package/dist/types/tools/subagent.d.ts +15 -0
  54. package/dist/types/tools/todo-write.d.ts +3 -0
  55. package/dist/types/tools/tool-result.d.ts +0 -1
  56. package/package.json +10 -8
  57. package/scripts/build-sdk-package-smoke.ts +51 -0
  58. package/scripts/capture-notifications-settings-showcase.ts +1 -1
  59. package/src/async/job-manager.ts +430 -46
  60. package/src/cli/notify-cli.ts +204 -94
  61. package/src/commands/notify.ts +19 -8
  62. package/src/config/file-lock.ts +6 -0
  63. package/src/config/model-discovery-manager.ts +50 -8
  64. package/src/config/model-registry.ts +862 -75
  65. package/src/config/resolve-config-value.ts +10 -8
  66. package/src/config/settings-schema.ts +25 -15
  67. package/src/config/settings.ts +15 -15
  68. package/src/cursor.ts +13 -20
  69. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
  70. package/src/eval/backend.ts +2 -2
  71. package/src/exec/bash-executor.ts +151 -425
  72. package/src/gjc-runtime/session-state-sidecar.ts +12 -2
  73. package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
  74. package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
  75. package/src/gjc-runtime/workflow-manifest.ts +0 -12
  76. package/src/internal-urls/agent-protocol.ts +38 -98
  77. package/src/internal-urls/artifact-protocol.ts +17 -28
  78. package/src/internal-urls/docs-index.generated.ts +7 -7
  79. package/src/internal-urls/registry-helpers.ts +0 -62
  80. package/src/main.ts +43 -7
  81. package/src/modes/DESIGN.md +11 -11
  82. package/src/modes/acp/acp-agent.ts +0 -24
  83. package/src/modes/acp/acp-client-bridge.ts +11 -154
  84. package/src/modes/components/hook-selector.ts +155 -44
  85. package/src/modes/components/model-selector.ts +242 -49
  86. package/src/modes/components/notifications-settings-editor.ts +631 -85
  87. package/src/modes/controllers/extension-ui-controller.ts +114 -15
  88. package/src/modes/controllers/selector-controller.ts +464 -50
  89. package/src/prompts/tools/bash.md +2 -2
  90. package/src/sdk/acp/adapter.ts +9 -200
  91. package/src/sdk/broker/broker.ts +54 -10
  92. package/src/sdk/broker/lifecycle-ledger.ts +34 -1
  93. package/src/sdk/broker/lifecycle.ts +47 -50
  94. package/src/sdk/bus/chat-daemon-cli.ts +12 -5
  95. package/src/sdk/bus/chat-daemon-control.ts +35 -8
  96. package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
  97. package/src/sdk/bus/config.ts +534 -286
  98. package/src/sdk/bus/discord-live-provider.ts +57 -2
  99. package/src/sdk/bus/discord-provider.ts +21 -0
  100. package/src/sdk/bus/index.ts +105 -359
  101. package/src/sdk/bus/notification-orchestration.ts +371 -68
  102. package/src/sdk/bus/notification-service.ts +431 -130
  103. package/src/sdk/bus/session-control.ts +119 -44
  104. package/src/sdk/bus/slack-live-provider.ts +78 -2
  105. package/src/sdk/bus/slack-provider.ts +24 -0
  106. package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
  107. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  108. package/src/sdk/bus/telegram-daemon-control.ts +3 -3
  109. package/src/sdk/bus/telegram-daemon.ts +23 -15
  110. package/src/sdk/client/discovery.ts +10 -4
  111. package/src/sdk/host/host.ts +9 -187
  112. package/src/sdk/host/query/handlers.ts +20 -1
  113. package/src/sdk/host/reverse-leases.ts +42 -249
  114. package/src/sdk/index.ts +1 -0
  115. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  116. package/src/sdk/protocol/operation-registry.ts +5 -2
  117. package/src/sdk/providers.ts +51 -0
  118. package/src/sdk/session-directory.ts +1 -0
  119. package/src/sdk/session.ts +10 -7
  120. package/src/session/agent-session.ts +59 -202
  121. package/src/session/artifacts.ts +74 -211
  122. package/src/session/client-bridge.ts +0 -15
  123. package/src/session/internal/managed-session-scope.ts +981 -450
  124. package/src/session/internal/managed-session-storage.ts +362 -124
  125. package/src/session/internal/native-publish-outcome.ts +3 -1
  126. package/src/session/internal/session-open-errors.ts +11 -0
  127. package/src/session/messages.ts +7 -32
  128. package/src/session/session-manager.ts +48 -15
  129. package/src/session/session-storage.ts +72 -5
  130. package/src/session/streaming-output.ts +40 -198
  131. package/src/slash-commands/builtin-registry.ts +82 -4
  132. package/src/task/index.ts +237 -65
  133. package/src/task/receipt.ts +2 -4
  134. package/src/task/render.ts +0 -30
  135. package/src/task/types.ts +10 -34
  136. package/src/tools/ask.ts +1 -1
  137. package/src/tools/bash.ts +239 -818
  138. package/src/tools/eval.ts +4 -99
  139. package/src/tools/fetch.ts +15 -77
  140. package/src/tools/find.ts +3 -7
  141. package/src/tools/output-meta.ts +114 -696
  142. package/src/tools/search.ts +4 -9
  143. package/src/tools/subagent.ts +97 -75
  144. package/src/tools/telegram-send.ts +4 -2
  145. package/src/tools/todo-write.ts +47 -0
  146. package/src/tools/tool-result.ts +0 -1
  147. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
  148. package/dist/types/utils/artifact-id.d.ts +0 -2
  149. package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
  150. package/src/utils/artifact-id.ts +0 -6
@@ -5,12 +5,7 @@ export const REVERSE_HEARTBEAT_MS = 5_000;
5
5
  export const REVERSE_LEASE_TTL_MS = 15_000;
6
6
  export const REVERSE_RECLAIM_GRACE_MS = 15_000;
7
7
  export const MAX_REVERSE_OUTSTANDING = 64;
8
- export const MAX_REVERSE_CLEANUP_OUTSTANDING = 2;
9
8
  export const MAX_REVERSE_PAYLOAD_BYTES = 256 * 1024;
10
- export const MAX_REVERSE_TERMINAL_OUTPUT_BYTES = 10 * 1024 * 1024;
11
- export const MAX_REVERSE_TERMINAL_OUTPUT_PAYLOAD_BYTES = 64 * 1024 * 1024;
12
- const MAX_REVERSE_IDEMPOTENCY_ENTRIES = 256;
13
- const MAX_REVERSE_IDEMPOTENCY_KEY_BYTES = 512;
14
9
 
15
10
  export class ReverseLeaseError extends Error {
16
11
  constructor(
@@ -44,7 +39,6 @@ interface Outstanding {
44
39
  connectionId: string;
45
40
  capability: string;
46
41
  leaseId: string;
47
- method: string;
48
42
  resolve: (value: unknown) => void;
49
43
  reject: (reason: Error) => void;
50
44
  signal?: AbortSignal;
@@ -55,13 +49,7 @@ export interface ReverseLeaseOptions {
55
49
  now?: () => number;
56
50
  leaseTtlMs?: number;
57
51
  sendFrame: (connectionId: string, frame: SdkFrame) => void | Promise<void>;
58
- installDefinitions?: (
59
- capability: string,
60
- definitions: unknown,
61
- leaseId: string,
62
- connectionId: string,
63
- connectionGeneration: number,
64
- ) => void;
52
+ installDefinitions?: (capability: string, definitions: unknown) => void;
65
53
  onCancel?: (requestId: string, reason: "provider_disconnected" | "lease_released") => void;
66
54
  onDefinitionsRemoved?: (capability: string) => void;
67
55
  }
@@ -93,10 +81,8 @@ export class ReverseLeaseRuntime {
93
81
  readonly #onCancel?: ReverseLeaseOptions["onCancel"];
94
82
  readonly #leases = new Map<string, ProviderLease>();
95
83
  readonly #idempotency = new Map<string, { fingerprint: string; lease: ProviderLease }>();
96
- readonly #idempotencyOrder: string[] = [];
97
-
98
84
  readonly #outstanding = new Map<string, Outstanding>();
99
- readonly #installedCapabilities = new Map<string, string>();
85
+ readonly #installedCapabilities = new Set<string>();
100
86
  readonly #sweepTimer: ReturnType<typeof setInterval>;
101
87
  #disposing = false;
102
88
 
@@ -117,15 +103,11 @@ export class ReverseLeaseRuntime {
117
103
  definitions: unknown,
118
104
  expectedLeaseId?: string,
119
105
  idempotencyKey?: string,
120
- connectionGeneration = 0,
121
106
  ): ProviderLease {
122
107
  if (this.#disposing) throw new Error("reverse runtime is disposing");
123
- if (idempotencyKey !== undefined && Buffer.byteLength(idempotencyKey, "utf8") > MAX_REVERSE_IDEMPOTENCY_KEY_BYTES)
124
- throw new ReverseLeaseError("payload_too_large");
125
108
  const key = `${connectionId}\u0000${idempotencyKey ?? ""}`;
126
109
  const fingerprint = registrationFingerprint(capability, definitions, expectedLeaseId);
127
110
  const replay = idempotencyKey ? this.#idempotency.get(key) : undefined;
128
-
129
111
  if (replay) {
130
112
  if (replay.fingerprint !== fingerprint) throw new ReverseLeaseError("idempotency_conflict");
131
113
  const current = this.#leases.get(capability);
@@ -133,19 +115,12 @@ export class ReverseLeaseRuntime {
133
115
  }
134
116
  const now = this.#now();
135
117
  const existing = this.#leases.get(capability);
136
- if (existing && existing.expiresAt <= now) {
137
- this.#cancelForLease(existing.capability, existing.leaseId);
138
- this.#removeDefinitions(existing.capability, existing.leaseId);
139
- this.#evictIdempotencyForLease(existing.leaseId);
140
- }
141
- const reclaimReserved = existing?.graceUntil !== undefined && existing.graceUntil > now;
142
- if (reclaimReserved && existing!.leaseId !== expectedLeaseId) {
143
- throw new ReverseLeaseError("provider_lease_conflict");
144
- }
118
+ if (existing && existing.expiresAt <= now) this.#removeDefinitions(existing.capability);
145
119
  const pendingHandoff = existing?.active === false && existing.expiresAt > now;
146
120
  if (pendingHandoff) {
147
121
  if (existing!.connectionId !== connectionId || existing!.leaseId !== expectedLeaseId)
148
122
  throw new ReverseLeaseError("provider_lease_conflict");
123
+ this.#installDefinitionsFor(capability, definitions);
149
124
  const lease: ProviderLease = {
150
125
  leaseId: existing!.leaseId,
151
126
  connectionId,
@@ -154,22 +129,17 @@ export class ReverseLeaseRuntime {
154
129
  expiresAt: now + this.#leaseTtlMs,
155
130
  active: true,
156
131
  };
157
- this.#installDefinitionsFor(capability, definitions, lease.leaseId, lease.connectionId, connectionGeneration);
158
132
  this.#leases.set(capability, lease);
159
- if (idempotencyKey) this.#rememberIdempotency(key, fingerprint, lease);
160
-
133
+ if (idempotencyKey) this.#idempotency.set(key, { fingerprint, lease });
161
134
  return { ...lease };
162
135
  }
163
136
  const reclaiming =
164
137
  existing?.leaseId === expectedLeaseId && existing?.graceUntil !== undefined && now <= existing.graceUntil;
165
138
  const refreshing =
166
- existing?.active !== false &&
167
- existing?.connectionId === connectionId &&
168
- existing.expiresAt > now &&
169
- (expectedLeaseId === undefined || existing.leaseId === expectedLeaseId);
170
-
139
+ existing?.active !== false && existing?.connectionId === connectionId && existing.expiresAt > now;
171
140
  if (existing && !reclaiming && !refreshing && existing.connectionId !== connectionId && existing.expiresAt > now)
172
141
  throw new ReverseLeaseError("provider_lease_conflict");
142
+ this.#installDefinitionsFor(capability, definitions);
173
143
  const lease: ProviderLease = {
174
144
  leaseId: reclaiming || refreshing ? existing!.leaseId : randomUUID(),
175
145
  connectionId,
@@ -178,22 +148,17 @@ export class ReverseLeaseRuntime {
178
148
  expiresAt: now + this.#leaseTtlMs,
179
149
  active: true,
180
150
  };
181
- this.#installDefinitionsFor(capability, definitions, lease.leaseId, lease.connectionId, connectionGeneration);
182
151
  this.#leases.set(capability, lease);
183
- if (idempotencyKey) this.#rememberIdempotency(key, fingerprint, lease);
184
-
152
+ if (idempotencyKey) this.#idempotency.set(key, { fingerprint, lease });
185
153
  return { ...lease };
186
154
  }
187
155
 
188
156
  heartbeat(connectionId: string, leaseId: string): ProviderLease {
189
157
  const lease = this.#owner(connectionId, leaseId);
190
158
  if (lease.expiresAt <= this.#now()) {
191
- this.#cancelForLease(lease.capability, lease.leaseId);
192
- this.#removeDefinitions(lease.capability, lease.leaseId);
193
- this.#evictIdempotencyForLease(lease.leaseId);
159
+ this.#removeDefinitions(lease.capability);
194
160
  throw new ReverseLeaseError("lease_expired");
195
161
  }
196
-
197
162
  lease.expiresAt = this.#now() + this.#leaseTtlMs;
198
163
  lease.graceUntil = undefined;
199
164
  return { ...lease };
@@ -201,14 +166,8 @@ export class ReverseLeaseRuntime {
201
166
 
202
167
  release(connectionId: string, leaseId: string, handoffTo?: string): ProviderLease {
203
168
  const lease = this.#owner(connectionId, leaseId);
204
- if (lease.expiresAt <= this.#now()) {
205
- this.#cancelForLease(lease.capability, lease.leaseId);
206
- this.#removeDefinitions(lease.capability, lease.leaseId);
207
- this.#evictIdempotencyForLease(lease.leaseId);
208
- throw new ReverseLeaseError("lease_expired");
209
- }
210
- this.#cancelForLease(lease.capability, lease.leaseId);
211
- this.#removeDefinitions(lease.capability, lease.leaseId);
169
+ this.#cancelForConnection(connectionId, "lease_released");
170
+ this.#removeDefinitions(lease.capability);
212
171
  if (handoffTo) {
213
172
  lease.connectionId = handoffTo;
214
173
  lease.expiresAt = this.#now() + REVERSE_RECLAIM_GRACE_MS;
@@ -216,32 +175,22 @@ export class ReverseLeaseRuntime {
216
175
  lease.active = false;
217
176
  return { ...lease };
218
177
  }
219
- this.#evictIdempotencyForLease(lease.leaseId);
220
178
  this.#leases.delete(lease.capability);
221
179
  return { ...lease };
222
180
  }
223
181
 
224
182
  disconnect(connectionId: string): void {
225
183
  const now = this.#now();
226
- for (const lease of this.#leases.values()) {
227
- if (lease.connectionId !== connectionId) continue;
228
- lease.expiresAt = now;
229
- lease.graceUntil = now + REVERSE_RECLAIM_GRACE_MS;
230
- this.#removeDefinitions(lease.capability, lease.leaseId);
231
- this.#evictIdempotencyForLease(lease.leaseId);
232
- }
184
+ for (const lease of this.#leases.values())
185
+ if (lease.connectionId === connectionId) {
186
+ lease.expiresAt = now;
187
+ lease.graceUntil = now + REVERSE_RECLAIM_GRACE_MS;
188
+ this.#removeDefinitions(lease.capability);
189
+ }
233
190
  this.#cancelForConnection(connectionId, "provider_disconnected");
234
191
  }
235
192
 
236
- request(
237
- capability: string,
238
- method: string,
239
- payload: unknown,
240
- signal?: AbortSignal,
241
- expectedLeaseId?: string,
242
- cleanup = false,
243
- expectedConnectionId?: string,
244
- ): Promise<unknown> {
193
+ request(capability: string, method: string, payload: unknown, signal?: AbortSignal): Promise<unknown> {
245
194
  if (this.#disposing) throw new Error("reverse runtime is disposing");
246
195
  this.#assertPayload(payload);
247
196
  const lease = this.#liveLease(capability);
@@ -251,24 +200,14 @@ export class ReverseLeaseRuntime {
251
200
  throw new ReverseLeaseError("lease_unavailable");
252
201
  throw new ReverseLeaseError("provider_required");
253
202
  }
254
- if (expectedLeaseId !== undefined && lease.leaseId !== expectedLeaseId) {
255
- throw new ReverseLeaseError("lease_unavailable");
256
- }
257
- if (expectedConnectionId !== undefined && lease.connectionId !== expectedConnectionId) {
258
- throw new ReverseLeaseError("lease_unavailable");
259
- }
260
203
  if (signal?.aborted)
261
204
  return Promise.reject(Object.assign(new Error("request_cancelled"), { name: "request_cancelled" }));
262
- const outstandingLimit = cleanup
263
- ? MAX_REVERSE_OUTSTANDING + MAX_REVERSE_CLEANUP_OUTSTANDING
264
- : MAX_REVERSE_OUTSTANDING;
265
- if (this.#outstanding.size >= outstandingLimit) throw new ReverseLeaseError("too_many_outstanding");
205
+ if (this.#outstanding.size >= MAX_REVERSE_OUTSTANDING) throw new ReverseLeaseError("too_many_outstanding");
266
206
  const id = randomUUID();
267
207
  return new Promise((resolve, reject) => {
268
208
  const outstanding: Outstanding = {
269
209
  connectionId: lease.connectionId,
270
210
  capability,
271
- method,
272
211
  leaseId: lease.leaseId,
273
212
  resolve,
274
213
  reject,
@@ -325,58 +264,13 @@ export class ReverseLeaseRuntime {
325
264
  leaseId: string,
326
265
  result: unknown,
327
266
  error?: { code: string; message: string },
328
- wireEnvelope?: unknown,
329
267
  ): void {
268
+ this.#assertPayload(result);
330
269
  const request = this.#outstanding.get(id);
331
270
  if (!request) throw new ReverseLeaseError("unknown_request");
332
271
  if (request.connectionId !== connectionId || request.leaseId !== leaseId)
333
272
  throw new ReverseLeaseError("not_lease_owner");
334
- const lease = this.#leases.get(request.capability);
335
- if (
336
- !lease?.active ||
337
- lease.leaseId !== request.leaseId ||
338
- lease.connectionId !== request.connectionId ||
339
- lease.expiresAt <= this.#now()
340
- ) {
341
- const failure = new ReverseLeaseError("lease_expired");
342
- this.#settleOutstandingWithFailure(id, request, failure, true);
343
- throw failure;
344
- }
345
- const envelope =
346
- wireEnvelope ??
347
- ({
348
- type: "reverse_response",
349
- id,
350
- connectionId,
351
- leaseId,
352
- ok: error === undefined,
353
- ...(error ? { error } : { result }),
354
- } satisfies Record<string, unknown>);
355
- try {
356
- this.#assertPayload(envelope, MAX_REVERSE_TERMINAL_OUTPUT_PAYLOAD_BYTES);
357
- const terminalOutputResponse =
358
- error === undefined &&
359
- request.capability === "terminal" &&
360
- request.method === "terminal.output" &&
361
- this.#isTerminalOutputEnvelope(envelope, id, connectionId, leaseId);
362
- const maxBytes = terminalOutputResponse
363
- ? MAX_REVERSE_TERMINAL_OUTPUT_PAYLOAD_BYTES
364
- : MAX_REVERSE_PAYLOAD_BYTES;
365
- if (
366
- terminalOutputResponse &&
367
- result &&
368
- typeof result === "object" &&
369
- typeof (result as { output?: unknown }).output === "string" &&
370
- Buffer.byteLength((result as { output: string }).output, "utf-8") > MAX_REVERSE_TERMINAL_OUTPUT_BYTES
371
- )
372
- throw new ReverseLeaseError("payload_too_large");
373
- this.#assertPayload(envelope, maxBytes);
374
- } catch (failure) {
375
- const responseError = failure instanceof Error ? failure : new Error(String(failure));
376
- this.#settleOutstandingWithFailure(id, request, responseError);
377
- throw responseError;
378
- }
379
- if (this.#takeOutstanding(id) !== request) return;
273
+ this.#takeOutstanding(id);
380
274
  if (error) {
381
275
  const rejection = new Error(error.message);
382
276
  rejection.name = error.code;
@@ -384,44 +278,6 @@ export class ReverseLeaseRuntime {
384
278
  } else request.resolve(result);
385
279
  }
386
280
 
387
- #isTerminalOutputEnvelope(value: unknown, id: string, connectionId: string, leaseId: string): boolean {
388
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
389
- const frame = value as Record<string, unknown>;
390
- if (!Object.keys(frame).every(key => ["type", "id", "connectionId", "leaseId", "ok", "result"].includes(key)))
391
- return false;
392
- if (
393
- frame.type !== "reverse_response" ||
394
- frame.id !== id ||
395
- frame.connectionId !== connectionId ||
396
- frame.leaseId !== leaseId ||
397
- frame.ok !== true
398
- )
399
- return false;
400
- const response = frame.result;
401
- if (!response || typeof response !== "object" || Array.isArray(response)) return false;
402
- const resultRecord = response as Record<string, unknown>;
403
- if (
404
- !Object.keys(resultRecord).every(key => ["output", "truncated", "exitStatus"].includes(key)) ||
405
- typeof resultRecord.output !== "string" ||
406
- typeof resultRecord.truncated !== "boolean"
407
- )
408
- return false;
409
- const exitStatus = resultRecord.exitStatus;
410
- if (exitStatus === undefined || exitStatus === null) return true;
411
- if (typeof exitStatus !== "object" || Array.isArray(exitStatus)) return false;
412
- const status = exitStatus as Record<string, unknown>;
413
- if (!Object.keys(status).every(key => ["exitCode", "signal"].includes(key))) return false;
414
- const exitCodeValid =
415
- status.exitCode === undefined ||
416
- status.exitCode === null ||
417
- (typeof status.exitCode === "number" && Number.isSafeInteger(status.exitCode));
418
- const signalValid =
419
- status.signal === undefined ||
420
- status.signal === null ||
421
- (typeof status.signal === "string" && Buffer.byteLength(status.signal, "utf-8") <= 128);
422
- return exitCodeValid && signalValid;
423
- }
424
-
425
281
  getLease(capability: string): ProviderLease | undefined {
426
282
  const lease = this.#liveLease(capability);
427
283
  return lease && { ...lease };
@@ -437,20 +293,20 @@ export class ReverseLeaseRuntime {
437
293
  this.#disposing = true;
438
294
  clearInterval(this.#sweepTimer);
439
295
  const outstanding = [...this.#outstanding.entries()];
440
- const installedCapabilities = [...this.#installedCapabilities.entries()];
441
- for (const [id] of outstanding) this.#takeOutstanding(id);
296
+ const installedCapabilities = [...this.#installedCapabilities];
297
+ this.#outstanding.clear();
442
298
  this.#installedCapabilities.clear();
443
299
  this.#leases.clear();
444
300
  this.#idempotency.clear();
445
- this.#idempotencyOrder.length = 0;
301
+ for (const request of outstanding.map(([, request]) => request))
302
+ if (request.signal && request.onAbort) request.signal.removeEventListener("abort", request.onAbort);
446
303
  for (const [id, request] of outstanding) {
447
- this.#sendCancellation(id, request);
448
304
  request.reject(new Error("request_cancelled"));
449
305
  try {
450
306
  this.#onCancel?.(id, "lease_released");
451
307
  } catch {}
452
308
  }
453
- for (const [capability] of installedCapabilities) {
309
+ for (const capability of installedCapabilities) {
454
310
  try {
455
311
  this.#onDefinitionsRemoved?.(capability);
456
312
  } catch {}
@@ -466,55 +322,22 @@ export class ReverseLeaseRuntime {
466
322
  #liveLease(capability: string): ProviderLease | undefined {
467
323
  const lease = this.#leases.get(capability);
468
324
  if (!lease?.active || lease.expiresAt <= this.#now()) {
469
- if (lease?.expiresAt !== undefined && lease.expiresAt <= this.#now()) {
470
- this.#removeDefinitions(capability, lease.leaseId);
471
- this.#cancelForLease(lease.capability, lease.leaseId);
472
- this.#evictIdempotencyForLease(lease.leaseId);
473
- }
325
+ if (lease?.expiresAt !== undefined && lease.expiresAt <= this.#now()) this.#removeDefinitions(capability);
474
326
  return undefined;
475
327
  }
476
-
477
328
  return lease;
478
329
  }
479
330
  #expireStaleLeases(): void {
480
- for (const lease of this.#leases.values()) {
481
- if (lease.expiresAt > this.#now()) continue;
482
- this.#removeDefinitions(lease.capability, lease.leaseId);
483
- this.#cancelForLease(lease.capability, lease.leaseId);
484
- this.#evictIdempotencyForLease(lease.leaseId);
485
- }
486
- }
487
- #sendCancellation(id: string, request: Outstanding): void {
488
- queueMicrotask(() => {
489
- try {
490
- const cancellation = this.#sendFrame(request.connectionId, {
491
- type: "reverse_cancel",
492
- id,
493
- connectionId: request.connectionId,
494
- leaseId: request.leaseId,
495
- });
496
- void Promise.resolve(cancellation).catch(() => {});
497
- } catch {
498
- // Cancellation is best effort; local retirement remains authoritative.
499
- }
500
- });
501
- }
502
- #cancelForLease(capability: string, leaseId: string): void {
503
- for (const [id, request] of this.#outstanding) {
504
- if (request.capability !== capability || request.leaseId !== leaseId) continue;
505
- if (this.#takeOutstanding(id) !== request) continue;
506
- this.#sendCancellation(id, request);
507
- request.reject(new Error("request_cancelled"));
508
- }
331
+ for (const lease of this.#leases.values())
332
+ if (lease.expiresAt <= this.#now()) this.#removeDefinitions(lease.capability);
509
333
  }
510
334
  #cancelForConnection(connectionId: string, reason: "provider_disconnected" | "lease_released"): void {
511
- for (const [id, request] of this.#outstanding) {
512
- if (request.connectionId !== connectionId) continue;
513
- if (this.#takeOutstanding(id) !== request) continue;
514
- this.#sendCancellation(id, request);
515
- request.reject(new Error("request_cancelled"));
516
- this.#onCancel?.(id, reason);
517
- }
335
+ for (const [id, request] of this.#outstanding)
336
+ if (request.connectionId === connectionId) {
337
+ this.#takeOutstanding(id);
338
+ request.reject(new Error("request_cancelled"));
339
+ this.#onCancel?.(id, reason);
340
+ }
518
341
  }
519
342
  #takeOutstanding(id: string): Outstanding | undefined {
520
343
  const request = this.#outstanding.get(id);
@@ -523,47 +346,17 @@ export class ReverseLeaseRuntime {
523
346
  if (request.signal && request.onAbort) request.signal.removeEventListener("abort", request.onAbort);
524
347
  return request;
525
348
  }
526
- #settleOutstandingWithFailure(id: string, request: Outstanding, failure: Error, sendCancellation = false): void {
527
- if (this.#takeOutstanding(id) !== request) return;
528
- if (sendCancellation) this.#sendCancellation(id, request);
529
- request.reject(failure);
349
+ #installDefinitionsFor(capability: string, definitions: unknown): void {
350
+ this.#installDefinitions?.(capability, definitions);
351
+ this.#installedCapabilities.add(capability);
530
352
  }
531
- #installDefinitionsFor(
532
- capability: string,
533
- definitions: unknown,
534
- leaseId: string,
535
- connectionId: string,
536
- connectionGeneration: number,
537
- ): void {
538
- this.#installDefinitions?.(capability, definitions, leaseId, connectionId, connectionGeneration);
539
- this.#installedCapabilities.set(capability, leaseId);
540
- }
541
- #removeDefinitions(capability: string, leaseId?: string): void {
542
- const installedLeaseId = this.#installedCapabilities.get(capability);
543
- if (installedLeaseId === undefined) return;
544
- if (leaseId !== undefined && installedLeaseId !== leaseId) return;
545
- this.#installedCapabilities.delete(capability);
353
+ #removeDefinitions(capability: string): void {
354
+ if (!this.#installedCapabilities.delete(capability)) return;
546
355
  this.#onDefinitionsRemoved?.(capability);
547
356
  }
548
-
549
- #rememberIdempotency(key: string, fingerprint: string, lease: ProviderLease): void {
550
- this.#idempotency.set(key, { fingerprint, lease });
551
- this.#idempotencyOrder.push(key);
552
- while (this.#idempotencyOrder.length > MAX_REVERSE_IDEMPOTENCY_ENTRIES) {
553
- const evicted = this.#idempotencyOrder.shift();
554
- if (evicted !== undefined) this.#idempotency.delete(evicted);
555
- }
556
- }
557
- #evictIdempotencyForLease(leaseId: string): void {
558
- for (const key of this.#idempotencyOrder) {
559
- if (this.#idempotency.get(key)?.lease.leaseId === leaseId) this.#idempotency.delete(key);
560
- }
561
- while (this.#idempotencyOrder.length > 0 && !this.#idempotency.has(this.#idempotencyOrder[0]!))
562
- this.#idempotencyOrder.shift();
563
- }
564
- #assertPayload(payload: unknown, maxBytes = MAX_REVERSE_PAYLOAD_BYTES): void {
357
+ #assertPayload(payload: unknown): void {
565
358
  const encoded = JSON.stringify(payload);
566
- if (encoded !== undefined && Buffer.byteLength(encoded) > maxBytes)
359
+ if (encoded !== undefined && Buffer.byteLength(encoded) > MAX_REVERSE_PAYLOAD_BYTES)
567
360
  throw new ReverseLeaseError("payload_too_large");
568
361
  }
569
362
  }
package/src/sdk/index.ts CHANGED
@@ -22,5 +22,6 @@ export type {
22
22
  Q10ThinkingMode,
23
23
  } from "./models";
24
24
  export * from "./prompt-status";
25
+ export type { ActiveProviderConnectionKind, ActiveProviderDescriptor } from "./providers";
25
26
  export * from "./session";
26
27
  export * from "./session-directory";
@@ -1583,6 +1583,24 @@
1583
1583
  "packages/coding-agent/test/sdk-operation-inventory.test.ts"
1584
1584
  ]
1585
1585
  },
1586
+ {
1587
+ "sourceId": "registry:Q29",
1588
+ "sourceFile": "packages/coding-agent/src/sdk/protocol/operation-registry.ts",
1589
+ "sourceKind": "registry",
1590
+ "decision": "include",
1591
+ "sdkId": "providers.list/active",
1592
+ "adapterMappings": {
1593
+ "telegram": "prohibited",
1594
+ "discord": "prohibited",
1595
+ "slack": "prohibited",
1596
+ "mcp": "generic_safe",
1597
+ "acp": "generic_safe",
1598
+ "daemonCli": "generic_safe"
1599
+ },
1600
+ "testIds": [
1601
+ "packages/coding-agent/test/sdk-operation-inventory.test.ts"
1602
+ ]
1603
+ },
1586
1604
  {
1587
1605
  "sourceId": "registry:R01",
1588
1606
  "sourceFile": "packages/coding-agent/src/sdk/protocol/operation-registry.ts",
@@ -164,6 +164,7 @@ const queries = [
164
164
  "skill.invoke_status",
165
165
  "Read the authoritative reconciliation status of a skill.invoke by command/turn IDs or clientRef.",
166
166
  ],
167
+ ["providers.list/active", "List active providers."],
167
168
  ] as const;
168
169
 
169
170
  const reverse = [
@@ -243,7 +244,7 @@ function queryContinuityClass(id: string): QueryContinuityClass {
243
244
  }
244
245
 
245
246
  function queryDisposition(id: string): Record<Adapter, AdapterDisposition> {
246
- if (["Q23", "Q24", "Q25", "Q26", "Q27", "Q28"].includes(id))
247
+ if (["Q23", "Q24", "Q25", "Q26", "Q27", "Q28", "Q29"].includes(id))
247
248
  return dispositions({ telegram: "prohibited", discord: "prohibited", slack: "prohibited" });
248
249
  return dispositions();
249
250
  }
@@ -306,7 +307,9 @@ export const OPERATIONS: readonly Operation[] = [
306
307
  errorCodes:
307
308
  id === "Q27"
308
309
  ? ["invalid_request", "resource_gone", "model_profile_registry_error"]
309
- : ["invalid_request", "resource_gone"],
310
+ : id === "Q29"
311
+ ? ["invalid_request", "resource_gone", "internal"]
312
+ : ["invalid_request", "resource_gone"],
310
313
  continuityClass: queryContinuityClass(id),
311
314
  adapterDispositions: queryDisposition(id),
312
315
  testIds: ["packages/coding-agent/test/sdk-operation-inventory.test.ts"],
@@ -0,0 +1,51 @@
1
+ export type ActiveProviderConnectionKind = "credential" | "credentialless";
2
+
3
+ export interface ActiveProviderDescriptor {
4
+ provider: string;
5
+ connectionKind: ActiveProviderConnectionKind;
6
+ }
7
+ function compareProviderIdsByUtf8(left: string, right: string): number {
8
+ const leftBytes = new TextEncoder().encode(left);
9
+ const rightBytes = new TextEncoder().encode(right);
10
+ const length = Math.min(leftBytes.length, rightBytes.length);
11
+ for (let index = 0; index < length; index += 1) {
12
+ const difference = leftBytes[index]! - rightBytes[index]!;
13
+ if (difference !== 0) return difference;
14
+ }
15
+ return leftBytes.length - rightBytes.length;
16
+ }
17
+
18
+ /**
19
+ * Project active-provider inputs to the exact public DTO shape.
20
+ *
21
+ * Connection credentials take precedence when the same provider is reported
22
+ * more than once. Provider IDs are sorted by their UTF-8 bytes without
23
+ * normalization.
24
+ */
25
+ export function projectActiveProviderDescriptors(
26
+ descriptors: readonly { provider: string; connectionKind: unknown }[],
27
+ ): ActiveProviderDescriptor[] {
28
+ const active = new Map<string, ActiveProviderConnectionKind>();
29
+ for (const descriptor of descriptors) {
30
+ if (descriptor.connectionKind !== "credential" && descriptor.connectionKind !== "credentialless") {
31
+ throw new Error("Invalid active provider connection kind.");
32
+ }
33
+ if (active.get(descriptor.provider) === "credential") continue;
34
+ active.set(descriptor.provider, descriptor.connectionKind);
35
+ }
36
+ return [...active.entries()]
37
+ .sort(([left], [right]) => compareProviderIdsByUtf8(left, right))
38
+ .map(([provider, connectionKind]) => ({ provider, connectionKind }));
39
+ }
40
+
41
+ export const ACTIVE_PROVIDER_RESOLUTION_ERROR_CODE = "internal" as const;
42
+ export const ACTIVE_PROVIDER_RESOLUTION_ERROR_MESSAGE = "Unable to resolve active providers.";
43
+
44
+ export class ActiveProviderResolutionError extends Error {
45
+ readonly code = ACTIVE_PROVIDER_RESOLUTION_ERROR_CODE;
46
+
47
+ constructor() {
48
+ super(ACTIVE_PROVIDER_RESOLUTION_ERROR_MESSAGE);
49
+ this.name = "ActiveProviderResolutionError";
50
+ }
51
+ }
@@ -39,6 +39,7 @@ export type ResolveManagedSessionScopeResult =
39
39
  | "sessions_root_unavailable"
40
40
  | "binding_conflict"
41
41
  | "binding_invalid"
42
+ | "migration_busy"
42
43
  | "atomic_unavailable"
43
44
  | "durability_not_provable"
44
45
  | "durability_failed"
@@ -108,10 +108,10 @@ import { MCPManager } from "../runtime-mcp";
108
108
  import { createNotificationsExtension } from "../sdk/bus";
109
109
  import {
110
110
  getNotificationConfig,
111
- isNotificationHostEligible,
111
+ isGenericNotificationHostEligible,
112
112
  type NotificationConfig,
113
113
  SPAWN_PROVENANCE_ENV,
114
- shouldRegisterNotificationsExtension,
114
+ shouldRegisterGenericNotificationsExtension,
115
115
  } from "../sdk/bus/config";
116
116
  import { NotificationSessionController } from "../sdk/bus/session-control";
117
117
  import { shouldHostSdk } from "../sdk/host";
@@ -184,6 +184,7 @@ import {
184
184
 
185
185
  type AsyncResultEntry = {
186
186
  jobId: string;
187
+ generation: string;
187
188
  result: string;
188
189
  job: AsyncJob | undefined;
189
190
  durationMs: number | undefined;
@@ -1502,13 +1503,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1502
1503
  ? new AsyncJobManager({
1503
1504
  maxRunningJobs: asyncMaxJobs,
1504
1505
  onJobComplete: async (jobId, result, job) => {
1505
- if (!session || asyncJobManager!.isDeliverySuppressed(jobId)) return;
1506
+ if (!session) return;
1506
1507
  const formattedResult = await formatAsyncResultForFollowUp(result);
1507
- if (asyncJobManager!.isDeliverySuppressed(jobId)) return;
1508
+ if (asyncJobManager!.isDeliverySuppressed(jobId, job?.generation)) return;
1508
1509
 
1509
1510
  const durationMs = job ? jobElapsedMs(job) : undefined;
1510
1511
  session.yieldQueue.enqueue<AsyncResultEntry>("async-result", {
1511
1512
  jobId,
1513
+ generation: job?.generation ?? "",
1512
1514
  result: formattedResult,
1513
1515
  job,
1514
1516
  durationMs,
@@ -1907,7 +1909,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1907
1909
  const spawnProvenance = process.env[SPAWN_PROVENANCE_ENV];
1908
1910
  const spawnedByGjc = typeof spawnProvenance === "string" && spawnProvenance.trim().length > 0;
1909
1911
  delete process.env[SPAWN_PROVENANCE_ENV];
1910
- const notificationHostEligible = isNotificationHostEligible({
1912
+ const notificationHostEligible = isGenericNotificationHostEligible({
1911
1913
  env: process.env,
1912
1914
  hostModeSupported: options.notificationHostModeSupported ?? true,
1913
1915
  taskDepth,
@@ -1919,10 +1921,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1919
1921
  const notificationSessionController = new NotificationSessionController({
1920
1922
  eligible: notificationHostEligible,
1921
1923
  getConfig: () => getNotificationConfig(settings),
1924
+ spawnedByGjc,
1922
1925
  });
1923
1926
  if (
1924
1927
  lifecycleStartupCapability ||
1925
- shouldRegisterNotificationsExtension({
1928
+ shouldRegisterGenericNotificationsExtension({
1926
1929
  env: process.env,
1927
1930
  cfg: notificationCfg,
1928
1931
  taskDepth,
@@ -2797,7 +2800,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2797
2800
  hasSession = true;
2798
2801
  if (asyncJobManager) {
2799
2802
  session.yieldQueue.register<AsyncResultEntry>("async-result", {
2800
- isStale: entry => asyncJobManager.isDeliverySuppressed(entry.jobId),
2803
+ isStale: entry => asyncJobManager.isDeliverySuppressed(entry.jobId, entry.generation),
2801
2804
  build: buildAsyncResultBatchMessage,
2802
2805
  });
2803
2806
  }