@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
@@ -5,7 +5,12 @@ 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;
8
9
  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;
9
14
 
10
15
  export class ReverseLeaseError extends Error {
11
16
  constructor(
@@ -39,6 +44,7 @@ interface Outstanding {
39
44
  connectionId: string;
40
45
  capability: string;
41
46
  leaseId: string;
47
+ method: string;
42
48
  resolve: (value: unknown) => void;
43
49
  reject: (reason: Error) => void;
44
50
  signal?: AbortSignal;
@@ -49,7 +55,13 @@ export interface ReverseLeaseOptions {
49
55
  now?: () => number;
50
56
  leaseTtlMs?: number;
51
57
  sendFrame: (connectionId: string, frame: SdkFrame) => void | Promise<void>;
52
- installDefinitions?: (capability: string, definitions: unknown) => void;
58
+ installDefinitions?: (
59
+ capability: string,
60
+ definitions: unknown,
61
+ leaseId: string,
62
+ connectionId: string,
63
+ connectionGeneration: number,
64
+ ) => void;
53
65
  onCancel?: (requestId: string, reason: "provider_disconnected" | "lease_released") => void;
54
66
  onDefinitionsRemoved?: (capability: string) => void;
55
67
  }
@@ -81,8 +93,10 @@ export class ReverseLeaseRuntime {
81
93
  readonly #onCancel?: ReverseLeaseOptions["onCancel"];
82
94
  readonly #leases = new Map<string, ProviderLease>();
83
95
  readonly #idempotency = new Map<string, { fingerprint: string; lease: ProviderLease }>();
96
+ readonly #idempotencyOrder: string[] = [];
97
+
84
98
  readonly #outstanding = new Map<string, Outstanding>();
85
- readonly #installedCapabilities = new Set<string>();
99
+ readonly #installedCapabilities = new Map<string, string>();
86
100
  readonly #sweepTimer: ReturnType<typeof setInterval>;
87
101
  #disposing = false;
88
102
 
@@ -103,11 +117,15 @@ export class ReverseLeaseRuntime {
103
117
  definitions: unknown,
104
118
  expectedLeaseId?: string,
105
119
  idempotencyKey?: string,
120
+ connectionGeneration = 0,
106
121
  ): ProviderLease {
107
122
  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");
108
125
  const key = `${connectionId}\u0000${idempotencyKey ?? ""}`;
109
126
  const fingerprint = registrationFingerprint(capability, definitions, expectedLeaseId);
110
127
  const replay = idempotencyKey ? this.#idempotency.get(key) : undefined;
128
+
111
129
  if (replay) {
112
130
  if (replay.fingerprint !== fingerprint) throw new ReverseLeaseError("idempotency_conflict");
113
131
  const current = this.#leases.get(capability);
@@ -115,12 +133,19 @@ export class ReverseLeaseRuntime {
115
133
  }
116
134
  const now = this.#now();
117
135
  const existing = this.#leases.get(capability);
118
- if (existing && existing.expiresAt <= now) this.#removeDefinitions(existing.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
+ }
119
145
  const pendingHandoff = existing?.active === false && existing.expiresAt > now;
120
146
  if (pendingHandoff) {
121
147
  if (existing!.connectionId !== connectionId || existing!.leaseId !== expectedLeaseId)
122
148
  throw new ReverseLeaseError("provider_lease_conflict");
123
- this.#installDefinitionsFor(capability, definitions);
124
149
  const lease: ProviderLease = {
125
150
  leaseId: existing!.leaseId,
126
151
  connectionId,
@@ -129,17 +154,22 @@ export class ReverseLeaseRuntime {
129
154
  expiresAt: now + this.#leaseTtlMs,
130
155
  active: true,
131
156
  };
157
+ this.#installDefinitionsFor(capability, definitions, lease.leaseId, lease.connectionId, connectionGeneration);
132
158
  this.#leases.set(capability, lease);
133
- if (idempotencyKey) this.#idempotency.set(key, { fingerprint, lease });
159
+ if (idempotencyKey) this.#rememberIdempotency(key, fingerprint, lease);
160
+
134
161
  return { ...lease };
135
162
  }
136
163
  const reclaiming =
137
164
  existing?.leaseId === expectedLeaseId && existing?.graceUntil !== undefined && now <= existing.graceUntil;
138
165
  const refreshing =
139
- existing?.active !== false && existing?.connectionId === connectionId && existing.expiresAt > now;
166
+ existing?.active !== false &&
167
+ existing?.connectionId === connectionId &&
168
+ existing.expiresAt > now &&
169
+ (expectedLeaseId === undefined || existing.leaseId === expectedLeaseId);
170
+
140
171
  if (existing && !reclaiming && !refreshing && existing.connectionId !== connectionId && existing.expiresAt > now)
141
172
  throw new ReverseLeaseError("provider_lease_conflict");
142
- this.#installDefinitionsFor(capability, definitions);
143
173
  const lease: ProviderLease = {
144
174
  leaseId: reclaiming || refreshing ? existing!.leaseId : randomUUID(),
145
175
  connectionId,
@@ -148,17 +178,22 @@ export class ReverseLeaseRuntime {
148
178
  expiresAt: now + this.#leaseTtlMs,
149
179
  active: true,
150
180
  };
181
+ this.#installDefinitionsFor(capability, definitions, lease.leaseId, lease.connectionId, connectionGeneration);
151
182
  this.#leases.set(capability, lease);
152
- if (idempotencyKey) this.#idempotency.set(key, { fingerprint, lease });
183
+ if (idempotencyKey) this.#rememberIdempotency(key, fingerprint, lease);
184
+
153
185
  return { ...lease };
154
186
  }
155
187
 
156
188
  heartbeat(connectionId: string, leaseId: string): ProviderLease {
157
189
  const lease = this.#owner(connectionId, leaseId);
158
190
  if (lease.expiresAt <= this.#now()) {
159
- this.#removeDefinitions(lease.capability);
191
+ this.#cancelForLease(lease.capability, lease.leaseId);
192
+ this.#removeDefinitions(lease.capability, lease.leaseId);
193
+ this.#evictIdempotencyForLease(lease.leaseId);
160
194
  throw new ReverseLeaseError("lease_expired");
161
195
  }
196
+
162
197
  lease.expiresAt = this.#now() + this.#leaseTtlMs;
163
198
  lease.graceUntil = undefined;
164
199
  return { ...lease };
@@ -166,8 +201,14 @@ export class ReverseLeaseRuntime {
166
201
 
167
202
  release(connectionId: string, leaseId: string, handoffTo?: string): ProviderLease {
168
203
  const lease = this.#owner(connectionId, leaseId);
169
- this.#cancelForConnection(connectionId, "lease_released");
170
- this.#removeDefinitions(lease.capability);
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);
171
212
  if (handoffTo) {
172
213
  lease.connectionId = handoffTo;
173
214
  lease.expiresAt = this.#now() + REVERSE_RECLAIM_GRACE_MS;
@@ -175,22 +216,32 @@ export class ReverseLeaseRuntime {
175
216
  lease.active = false;
176
217
  return { ...lease };
177
218
  }
219
+ this.#evictIdempotencyForLease(lease.leaseId);
178
220
  this.#leases.delete(lease.capability);
179
221
  return { ...lease };
180
222
  }
181
223
 
182
224
  disconnect(connectionId: string): void {
183
225
  const now = this.#now();
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
- }
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
+ }
190
233
  this.#cancelForConnection(connectionId, "provider_disconnected");
191
234
  }
192
235
 
193
- request(capability: string, method: string, payload: unknown, signal?: AbortSignal): Promise<unknown> {
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> {
194
245
  if (this.#disposing) throw new Error("reverse runtime is disposing");
195
246
  this.#assertPayload(payload);
196
247
  const lease = this.#liveLease(capability);
@@ -200,14 +251,24 @@ export class ReverseLeaseRuntime {
200
251
  throw new ReverseLeaseError("lease_unavailable");
201
252
  throw new ReverseLeaseError("provider_required");
202
253
  }
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
+ }
203
260
  if (signal?.aborted)
204
261
  return Promise.reject(Object.assign(new Error("request_cancelled"), { name: "request_cancelled" }));
205
- if (this.#outstanding.size >= MAX_REVERSE_OUTSTANDING) throw new ReverseLeaseError("too_many_outstanding");
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");
206
266
  const id = randomUUID();
207
267
  return new Promise((resolve, reject) => {
208
268
  const outstanding: Outstanding = {
209
269
  connectionId: lease.connectionId,
210
270
  capability,
271
+ method,
211
272
  leaseId: lease.leaseId,
212
273
  resolve,
213
274
  reject,
@@ -264,13 +325,58 @@ export class ReverseLeaseRuntime {
264
325
  leaseId: string,
265
326
  result: unknown,
266
327
  error?: { code: string; message: string },
328
+ wireEnvelope?: unknown,
267
329
  ): void {
268
- this.#assertPayload(result);
269
330
  const request = this.#outstanding.get(id);
270
331
  if (!request) throw new ReverseLeaseError("unknown_request");
271
332
  if (request.connectionId !== connectionId || request.leaseId !== leaseId)
272
333
  throw new ReverseLeaseError("not_lease_owner");
273
- this.#takeOutstanding(id);
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;
274
380
  if (error) {
275
381
  const rejection = new Error(error.message);
276
382
  rejection.name = error.code;
@@ -278,6 +384,44 @@ export class ReverseLeaseRuntime {
278
384
  } else request.resolve(result);
279
385
  }
280
386
 
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
+
281
425
  getLease(capability: string): ProviderLease | undefined {
282
426
  const lease = this.#liveLease(capability);
283
427
  return lease && { ...lease };
@@ -293,20 +437,20 @@ export class ReverseLeaseRuntime {
293
437
  this.#disposing = true;
294
438
  clearInterval(this.#sweepTimer);
295
439
  const outstanding = [...this.#outstanding.entries()];
296
- const installedCapabilities = [...this.#installedCapabilities];
297
- this.#outstanding.clear();
440
+ const installedCapabilities = [...this.#installedCapabilities.entries()];
441
+ for (const [id] of outstanding) this.#takeOutstanding(id);
298
442
  this.#installedCapabilities.clear();
299
443
  this.#leases.clear();
300
444
  this.#idempotency.clear();
301
- for (const request of outstanding.map(([, request]) => request))
302
- if (request.signal && request.onAbort) request.signal.removeEventListener("abort", request.onAbort);
445
+ this.#idempotencyOrder.length = 0;
303
446
  for (const [id, request] of outstanding) {
447
+ this.#sendCancellation(id, request);
304
448
  request.reject(new Error("request_cancelled"));
305
449
  try {
306
450
  this.#onCancel?.(id, "lease_released");
307
451
  } catch {}
308
452
  }
309
- for (const capability of installedCapabilities) {
453
+ for (const [capability] of installedCapabilities) {
310
454
  try {
311
455
  this.#onDefinitionsRemoved?.(capability);
312
456
  } catch {}
@@ -322,22 +466,55 @@ export class ReverseLeaseRuntime {
322
466
  #liveLease(capability: string): ProviderLease | undefined {
323
467
  const lease = this.#leases.get(capability);
324
468
  if (!lease?.active || lease.expiresAt <= this.#now()) {
325
- if (lease?.expiresAt !== undefined && lease.expiresAt <= this.#now()) this.#removeDefinitions(capability);
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
+ }
326
474
  return undefined;
327
475
  }
476
+
328
477
  return lease;
329
478
  }
330
479
  #expireStaleLeases(): void {
331
- for (const lease of this.#leases.values())
332
- if (lease.expiresAt <= this.#now()) this.#removeDefinitions(lease.capability);
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
+ }
333
486
  }
334
- #cancelForConnection(connectionId: string, reason: "provider_disconnected" | "lease_released"): void {
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);
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.
340
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
+ }
509
+ }
510
+ #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
+ }
341
518
  }
342
519
  #takeOutstanding(id: string): Outstanding | undefined {
343
520
  const request = this.#outstanding.get(id);
@@ -346,17 +523,47 @@ export class ReverseLeaseRuntime {
346
523
  if (request.signal && request.onAbort) request.signal.removeEventListener("abort", request.onAbort);
347
524
  return request;
348
525
  }
349
- #installDefinitionsFor(capability: string, definitions: unknown): void {
350
- this.#installDefinitions?.(capability, definitions);
351
- this.#installedCapabilities.add(capability);
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);
352
530
  }
353
- #removeDefinitions(capability: string): void {
354
- if (!this.#installedCapabilities.delete(capability)) return;
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);
355
546
  this.#onDefinitionsRemoved?.(capability);
356
547
  }
357
- #assertPayload(payload: unknown): void {
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 {
358
565
  const encoded = JSON.stringify(payload);
359
- if (encoded !== undefined && Buffer.byteLength(encoded) > MAX_REVERSE_PAYLOAD_BYTES)
566
+ if (encoded !== undefined && Buffer.byteLength(encoded) > maxBytes)
360
567
  throw new ReverseLeaseError("payload_too_large");
361
568
  }
362
569
  }
@@ -11,6 +11,7 @@ import {
11
11
  } from "@gajae-code/agent-core";
12
12
  import {
13
13
  type AssistantMessage,
14
+ type AttemptScopeRef,
14
15
  type AuthCredentialSelector,
15
16
  type CredentialDisabledEvent,
16
17
  type Message,
@@ -2538,21 +2539,21 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2538
2539
  if (!obfuscator?.hasSecrets()) return converted;
2539
2540
  return obfuscateMessages(obfuscator, converted);
2540
2541
  };
2541
- const transformContext = async (messages: AgentMessage[], _signal?: AbortSignal) => {
2542
+ const transformContext = async (messages: AgentMessage[], _signal?: AbortSignal, scope?: AttemptScopeRef) => {
2542
2543
  // External Agent events dispatch listeners without awaiting them. The
2543
2544
  // session-owned barrier makes any pre-admission artifact transformation
2544
2545
  // visible before this provider context is normalized.
2545
2546
  await session?.awaitPendingContextTransformations();
2546
- return extensionRunner ? await extensionRunner.emitContext(messages) : messages;
2547
+ return extensionRunner ? await extensionRunner.emitContext(messages, scope) : messages;
2547
2548
  };
2548
2549
  const onPayload = extensionRunner
2549
- ? async (payload: unknown, _model?: Model) => {
2550
- return await extensionRunner.emitBeforeProviderRequest(payload);
2550
+ ? async (payload: unknown, _model?: Model, scope?: AttemptScopeRef) => {
2551
+ return await extensionRunner.emitBeforeProviderRequest(payload, scope);
2551
2552
  }
2552
2553
  : undefined;
2553
2554
  const onResponse: SimpleStreamOptions["onResponse"] | undefined = extensionRunner
2554
- ? async (response, model) => {
2555
- await extensionRunner.emitAfterProviderResponse(response, model);
2555
+ ? async (response, model, scope) => {
2556
+ await extensionRunner.emitAfterProviderResponse(response, model, scope);
2556
2557
  }
2557
2558
  : undefined;
2558
2559