@gajae-code/coding-agent 0.15.2 → 0.15.4
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.
- package/CHANGELOG.md +43 -1
- package/dist/types/config/autorouting-tier-map.d.ts +0 -9
- package/dist/types/config/file-lock.d.ts +7 -1
- package/dist/types/config/keybindings.d.ts +13 -3
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +65 -1
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +42 -2
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
- package/dist/types/gjc-runtime/workflow-placeholder.d.ts +6 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/modes/action-registry.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +16 -1
- package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +42 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/shared/oauth-url-copy.d.ts +9 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/ui-language.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/broker/session-index.d.ts +18 -0
- package/dist/types/sdk/bus/index.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/cli/session-cli.d.ts +1 -1
- package/dist/types/sdk/lifecycle/service.d.ts +32 -5
- package/dist/types/sdk/session.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +4 -0
- package/dist/types/session/fallback-chain-controller.d.ts +8 -0
- package/dist/types/slash-commands/helpers/aside.d.ts +54 -0
- package/dist/types/tools/ask.d.ts +9 -0
- package/dist/types/utils/clipboard.d.ts +9 -1
- package/package.json +7 -7
- package/scripts/generate-sdk-adapter-parity-manifest.ts +5 -2
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/commands/sdk.ts +2 -2
- package/src/config/autorouting-tier-map.ts +0 -9
- package/src/config/file-lock.ts +15 -1
- package/src/config/keybindings.ts +13 -3
- package/src/config/model-registry.ts +132 -21
- package/src/config/model-resolver.ts +2 -1
- package/src/config/models-config-schema.ts +2 -2
- package/src/config/settings-schema.ts +66 -1
- package/src/config/settings.ts +1 -1
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +235 -4
- package/src/coordinator-mcp/server.ts +402 -2
- package/src/dap/client.ts +19 -8
- package/src/deep-interview/render-middleware.ts +6 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/deep-interview-stage.ts +20 -1
- package/src/gjc-runtime/deep-interview-state.ts +3 -1
- package/src/gjc-runtime/ralplan-runtime.ts +6 -1
- package/src/gjc-runtime/session-state-lock.ts +11 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +5 -0
- package/src/gjc-runtime/workflow-placeholder.ts +33 -0
- package/src/internal-urls/docs-index.generated.ts +11 -9
- package/src/lsp/client.ts +39 -4
- package/src/main.ts +42 -11
- package/src/modes/action-registry.ts +7 -0
- package/src/modes/components/hook-selector.ts +2 -1
- package/src/modes/components/iterm-pet-transport.ts +4 -2
- package/src/modes/components/runtime-mcp-add-wizard.ts +38 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/tool-status-header.ts +149 -41
- package/src/modes/components/welcome.ts +8 -0
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +141 -7
- package/src/modes/controllers/plan-mode-controller.ts +12 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +164 -20
- package/src/modes/controllers/selector-controller.ts +11 -2
- package/src/modes/interactive-mode.ts +88 -1
- package/src/modes/shared/oauth-url-copy.ts +33 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/ui-language.ts +44 -3
- package/src/runtime-mcp/oauth-flow.ts +19 -8
- package/src/runtime-mcp/transports/stdio.ts +27 -13
- package/src/sdk/acp/adapter.ts +29 -2
- package/src/sdk/broker/broker.ts +26 -6
- package/src/sdk/broker/lifecycle-ledger.ts +28 -1
- package/src/sdk/broker/lifecycle.ts +664 -5
- package/src/sdk/broker/session-index.ts +246 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/index.ts +135 -38
- package/src/sdk/bus/natives-augment.d.ts +20 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +3 -0
- package/src/sdk/cli/session-cli.ts +34 -7
- package/src/sdk/lifecycle/service.ts +166 -11
- package/src/sdk/mcp/server.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +40 -0
- package/src/sdk/protocol/operation-registry.ts +2 -1
- package/src/sdk/session.ts +91 -7
- package/src/session/agent-session.ts +473 -66
- package/src/session/fallback-chain-controller.ts +17 -0
- package/src/setup/frictionless-onboarding.ts +4 -4
- package/src/slash-commands/aside-command.test.ts +366 -0
- package/src/slash-commands/builtin-registry.ts +19 -0
- package/src/slash-commands/helpers/aside.ts +299 -0
- package/src/tools/ask-contract.ts +54 -3
- package/src/tools/ask.ts +54 -6
- package/src/utils/clipboard.ts +37 -28
|
@@ -192,9 +192,11 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
|
|
195
|
+
this.ctrl.signal?.throwIfAborted();
|
|
195
196
|
if (!this.#resolvedClientId) {
|
|
196
|
-
await this.#tryRegisterClient(redirectUri);
|
|
197
|
+
await this.#tryRegisterClient(redirectUri, this.ctrl.signal);
|
|
197
198
|
}
|
|
199
|
+
this.ctrl.signal?.throwIfAborted();
|
|
198
200
|
|
|
199
201
|
const authUrl = new URL(this.config.authorizationUrl);
|
|
200
202
|
const params = authUrl.searchParams;
|
|
@@ -226,13 +228,14 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
226
228
|
this.#codeVerifier = codeVerifier;
|
|
227
229
|
|
|
228
230
|
if (!params.get("client_id")) {
|
|
229
|
-
await this.#assertClientIdNotRequired(authUrl.toString());
|
|
231
|
+
await this.#assertClientIdNotRequired(authUrl.toString(), this.ctrl.signal);
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
return { url: authUrl.toString() };
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
|
|
238
|
+
this.ctrl.signal?.throwIfAborted();
|
|
236
239
|
const params = new URLSearchParams({
|
|
237
240
|
grant_type: "authorization_code",
|
|
238
241
|
code,
|
|
@@ -264,7 +267,9 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
264
267
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
265
268
|
},
|
|
266
269
|
body: params.toString(),
|
|
270
|
+
signal: this.ctrl.signal,
|
|
267
271
|
});
|
|
272
|
+
this.ctrl.signal?.throwIfAborted();
|
|
268
273
|
|
|
269
274
|
if (!response.ok) {
|
|
270
275
|
const errorText = await response.text();
|
|
@@ -330,8 +335,8 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
330
335
|
/**
|
|
331
336
|
* Try OAuth dynamic client registration when provider requires a client_id.
|
|
332
337
|
*/
|
|
333
|
-
async #tryRegisterClient(redirectUri: string): Promise<void> {
|
|
334
|
-
const registrationEndpoint = await this.#resolveRegistrationEndpoint();
|
|
338
|
+
async #tryRegisterClient(redirectUri: string, signal?: AbortSignal): Promise<void> {
|
|
339
|
+
const registrationEndpoint = await this.#resolveRegistrationEndpoint(signal);
|
|
335
340
|
if (!registrationEndpoint) return;
|
|
336
341
|
|
|
337
342
|
try {
|
|
@@ -349,6 +354,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
349
354
|
token_endpoint_auth_method: "none",
|
|
350
355
|
application_type: "native",
|
|
351
356
|
}),
|
|
357
|
+
signal,
|
|
352
358
|
});
|
|
353
359
|
|
|
354
360
|
if (!response.ok) return;
|
|
@@ -364,18 +370,20 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
364
370
|
if (data.client_secret && data.client_secret.trim() !== "") {
|
|
365
371
|
this.#registeredClientSecret = data.client_secret;
|
|
366
372
|
}
|
|
367
|
-
} catch {
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (signal?.aborted) throw error;
|
|
368
375
|
// Ignore registration failures and continue without client registration.
|
|
369
376
|
}
|
|
370
377
|
}
|
|
371
378
|
|
|
372
|
-
async #resolveRegistrationEndpoint(): Promise<string | null> {
|
|
379
|
+
async #resolveRegistrationEndpoint(signal?: AbortSignal): Promise<string | null> {
|
|
373
380
|
try {
|
|
374
381
|
const authorizationEndpoint = new URL(this.config.authorizationUrl);
|
|
375
382
|
const metadataUrl = new URL("/.well-known/oauth-authorization-server", authorizationEndpoint.origin);
|
|
376
383
|
const response = await fetch(metadataUrl.toString(), {
|
|
377
384
|
method: "GET",
|
|
378
385
|
headers: { Accept: "application/json" },
|
|
386
|
+
signal,
|
|
379
387
|
});
|
|
380
388
|
|
|
381
389
|
if (!response.ok) return null;
|
|
@@ -383,19 +391,21 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
383
391
|
if (metadata.registration_endpoint && metadata.registration_endpoint.trim() !== "") {
|
|
384
392
|
return metadata.registration_endpoint;
|
|
385
393
|
}
|
|
386
|
-
} catch {
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (signal?.aborted) throw error;
|
|
387
396
|
// Ignore metadata discovery failures.
|
|
388
397
|
}
|
|
389
398
|
|
|
390
399
|
return null;
|
|
391
400
|
}
|
|
392
401
|
|
|
393
|
-
async #assertClientIdNotRequired(authorizationUrl: string): Promise<void> {
|
|
402
|
+
async #assertClientIdNotRequired(authorizationUrl: string, signal?: AbortSignal): Promise<void> {
|
|
394
403
|
try {
|
|
395
404
|
const response = await fetch(authorizationUrl, {
|
|
396
405
|
method: "GET",
|
|
397
406
|
redirect: "manual",
|
|
398
407
|
headers: { Accept: "text/plain,text/html,application/json" },
|
|
408
|
+
signal,
|
|
399
409
|
});
|
|
400
410
|
if (response.status < 400) return;
|
|
401
411
|
const body = await response.text();
|
|
@@ -403,6 +413,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
403
413
|
throw new Error("OAuth provider requires client_id");
|
|
404
414
|
}
|
|
405
415
|
} catch (error) {
|
|
416
|
+
if (signal?.aborted) throw error;
|
|
406
417
|
if (error instanceof Error && /client[_-]?id/i.test(error.message)) {
|
|
407
418
|
throw error;
|
|
408
419
|
}
|
|
@@ -239,8 +239,15 @@ export class StdioTransport implements MCPTransport {
|
|
|
239
239
|
const response = error
|
|
240
240
|
? { jsonrpc: "2.0" as const, id, error }
|
|
241
241
|
: { jsonrpc: "2.0" as const, id, result: result ?? {} };
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
// FileSink.write()/flush() return `number | Promise<number>` (flush may
|
|
243
|
+
// also return undefined): under backpressure a write returns a promise
|
|
244
|
+
// whose rejection — e.g. EPIPE when the server dies mid-write — must
|
|
245
|
+
// not escape as an unhandled rejection. Best-effort, like the caller.
|
|
246
|
+
Promise.resolve(stdin.write(`${JSON.stringify(response)}\n`))
|
|
247
|
+
.then(() => stdin.flush())
|
|
248
|
+
.catch(() => {
|
|
249
|
+
// Best-effort — process may have exited
|
|
250
|
+
});
|
|
244
251
|
}
|
|
245
252
|
|
|
246
253
|
#handleClose(failure?: MCPExpectedFailure): void {
|
|
@@ -276,6 +283,7 @@ export class StdioTransport implements MCPTransport {
|
|
|
276
283
|
}
|
|
277
284
|
|
|
278
285
|
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
286
|
+
void promise.catch(() => {});
|
|
279
287
|
let timer: NodeJS.Timeout | undefined;
|
|
280
288
|
let settled = false;
|
|
281
289
|
|
|
@@ -319,14 +327,18 @@ export class StdioTransport implements MCPTransport {
|
|
|
319
327
|
}, timeout);
|
|
320
328
|
|
|
321
329
|
const message = `${JSON.stringify(request)}\n`;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
+
void (async () => {
|
|
331
|
+
try {
|
|
332
|
+
// Bun's FileSink has write() method directly. Await both write and
|
|
333
|
+
// flush: under backpressure write() returns a promise, and either
|
|
334
|
+
// call can fail with EPIPE when the server dies mid-write.
|
|
335
|
+
await stdin.write(message);
|
|
336
|
+
await stdin.flush();
|
|
337
|
+
} catch (error: unknown) {
|
|
338
|
+
cleanup();
|
|
339
|
+
reject(new MCPExpectedFailure(error));
|
|
340
|
+
}
|
|
341
|
+
})();
|
|
330
342
|
|
|
331
343
|
return promise;
|
|
332
344
|
}
|
|
@@ -345,9 +357,11 @@ export class StdioTransport implements MCPTransport {
|
|
|
345
357
|
|
|
346
358
|
const message = `${JSON.stringify(notification)}\n`;
|
|
347
359
|
try {
|
|
348
|
-
// Bun's FileSink has write() method directly
|
|
349
|
-
|
|
350
|
-
|
|
360
|
+
// Bun's FileSink has write() method directly. Await both write and
|
|
361
|
+
// flush: under backpressure write() returns a promise, and either
|
|
362
|
+
// call can fail with EPIPE when the server dies mid-write.
|
|
363
|
+
await stdin.write(message);
|
|
364
|
+
await stdin.flush();
|
|
351
365
|
} catch (error) {
|
|
352
366
|
throw new MCPExpectedFailure(error);
|
|
353
367
|
}
|
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -2,9 +2,13 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { logger } from "@gajae-code/utils";
|
|
3
3
|
import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
4
4
|
import { type SdkClient, SdkClientError } from "../client";
|
|
5
|
-
|
|
6
5
|
import type { AbortScope } from "../host/control/operations";
|
|
7
6
|
import { assertReverseResponseFrame, ReverseLeaseError } from "../host/reverse-leases";
|
|
7
|
+
import {
|
|
8
|
+
SessionLifecycleService,
|
|
9
|
+
type SessionReconcileUncertainTarget,
|
|
10
|
+
validateSessionReconcileUncertainTarget,
|
|
11
|
+
} from "../lifecycle/service";
|
|
8
12
|
import { validateAdapterControl, validateAdapterSecretFields } from "../protocol/adapter-validation";
|
|
9
13
|
import { OPERATIONS } from "../protocol/operation-registry";
|
|
10
14
|
import { type SessionAttachment, type SessionRouter, SessionRouterError } from "../router";
|
|
@@ -132,7 +136,8 @@ function isLifecycleOperation(operation: string): boolean {
|
|
|
132
136
|
operation === "session.fork" ||
|
|
133
137
|
operation === "session.resume" ||
|
|
134
138
|
operation === "session.close" ||
|
|
135
|
-
operation === "session.delete"
|
|
139
|
+
operation === "session.delete" ||
|
|
140
|
+
operation === "session.reconcile_uncertain"
|
|
136
141
|
);
|
|
137
142
|
}
|
|
138
143
|
|
|
@@ -449,8 +454,30 @@ export class AcpSdkAdapter {
|
|
|
449
454
|
this.#assertGenericDisposition("global", operation);
|
|
450
455
|
if (isLifecycleOperation(operation) && !idempotencyKey)
|
|
451
456
|
throw new AcpSdkAdapterError("invalid_input", "idempotencyKey is required for lifecycle operations.");
|
|
457
|
+
if (operation === "session.reconcile_uncertain" && !validateSessionReconcileUncertainTarget(input))
|
|
458
|
+
throw new AcpSdkAdapterError(
|
|
459
|
+
"invalid_input",
|
|
460
|
+
"session.reconcile_uncertain requires complete identity-bound retirement proof.",
|
|
461
|
+
);
|
|
452
462
|
if (!this.#client)
|
|
453
463
|
throw new AcpSdkAdapterError("operation_prohibited", "Lifecycle operations require the Broker connection.");
|
|
464
|
+
const client = this.#client;
|
|
465
|
+
if (operation === "session.reconcile_uncertain") {
|
|
466
|
+
const outcome = await new SessionLifecycleService({
|
|
467
|
+
global: (lifecycleOperation, lifecycleInput, options) =>
|
|
468
|
+
client.global(lifecycleOperation, lifecycleInput, options),
|
|
469
|
+
}).executeWithIdempotencyKey(
|
|
470
|
+
{
|
|
471
|
+
operation,
|
|
472
|
+
actor: { id: "acp", namespace: "sdk:acp" },
|
|
473
|
+
capability: operation,
|
|
474
|
+
requestKey: idempotencyKey!,
|
|
475
|
+
target: input as unknown as SessionReconcileUncertainTarget,
|
|
476
|
+
},
|
|
477
|
+
idempotencyKey!,
|
|
478
|
+
);
|
|
479
|
+
return outcome.ok ? { ok: true, result: outcome.result } : { ok: false, error: outcome.error };
|
|
480
|
+
}
|
|
454
481
|
// The broker may hold a startup in its admission queue before the readiness
|
|
455
482
|
// clock even starts, so the caller deadline covers the queue wait too; sizing
|
|
456
483
|
// it on readiness alone times out requests the broker is still running. A
|
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -190,15 +190,27 @@ export type BrokerResponse =
|
|
|
190
190
|
const error = (code: BrokerErrorCode, message: string): BrokerResponse => ({ ok: false, error: { code, message } });
|
|
191
191
|
|
|
192
192
|
function isCleanupPending(response: BrokerResponse): boolean {
|
|
193
|
-
|
|
193
|
+
if (response.ok) return false;
|
|
194
|
+
const error = (response as { error?: unknown }).error;
|
|
195
|
+
if (typeof error !== "object" || error === null) return false;
|
|
196
|
+
const value = error as { code?: unknown; cleanup?: unknown };
|
|
197
|
+
return value.code === "cleanup_pending" && value.cleanup !== undefined;
|
|
194
198
|
}
|
|
195
199
|
|
|
196
200
|
function cleanupFromResponse(response: unknown): BrokerCleanupEvidence | undefined {
|
|
197
|
-
|
|
201
|
+
if (!isBrokerResponse(response) || response.ok) return undefined;
|
|
202
|
+
const error = (response as { error?: unknown }).error;
|
|
203
|
+
if (typeof error !== "object" || error === null) return undefined;
|
|
204
|
+
const cleanup = (error as { cleanup?: unknown }).cleanup;
|
|
205
|
+
return cleanup && typeof cleanup === "object" ? (cleanup as BrokerCleanupEvidence) : undefined;
|
|
198
206
|
}
|
|
199
207
|
function pendingCleanupSessionId(response: BrokerResponse): string | undefined {
|
|
200
|
-
if (response.ok
|
|
201
|
-
|
|
208
|
+
if (response.ok) return undefined;
|
|
209
|
+
const error = (response as { error?: unknown }).error;
|
|
210
|
+
if (typeof error !== "object" || error === null) return undefined;
|
|
211
|
+
const value = error as { code?: unknown; cleanup?: { sessionId?: unknown } };
|
|
212
|
+
if (value.code !== "cleanup_pending") return undefined;
|
|
213
|
+
return typeof value.cleanup?.sessionId === "string" ? value.cleanup.sessionId : undefined;
|
|
202
214
|
}
|
|
203
215
|
|
|
204
216
|
const LIFECYCLE_OPERATIONS = new Set([
|
|
@@ -207,6 +219,7 @@ const LIFECYCLE_OPERATIONS = new Set([
|
|
|
207
219
|
"session.resume",
|
|
208
220
|
"session.close",
|
|
209
221
|
"session.delete",
|
|
222
|
+
"session.reconcile_uncertain",
|
|
210
223
|
]);
|
|
211
224
|
|
|
212
225
|
/** Bootstrap signing material and its public candidate id authorize a launch but are not lifecycle request identity. */
|
|
@@ -225,7 +238,10 @@ function lifecycleFingerprint(operation: string, input: Record<string, unknown>)
|
|
|
225
238
|
function lifecycleResponseState(response: BrokerResponse): LifecycleState {
|
|
226
239
|
if (response.ok) return "terminal_ok";
|
|
227
240
|
if (isCleanupPending(response)) return "effect_started";
|
|
228
|
-
|
|
241
|
+
const error = (response as { error?: unknown }).error;
|
|
242
|
+
return typeof error === "object" && error !== null && (error as { code?: unknown }).code === "terminal_uncertain"
|
|
243
|
+
? "terminal_uncertain"
|
|
244
|
+
: "terminal_error";
|
|
229
245
|
}
|
|
230
246
|
|
|
231
247
|
type InputNormalization = { input: Record<string, unknown> } | BrokerResponse;
|
|
@@ -307,7 +323,8 @@ function normalizeBrokerInput(operation: string, input: Record<string, unknown>)
|
|
|
307
323
|
operation !== "session.fork" &&
|
|
308
324
|
operation !== "session.resume" &&
|
|
309
325
|
operation !== "session.close" &&
|
|
310
|
-
operation !== "session.delete"
|
|
326
|
+
operation !== "session.delete" &&
|
|
327
|
+
operation !== "session.reconcile_uncertain"
|
|
311
328
|
)
|
|
312
329
|
return { input: normalized };
|
|
313
330
|
|
|
@@ -469,6 +486,7 @@ function lifecycleTarget(operation: string, input: Record<string, unknown>): unk
|
|
|
469
486
|
case "session.resume":
|
|
470
487
|
case "session.close":
|
|
471
488
|
case "session.delete":
|
|
489
|
+
case "session.reconcile_uncertain":
|
|
472
490
|
return { sessionId: id };
|
|
473
491
|
default:
|
|
474
492
|
return { operation, root, sessionId: id };
|
|
@@ -1575,6 +1593,8 @@ export class Broker {
|
|
|
1575
1593
|
}
|
|
1576
1594
|
|
|
1577
1595
|
if (!idempotencyKey) return error("invalid_input", "idempotencyKey is required for lifecycle operations");
|
|
1596
|
+
if (idempotencyKey.length > 256 || /[\u0000-\u001f\u007f]/u.test(idempotencyKey))
|
|
1597
|
+
return error("invalid_input", "idempotencyKey must be a bounded non-empty string");
|
|
1578
1598
|
const target = createHash("sha256")
|
|
1579
1599
|
.update(canonicalJson(lifecycleTarget(operation, input)))
|
|
1580
1600
|
.digest("hex");
|
|
@@ -161,6 +161,7 @@ function isLifecycleLedgerEntry(value: unknown): value is LifecycleLedgerEntry {
|
|
|
161
161
|
);
|
|
162
162
|
}
|
|
163
163
|
function canonicalJson(value: unknown): string {
|
|
164
|
+
if (typeof value === "bigint") return JSON.stringify(value.toString());
|
|
164
165
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
165
166
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
166
167
|
const record = value as Record<string, unknown>;
|
|
@@ -698,7 +699,9 @@ export class LifecycleLedger {
|
|
|
698
699
|
return this.#warnings;
|
|
699
700
|
}
|
|
700
701
|
async #append(entry: LifecycleLedgerEntry): Promise<LifecycleLedgerEntry> {
|
|
701
|
-
const line = Buffer.from(
|
|
702
|
+
const line = Buffer.from(
|
|
703
|
+
`${JSON.stringify(entry, (_key, value: unknown) => (typeof value === "bigint" ? value.toString() : value))}\n`,
|
|
704
|
+
);
|
|
702
705
|
if (line.length - 1 > this.#limits.maxLineBytes)
|
|
703
706
|
throw new Error("Lifecycle ledger row exceeds the maximum byte length.");
|
|
704
707
|
let replacementCompacted = false;
|
|
@@ -907,6 +910,30 @@ export class LifecycleLedger {
|
|
|
907
910
|
return this.#byIdentity.get(identity);
|
|
908
911
|
}
|
|
909
912
|
|
|
913
|
+
listUncertainCreatesBySessionId(
|
|
914
|
+
sessionId: string,
|
|
915
|
+
effectMarker?: string,
|
|
916
|
+
remoteCreateKey?: string,
|
|
917
|
+
): LifecycleLedgerEntry[] {
|
|
918
|
+
const matches: LifecycleLedgerEntry[] = [];
|
|
919
|
+
for (const current of this.#byIdentity.values()) {
|
|
920
|
+
const effectIntent = current.effectIntent;
|
|
921
|
+
if (
|
|
922
|
+
current.state === "terminal_uncertain" &&
|
|
923
|
+
current.intendedSessionId === sessionId &&
|
|
924
|
+
current.operationKey?.startsWith("session.create\u0000") &&
|
|
925
|
+
effectIntent?.sessionId === sessionId &&
|
|
926
|
+
typeof effectIntent.stateRoot === "string" &&
|
|
927
|
+
path.isAbsolute(effectIntent.stateRoot) &&
|
|
928
|
+
effectIntent.childOwnershipEstablished === true &&
|
|
929
|
+
(effectMarker === undefined || current.effectMarker === effectMarker) &&
|
|
930
|
+
(remoteCreateKey === undefined || current.operationKey === `session.create\u0000${remoteCreateKey}`)
|
|
931
|
+
)
|
|
932
|
+
matches.push(current);
|
|
933
|
+
}
|
|
934
|
+
return matches;
|
|
935
|
+
}
|
|
936
|
+
|
|
910
937
|
findPendingCleanupByTarget(
|
|
911
938
|
sessionId: string,
|
|
912
939
|
cwd: string,
|