@gajae-code/coding-agent 0.15.2 → 0.15.3

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 (95) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/dist/types/config/autorouting-tier-map.d.ts +0 -9
  3. package/dist/types/config/file-lock.d.ts +7 -1
  4. package/dist/types/config/keybindings.d.ts +13 -3
  5. package/dist/types/config/model-registry.d.ts +5 -0
  6. package/dist/types/config/model-resolver.d.ts +1 -0
  7. package/dist/types/config/settings-schema.d.ts +13 -1
  8. package/dist/types/coordinator/contract.d.ts +1 -1
  9. package/dist/types/coordinator-mcp/question-state.d.ts +42 -2
  10. package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
  11. package/dist/types/main.d.ts +1 -1
  12. package/dist/types/modes/action-registry.d.ts +1 -0
  13. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -1
  14. package/dist/types/modes/components/welcome.d.ts +2 -0
  15. package/dist/types/modes/controllers/input-controller.d.ts +16 -1
  16. package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +42 -0
  17. package/dist/types/modes/interactive-mode.d.ts +7 -0
  18. package/dist/types/modes/shared/oauth-url-copy.d.ts +9 -0
  19. package/dist/types/modes/types.d.ts +3 -0
  20. package/dist/types/modes/ui-language.d.ts +2 -1
  21. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  22. package/dist/types/sdk/broker/session-index.d.ts +18 -0
  23. package/dist/types/sdk/bus/index.d.ts +4 -1
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  26. package/dist/types/sdk/cli/session-cli.d.ts +1 -1
  27. package/dist/types/sdk/lifecycle/service.d.ts +32 -5
  28. package/dist/types/sdk/session.d.ts +5 -0
  29. package/dist/types/session/agent-session.d.ts +4 -0
  30. package/dist/types/session/fallback-chain-controller.d.ts +8 -0
  31. package/dist/types/slash-commands/helpers/aside.d.ts +54 -0
  32. package/dist/types/tools/ask.d.ts +8 -0
  33. package/dist/types/utils/clipboard.d.ts +9 -1
  34. package/package.json +7 -7
  35. package/scripts/generate-sdk-adapter-parity-manifest.ts +5 -2
  36. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  37. package/src/commands/sdk.ts +2 -2
  38. package/src/config/autorouting-tier-map.ts +0 -9
  39. package/src/config/file-lock.ts +15 -1
  40. package/src/config/keybindings.ts +13 -3
  41. package/src/config/model-registry.ts +132 -21
  42. package/src/config/model-resolver.ts +2 -1
  43. package/src/config/models-config-schema.ts +2 -2
  44. package/src/config/settings-schema.ts +13 -1
  45. package/src/coordinator/contract.ts +1 -0
  46. package/src/coordinator-mcp/question-state.ts +235 -4
  47. package/src/coordinator-mcp/server.ts +402 -2
  48. package/src/dap/client.ts +19 -8
  49. package/src/gjc-runtime/deep-interview-stage.ts +20 -1
  50. package/src/gjc-runtime/deep-interview-state.ts +3 -1
  51. package/src/gjc-runtime/session-state-lock.ts +11 -2
  52. package/src/internal-urls/docs-index.generated.ts +8 -7
  53. package/src/lsp/client.ts +39 -4
  54. package/src/main.ts +42 -11
  55. package/src/modes/action-registry.ts +7 -0
  56. package/src/modes/components/iterm-pet-transport.ts +4 -2
  57. package/src/modes/components/runtime-mcp-add-wizard.ts +38 -1
  58. package/src/modes/components/settings-selector.ts +1 -0
  59. package/src/modes/components/tool-status-header.ts +149 -41
  60. package/src/modes/components/welcome.ts +8 -0
  61. package/src/modes/controllers/goal-mode-controller.ts +7 -2
  62. package/src/modes/controllers/input-controller.ts +141 -7
  63. package/src/modes/controllers/plan-mode-controller.ts +12 -4
  64. package/src/modes/controllers/runtime-mcp-command-controller.ts +164 -20
  65. package/src/modes/controllers/selector-controller.ts +11 -2
  66. package/src/modes/interactive-mode.ts +88 -1
  67. package/src/modes/shared/oauth-url-copy.ts +33 -0
  68. package/src/modes/types.ts +3 -0
  69. package/src/modes/ui-language.ts +44 -3
  70. package/src/runtime-mcp/oauth-flow.ts +19 -8
  71. package/src/runtime-mcp/transports/stdio.ts +27 -13
  72. package/src/sdk/acp/adapter.ts +29 -2
  73. package/src/sdk/broker/broker.ts +26 -6
  74. package/src/sdk/broker/lifecycle-ledger.ts +28 -1
  75. package/src/sdk/broker/lifecycle.ts +664 -5
  76. package/src/sdk/broker/session-index.ts +246 -0
  77. package/src/sdk/broker/transport.ts +1 -0
  78. package/src/sdk/bus/index.ts +135 -38
  79. package/src/sdk/bus/natives-augment.d.ts +20 -1
  80. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  81. package/src/sdk/bus/telegram-daemon.ts +3 -0
  82. package/src/sdk/cli/session-cli.ts +34 -7
  83. package/src/sdk/lifecycle/service.ts +166 -11
  84. package/src/sdk/mcp/server.ts +2 -1
  85. package/src/sdk/protocol/operation-inventory.generated.json +40 -0
  86. package/src/sdk/protocol/operation-registry.ts +2 -1
  87. package/src/sdk/session.ts +91 -7
  88. package/src/session/agent-session.ts +403 -56
  89. package/src/session/fallback-chain-controller.ts +17 -0
  90. package/src/setup/frictionless-onboarding.ts +4 -4
  91. package/src/slash-commands/aside-command.test.ts +366 -0
  92. package/src/slash-commands/builtin-registry.ts +19 -0
  93. package/src/slash-commands/helpers/aside.ts +299 -0
  94. package/src/tools/ask.ts +8 -0
  95. package/src/utils/clipboard.ts +37 -28
@@ -273,6 +273,8 @@ export const ASK_CONTROLS_CAPABILITY = "ask_controls_v1";
273
273
  export const TOOL_ACTIVITY_CAPABILITY = "tool_activity_v2";
274
274
  /** Receive-only compatibility capability for pre-v2 hosts. */
275
275
  export const LEGACY_TOOL_ACTIVITY_CAPABILITY = "tool_activity_v1";
276
+ /** Opts notification adapters into positioned-only delivery for matching live effects. */
277
+ export const POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY = "positioned_notification_effects_v1";
276
278
  type ToolActivityCapability = "v1" | "v2";
277
279
 
278
280
  function negotiateToolActivityCapability(
@@ -5261,6 +5263,7 @@ export class TelegramNotificationDaemon {
5261
5263
  ASK_SELECTED_ACK_CAPABILITY,
5262
5264
  TOOL_ACTIVITY_CAPABILITY,
5263
5265
  LEGACY_TOOL_ACTIVITY_CAPABILITY,
5266
+ POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY,
5264
5267
  "ephemeral_turn_v1",
5265
5268
  ],
5266
5269
  });
@@ -11,6 +11,7 @@ import type {
11
11
  SessionLifecycleOperation,
12
12
  SessionLifecycleSavedSession,
13
13
  SessionLifecycleSavedSessionIdentity,
14
+ SessionReconcileUncertainTarget,
14
15
  } from "../lifecycle/service";
15
16
  import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../prompt-status";
16
17
  import { validateAdapterControl, validateAdapterSecretFields } from "../protocol/adapter-validation";
@@ -36,6 +37,7 @@ export type SdkSessionCliAction =
36
37
  | "send"
37
38
  | "status"
38
39
  | "tail"
40
+ | "retire"
39
41
  | "raw"
40
42
  | "control"
41
43
  | "query"
@@ -209,7 +211,8 @@ function isLifecycleOperation(operation: string): operation is LifecycleMutation
209
211
  operation === "session.fork" ||
210
212
  operation === "session.resume" ||
211
213
  operation === "session.close" ||
212
- operation === "session.delete"
214
+ operation === "session.delete" ||
215
+ operation === "session.reconcile_uncertain"
213
216
  );
214
217
  }
215
218
 
@@ -1294,11 +1297,16 @@ function lifecycleMutationRequest(
1294
1297
  2,
1295
1298
  );
1296
1299
  const target = input as JsonRecord & { sessionId: string };
1297
- return operation === "session.resume"
1298
- ? { ...base, operation, capability: operation, target }
1299
- : operation === "session.close"
1300
- ? { ...base, operation, capability: operation, target }
1301
- : { ...base, operation, capability: "session.delete", target };
1300
+ if (operation === "session.resume") return { ...base, operation, capability: operation, target };
1301
+ if (operation === "session.close") return { ...base, operation, capability: operation, target };
1302
+ if (operation === "session.reconcile_uncertain")
1303
+ return {
1304
+ ...base,
1305
+ operation,
1306
+ capability: operation,
1307
+ target: input as unknown as SessionReconcileUncertainTarget,
1308
+ };
1309
+ return { ...base, operation, capability: "session.delete", target };
1302
1310
  }
1303
1311
 
1304
1312
  async function runRawGlobal(
@@ -1346,6 +1354,7 @@ export async function runSdkSessionCli(
1346
1354
  action !== "send" &&
1347
1355
  action !== "status" &&
1348
1356
  action !== "tail" &&
1357
+ action !== "retire" &&
1349
1358
  action !== "raw" &&
1350
1359
  action !== "control" &&
1351
1360
  action !== "query" &&
@@ -1353,7 +1362,7 @@ export async function runSdkSessionCli(
1353
1362
  )
1354
1363
  throw new SdkSessionCliError(
1355
1364
  "usage",
1356
- "Expected one of: list, inspect, send, status, tail, raw (control|query|global).",
1365
+ "Expected one of: list, inspect, send, status, tail, retire, raw (control|query|global).",
1357
1366
  2,
1358
1367
  );
1359
1368
  const agentDir = args.agentDir ?? getAgentDir();
@@ -1390,6 +1399,24 @@ export async function runSdkSessionCli(
1390
1399
  );
1391
1400
  return;
1392
1401
  }
1402
+ if (action === "retire") {
1403
+ const sessionId = requireValue(args.sessionId, "<sessionId>");
1404
+ const input = await inputFromArgs(args);
1405
+ if (input.sessionId !== undefined && input.sessionId !== sessionId)
1406
+ throw new SdkSessionCliError(
1407
+ "invalid_input",
1408
+ "Retirement sessionId does not match the selected session.",
1409
+ 2,
1410
+ );
1411
+ const secretError = validateAdapterSecretFields("session.reconcile_uncertain", input);
1412
+ if (secretError) throw new SdkSessionCliError(secretError.code, secretError.message, 2);
1413
+ writeOutput(
1414
+ stripSecretFields(
1415
+ await runRawGlobal(agentDir, "session.reconcile_uncertain", { ...input, sessionId }, args),
1416
+ ),
1417
+ );
1418
+ return;
1419
+ }
1393
1420
 
1394
1421
  const kind = rawKind(requireValue(action, "<verb>"), args);
1395
1422
  if (!kind) throw new SdkSessionCliError("usage", "raw requires one of: control, query, global.", 2);
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import path from "node:path";
2
3
  import {
3
4
  SessionListTraversalError,
4
5
  type SessionListTraversalPage,
@@ -12,6 +13,7 @@ export type SessionLifecycleOperation =
12
13
  | "session.resume"
13
14
  | "session.close"
14
15
  | "session.delete"
16
+ | "session.reconcile_uncertain"
15
17
  | "session.list";
16
18
 
17
19
  export interface SessionLifecycleActor {
@@ -117,6 +119,18 @@ export interface SessionCloseTarget {
117
119
  readonly endpointIncarnation?: string;
118
120
  }
119
121
 
122
+ export interface SessionReconcileUncertainTarget {
123
+ readonly sessionId: string;
124
+ readonly cwd: string;
125
+ readonly stateRoot: string;
126
+ readonly endpointGeneration: number;
127
+ readonly endpointMtimeMs: number;
128
+ readonly processIncarnation: string;
129
+ readonly hostIncarnation: string;
130
+ readonly lifecycleRequestId: string;
131
+ readonly remoteCreateKey: string;
132
+ }
133
+
120
134
  export interface SessionDeleteTarget {
121
135
  readonly sessionId: string;
122
136
  readonly cwd?: string;
@@ -146,6 +160,10 @@ export type SessionForkRequest = SessionLifecycleMutationRequestBase<"session.fo
146
160
  export type SessionResumeRequest = SessionLifecycleMutationRequestBase<"session.resume", SessionResumeTarget>;
147
161
  export type SessionCloseRequest = SessionLifecycleMutationRequestBase<"session.close", SessionCloseTarget>;
148
162
  export type SessionDeleteRequest = SessionLifecycleMutationRequestBase<"session.delete", SessionDeleteTarget>;
163
+ export type SessionReconcileUncertainRequest = SessionLifecycleMutationRequestBase<
164
+ "session.reconcile_uncertain",
165
+ SessionReconcileUncertainTarget
166
+ >;
149
167
  export interface SessionListRequest {
150
168
  readonly operation: "session.list";
151
169
  readonly actor: SessionLifecycleActor;
@@ -161,7 +179,8 @@ export type SessionLifecycleMutationRequest =
161
179
  | SessionForkRequest
162
180
  | SessionResumeRequest
163
181
  | SessionCloseRequest
164
- | SessionDeleteRequest;
182
+ | SessionDeleteRequest
183
+ | SessionReconcileUncertainRequest;
165
184
 
166
185
  export type SessionLifecycleCertainty = "terminal" | "retryable" | "cleanup_pending" | "uncertain";
167
186
 
@@ -213,6 +232,11 @@ export interface SessionDeleteResult {
213
232
  readonly operation: "session.delete";
214
233
  readonly result: SessionLifecycleSessionResult;
215
234
  }
235
+ export interface SessionReconcileUncertainResult {
236
+ readonly ok: true;
237
+ readonly operation: "session.reconcile_uncertain";
238
+ readonly result: SessionLifecycleSessionResult;
239
+ }
216
240
  export interface SessionListSuccessResult {
217
241
  readonly ok: true;
218
242
  readonly operation: "session.list";
@@ -254,6 +278,12 @@ export type SessionDeleteFailure = {
254
278
  readonly certainty: SessionLifecycleCertainty;
255
279
  readonly error: SessionLifecycleError;
256
280
  };
281
+ export type SessionReconcileUncertainFailure = {
282
+ readonly ok: false;
283
+ readonly operation: "session.reconcile_uncertain";
284
+ readonly certainty: SessionLifecycleCertainty;
285
+ readonly error: SessionLifecycleError;
286
+ };
257
287
  export type SessionListFailure = {
258
288
  readonly ok: false;
259
289
  readonly operation: "session.list";
@@ -266,6 +296,7 @@ export type SessionForkOutcome = SessionForkResult | SessionForkFailure;
266
296
  export type SessionResumeOutcome = SessionResumeResult | SessionResumeFailure;
267
297
  export type SessionCloseOutcome = SessionCloseResult | SessionCloseFailure;
268
298
  export type SessionDeleteOutcome = SessionDeleteResult | SessionDeleteFailure;
299
+ export type SessionReconcileUncertainOutcome = SessionReconcileUncertainResult | SessionReconcileUncertainFailure;
269
300
  export type SessionListOutcome = SessionListSuccessResult | SessionListFailure;
270
301
  export type SessionLifecycleResult =
271
302
  | SessionCreateOutcome
@@ -273,6 +304,7 @@ export type SessionLifecycleResult =
273
304
  | SessionResumeOutcome
274
305
  | SessionCloseOutcome
275
306
  | SessionDeleteOutcome
307
+ | SessionReconcileUncertainOutcome
276
308
  | SessionListOutcome;
277
309
 
278
310
  /** Shared, side-effect-free validation for lifecycle mutation requests. */
@@ -288,7 +320,8 @@ export type SessionLifecycleMutationValidation =
288
320
  | SessionForkFailure
289
321
  | SessionResumeFailure
290
322
  | SessionCloseFailure
291
- | SessionDeleteFailure;
323
+ | SessionDeleteFailure
324
+ | SessionReconcileUncertainFailure;
292
325
 
293
326
  const RETRYABLE_BROKER_ERRORS = new Set([
294
327
  "unavailable",
@@ -338,6 +371,7 @@ function operationOf(value: unknown): SessionLifecycleOperation {
338
371
  value === "session.resume" ||
339
372
  value === "session.close" ||
340
373
  value === "session.delete" ||
374
+ value === "session.reconcile_uncertain" ||
341
375
  value === "session.list"
342
376
  )
343
377
  return value;
@@ -376,6 +410,33 @@ function validTarget(target: unknown): target is Readonly<Record<string, unknown
376
410
  return isRecord(target);
377
411
  }
378
412
 
413
+ export function validateSessionReconcileUncertainTarget(value: unknown): value is SessionReconcileUncertainTarget {
414
+ if (!isRecord(value)) return false;
415
+ const target = value;
416
+ const bounded = (value: unknown, max: number): value is string =>
417
+ typeof value === "string" && value.length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/u.test(value);
418
+ return (
419
+ bounded(target.sessionId, 256) &&
420
+ /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(target.sessionId) &&
421
+ bounded(target.cwd, 4096) &&
422
+ path.isAbsolute(target.cwd) &&
423
+ bounded(target.stateRoot, 4096) &&
424
+ path.isAbsolute(target.stateRoot) &&
425
+ path.resolve(target.stateRoot) === path.join(path.resolve(target.cwd), ".gjc", "state") &&
426
+ typeof target.endpointGeneration === "number" &&
427
+ Number.isSafeInteger(target.endpointGeneration) &&
428
+ target.endpointGeneration > 0 &&
429
+ typeof target.endpointMtimeMs === "number" &&
430
+ Number.isFinite(target.endpointMtimeMs) &&
431
+ target.endpointMtimeMs > 0 &&
432
+ bounded(target.processIncarnation, 256) &&
433
+ bounded(target.hostIncarnation, 256) &&
434
+ bounded(target.lifecycleRequestId, 128) &&
435
+ /^[A-Za-z0-9._-]+$/u.test(target.lifecycleRequestId) &&
436
+ bounded(target.remoteCreateKey, 256)
437
+ );
438
+ }
439
+
379
440
  /** Validates lifecycle authority and shape without contacting the Broker. */
380
441
  export function validateSessionLifecycleMutationRequest(request: unknown): SessionLifecycleMutationValidation {
381
442
  const record = isRecord(request) ? request : {};
@@ -390,6 +451,13 @@ export function validateSessionLifecycleMutationRequest(request: unknown): Sessi
390
451
  return failure(operation, "terminal", "capability_denied", `capability does not authorize ${operation}`);
391
452
  if (!validTarget(record.target))
392
453
  return failure(operation, "terminal", "invalid_request", "target must be an object");
454
+ if (operation === "session.reconcile_uncertain" && !validateSessionReconcileUncertainTarget(record.target))
455
+ return failure(
456
+ operation,
457
+ "terminal",
458
+ "invalid_request",
459
+ "session.reconcile_uncertain target must carry complete identity-bound retirement proof",
460
+ );
393
461
  return {
394
462
  ok: true,
395
463
  operation,
@@ -439,6 +507,28 @@ function sessionResult(value: unknown, expectedSessionId?: string): SessionLifec
439
507
  return result;
440
508
  }
441
509
 
510
+ function reconcileUncertainResult(
511
+ value: unknown,
512
+ target: SessionReconcileUncertainTarget,
513
+ ): SessionLifecycleSessionResult | undefined {
514
+ if (!isRecord(value)) return undefined;
515
+ if (
516
+ value.sessionId !== target.sessionId ||
517
+ value.retired !== true ||
518
+ value.ledgerState !== "terminal_error" ||
519
+ value.indexType !== "session_closed" ||
520
+ value.stateRoot !== target.stateRoot ||
521
+ value.endpointGeneration !== target.endpointGeneration ||
522
+ value.endpointMtimeMs !== target.endpointMtimeMs ||
523
+ value.processIncarnation !== target.processIncarnation ||
524
+ value.hostIncarnation !== target.hostIncarnation ||
525
+ value.lifecycleRequestId !== target.lifecycleRequestId ||
526
+ value.remoteCreateKey !== target.remoteCreateKey
527
+ )
528
+ return undefined;
529
+ return { sessionId: target.sessionId, endpointGeneration: target.endpointGeneration };
530
+ }
531
+
442
532
  function savedSessionTranscriptIdentity(value: unknown): SessionLifecycleSavedSessionIdentity | undefined {
443
533
  if (!isRecord(value)) return undefined;
444
534
  const { dev, ino, nlink, size, mtimeMs, mtimeNs, ctimeNs, sha256 } = value;
@@ -588,20 +678,39 @@ export class SessionLifecycleService {
588
678
  this.#client = client;
589
679
  }
590
680
 
591
- async execute(
681
+ async #execute(
592
682
  request: SessionLifecycleMutationRequest,
683
+ idempotencyKeyOverride?: string,
593
684
  ): Promise<
594
- SessionCreateOutcome | SessionForkOutcome | SessionResumeOutcome | SessionCloseOutcome | SessionDeleteOutcome
685
+ | SessionCreateOutcome
686
+ | SessionForkOutcome
687
+ | SessionResumeOutcome
688
+ | SessionCloseOutcome
689
+ | SessionDeleteOutcome
690
+ | SessionReconcileUncertainOutcome
595
691
  > {
596
692
  const validation = validateSessionLifecycleMutationRequest(request);
597
693
  if (!validation.ok) return validation;
598
694
  const { operation, actor, requestKey, target } = validation;
599
- const idempotencyKey = deriveSessionLifecycleIdempotencyKey(actor, requestKey, operation);
695
+ const normalizedTarget =
696
+ operation === "session.reconcile_uncertain"
697
+ ? {
698
+ ...target,
699
+ cwd: path.resolve((target as unknown as SessionReconcileUncertainTarget).cwd),
700
+ stateRoot: path.join(
701
+ path.resolve((target as unknown as SessionReconcileUncertainTarget).cwd),
702
+ ".gjc",
703
+ "state",
704
+ ),
705
+ }
706
+ : target;
707
+ const idempotencyKey =
708
+ idempotencyKeyOverride ?? deriveSessionLifecycleIdempotencyKey(actor, requestKey, operation);
600
709
  let response: unknown;
601
710
  try {
602
711
  response = await this.#client.global(
603
712
  operation,
604
- { ...target },
713
+ { ...normalizedTarget },
605
714
  {
606
715
  idempotencyKey,
607
716
  ...(request.timeoutMs === undefined ? {} : { timeoutMs: request.timeoutMs }),
@@ -619,12 +728,21 @@ export class SessionLifecycleService {
619
728
  if (!isRecord(response) || response.ok !== true)
620
729
  return failure(operation, "uncertain", "malformed_response", "lifecycle broker returned a malformed response");
621
730
  const expectedSessionId =
622
- operation === "session.resume" || operation === "session.close" || operation === "session.delete"
623
- ? typeof target.sessionId === "string"
624
- ? target.sessionId
731
+ operation === "session.resume" ||
732
+ operation === "session.close" ||
733
+ operation === "session.delete" ||
734
+ operation === "session.reconcile_uncertain"
735
+ ? typeof normalizedTarget.sessionId === "string"
736
+ ? normalizedTarget.sessionId
625
737
  : undefined
626
738
  : undefined;
627
- const parsed = sessionResult(brokerSuccess(response), expectedSessionId);
739
+ const parsed =
740
+ operation === "session.reconcile_uncertain"
741
+ ? reconcileUncertainResult(
742
+ brokerSuccess(response),
743
+ normalizedTarget as unknown as SessionReconcileUncertainTarget,
744
+ )
745
+ : sessionResult(brokerSuccess(response), expectedSessionId);
628
746
  if (!parsed)
629
747
  return failure(
630
748
  operation,
@@ -637,7 +755,35 @@ export class SessionLifecycleService {
637
755
  | SessionForkOutcome
638
756
  | SessionResumeOutcome
639
757
  | SessionCloseOutcome
640
- | SessionDeleteOutcome;
758
+ | SessionDeleteOutcome
759
+ | SessionReconcileUncertainOutcome;
760
+ }
761
+
762
+ async execute(
763
+ request: SessionLifecycleMutationRequest,
764
+ ): Promise<
765
+ | SessionCreateOutcome
766
+ | SessionForkOutcome
767
+ | SessionResumeOutcome
768
+ | SessionCloseOutcome
769
+ | SessionDeleteOutcome
770
+ | SessionReconcileUncertainOutcome
771
+ > {
772
+ return this.#execute(request);
773
+ }
774
+
775
+ async executeWithIdempotencyKey(
776
+ request: SessionLifecycleMutationRequest,
777
+ idempotencyKey: string,
778
+ ): Promise<
779
+ | SessionCreateOutcome
780
+ | SessionForkOutcome
781
+ | SessionResumeOutcome
782
+ | SessionCloseOutcome
783
+ | SessionDeleteOutcome
784
+ | SessionReconcileUncertainOutcome
785
+ > {
786
+ return this.#execute(request, idempotencyKey);
641
787
  }
642
788
 
643
789
  async create(request: Omit<SessionCreateRequest, "operation">): Promise<SessionCreateOutcome> {
@@ -660,6 +806,15 @@ export class SessionLifecycleService {
660
806
  return (await this.execute({ ...request, operation: "session.delete" })) as SessionDeleteOutcome;
661
807
  }
662
808
 
809
+ async reconcileUncertain(
810
+ request: Omit<SessionReconcileUncertainRequest, "operation">,
811
+ ): Promise<SessionReconcileUncertainOutcome> {
812
+ return (await this.execute({
813
+ ...request,
814
+ operation: "session.reconcile_uncertain",
815
+ })) as SessionReconcileUncertainOutcome;
816
+ }
817
+
663
818
  async list(request: Omit<SessionListRequest, "operation">): Promise<SessionListOutcome> {
664
819
  if (!validActor((request as { actor?: unknown }).actor))
665
820
  return failure("session.list", "terminal", "unauthorized", "authenticated actor is required");
@@ -158,7 +158,8 @@ function isLifecycleOperation(operation: string): operation is LifecycleMutation
158
158
  operation === "session.fork" ||
159
159
  operation === "session.resume" ||
160
160
  operation === "session.close" ||
161
- operation === "session.delete"
161
+ operation === "session.delete" ||
162
+ operation === "session.reconcile_uncertain"
162
163
  );
163
164
  }
164
165
 
@@ -1079,6 +1079,24 @@
1079
1079
  "packages/coding-agent/test/sdk-operation-inventory.test.ts"
1080
1080
  ]
1081
1081
  },
1082
+ {
1083
+ "sourceId": "registry:G08",
1084
+ "sourceFile": "packages/coding-agent/src/sdk/protocol/operation-registry.ts",
1085
+ "sourceKind": "registry",
1086
+ "decision": "include",
1087
+ "sdkId": "session.reconcile_uncertain",
1088
+ "adapterMappings": {
1089
+ "telegram": "prohibited",
1090
+ "discord": "prohibited",
1091
+ "slack": "prohibited",
1092
+ "mcp": "generic_safe",
1093
+ "acp": "generic_safe",
1094
+ "daemonCli": "generic_safe"
1095
+ },
1096
+ "testIds": [
1097
+ "packages/coding-agent/test/sdk-operation-inventory.test.ts"
1098
+ ]
1099
+ },
1082
1100
  {
1083
1101
  "sourceId": "registry:Q01",
1084
1102
  "sourceFile": "packages/coding-agent/src/sdk/protocol/operation-registry.ts",
@@ -1727,6 +1745,17 @@
1727
1745
  "packages/coding-agent/test/sdk-operation-inventory.test.ts"
1728
1746
  ]
1729
1747
  },
1748
+ {
1749
+ "sourceId": "slash_command:aside",
1750
+ "sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
1751
+ "sourceKind": "slash_command",
1752
+ "decision": "exclude",
1753
+ "rationale": "explicit user-owned external CLI process launcher; not a credential-free SDK control seam",
1754
+ "exclusionMetadata": {
1755
+ "adapterMappings": "not_applicable",
1756
+ "testIds": "not_applicable"
1757
+ }
1758
+ },
1730
1759
  {
1731
1760
  "sourceId": "slash_command:import-session",
1732
1761
  "sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
@@ -2874,6 +2903,17 @@
2874
2903
  "packages/coding-agent/test/sdk-operation-inventory.test.ts"
2875
2904
  ]
2876
2905
  },
2906
+ {
2907
+ "sourceId": "agent_session:isExplicitEmptyToolSelection",
2908
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2909
+ "sourceKind": "agent_session",
2910
+ "decision": "exclude",
2911
+ "rationale": "internal tool-selection predicate used to preserve the --no-tools boundary, not a user-facing SDK control seam",
2912
+ "exclusionMetadata": {
2913
+ "adapterMappings": "not_applicable",
2914
+ "testIds": "not_applicable"
2915
+ }
2916
+ },
2877
2917
  {
2878
2918
  "sourceId": "agent_session:getToolByName",
2879
2919
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
@@ -135,6 +135,7 @@ const globals = [
135
135
  ["session.resume", "Resume a saved session."],
136
136
  ["session.close", "Close a saved session."],
137
137
  ["session.delete", "Delete a saved session."],
138
+ ["session.reconcile_uncertain", "Retire an indexed terminalUncertain create effect after dead-host proof."],
138
139
  ] as const;
139
140
 
140
141
  const queries = [
@@ -349,7 +350,7 @@ export const OPERATIONS: readonly Operation[] = [
349
350
  acp: "machine_only",
350
351
  daemonCli: "machine_only",
351
352
  })
352
- : ["G03", "G04", "G05", "G06", "G07"].includes(id)
353
+ : ["G03", "G04", "G05", "G06", "G07", "G08"].includes(id)
353
354
  ? dispositions({ telegram: "prohibited", discord: "prohibited", slack: "prohibited" })
354
355
  : dispositions(),
355
356
  testIds: ["packages/coding-agent/test/sdk-operation-inventory.test.ts"],
@@ -59,6 +59,7 @@ import { kNoAuth, ModelRegistry } from "../config/model-registry";
59
59
  import {
60
60
  formatModelString,
61
61
  parseModelPattern,
62
+ parseModelString,
62
63
  resolveAllowedModels,
63
64
  resolveModelChainWithAuth,
64
65
  resolveModelRoleValue,
@@ -430,6 +431,8 @@ export interface CreateAgentSessionOptions {
430
431
  authStorage?: AuthStorage;
431
432
  /** Model registry. Default: discoverModels(authStorage, agentDir) */
432
433
  modelRegistry?: ModelRegistry;
434
+ /** @internal Allows the first-party CLI root to mutate its own registry and observe an admission attempt. */
435
+ modelRegistryStartupMutation?: { owner: "cli-root"; onAttempt(): void };
433
436
 
434
437
  /** Model to use. Default: from settings, else first available */
435
438
  model?: Model;
@@ -1442,7 +1445,45 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1442
1445
  const runtimeServices = createOptionalRuntimeServices(settings, options.runtimeServices, { cwd: getLiveCwd });
1443
1446
  modelRegistry.applyConfiguredModelBindings(settings);
1444
1447
  logger.time("initializeWithSettings", initializeWithSettings, settings);
1445
- if (!options.modelRegistry) {
1448
+ const startupModelReference =
1449
+ options.model === undefined
1450
+ ? options.modelPattern
1451
+ ? parseModelString(options.modelPattern)
1452
+ : undefined
1453
+ : options.modelRegistryStartupMutation?.owner === "cli-root"
1454
+ ? { provider: options.model.provider, id: options.model.id }
1455
+ : undefined;
1456
+ const startupRegistryMutationAuthorized =
1457
+ options.modelRegistry === undefined || options.modelRegistryStartupMutation?.owner === "cli-root";
1458
+ const startupCredentialSelector =
1459
+ startupModelReference &&
1460
+ options.credentialSelector &&
1461
+ (!options.credentialSelector.provider ||
1462
+ options.credentialSelector.provider.toLowerCase() === startupModelReference.provider.toLowerCase())
1463
+ ? options.credentialSelector.selector
1464
+ : undefined;
1465
+ const startupCredentialProviderMismatch =
1466
+ startupModelReference !== undefined &&
1467
+ options.credentialSelector?.provider !== undefined &&
1468
+ options.credentialSelector.provider.toLowerCase() !== startupModelReference.provider.toLowerCase();
1469
+ const attemptedStartupCacheAdmission =
1470
+ startupModelReference !== undefined &&
1471
+ startupRegistryMutationAuthorized &&
1472
+ options.credentialSelector !== undefined &&
1473
+ modelRegistry.requiresStoredLiteralCredentialCacheAdmission(
1474
+ startupModelReference.provider,
1475
+ startupModelReference.id,
1476
+ );
1477
+ if (attemptedStartupCacheAdmission) options.modelRegistryStartupMutation?.onAttempt();
1478
+ if (startupModelReference && startupCredentialSelector) {
1479
+ if (attemptedStartupCacheAdmission) {
1480
+ modelRegistry.admitCachedProviderForStoredLiteralCredential(
1481
+ startupModelReference.provider,
1482
+ startupCredentialSelector,
1483
+ );
1484
+ }
1485
+ }
1486
+ if (!options.modelRegistry && !attemptedStartupCacheAdmission) {
1446
1487
  modelRegistry.refreshInBackground();
1447
1488
  }
1448
1489
  // Resolve the workspace tree through its runtime service. The compatibility
@@ -3254,8 +3295,41 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3254
3295
  extensionsResult.runtime.pendingProviderRegistrations = [];
3255
3296
  }
3256
3297
 
3298
+ let startupCredentialModelRejected = false;
3299
+ if (startupModelReference && (startupCredentialSelector || startupCredentialProviderMismatch)) {
3300
+ const validated =
3301
+ !startupCredentialProviderMismatch &&
3302
+ startupCredentialSelector !== undefined &&
3303
+ modelRegistry.validateModelForStoredLiteralCredential(
3304
+ startupModelReference.provider,
3305
+ startupModelReference.id,
3306
+ startupCredentialSelector,
3307
+ );
3308
+ const currentStartupModel = modelRegistry.find(startupModelReference.provider, startupModelReference.id);
3309
+ const providerMismatchRejectsTarget =
3310
+ startupCredentialProviderMismatch &&
3311
+ modelRegistry.requiresStoredLiteralCredentialCacheAdmission(
3312
+ startupModelReference.provider,
3313
+ startupModelReference.id,
3314
+ );
3315
+ if (
3316
+ (!validated && (startupCredentialSelector !== undefined || providerMismatchRejectsTarget)) ||
3317
+ !currentStartupModel
3318
+ ) {
3319
+ startupCredentialModelRejected = true;
3320
+ if (model?.provider === startupModelReference.provider && model.id === startupModelReference.id) {
3321
+ model = undefined;
3322
+ }
3323
+ modelFallbackMessage = `Model "${
3324
+ options.modelPattern ?? `${startupModelReference.provider}/${startupModelReference.id}`
3325
+ }" not found`;
3326
+ } else if (model?.provider === startupModelReference.provider && model.id === startupModelReference.id) {
3327
+ model = currentStartupModel;
3328
+ }
3329
+ }
3330
+
3257
3331
  // Resolve deferred --model pattern now that extension models are registered.
3258
- if (!model && options.modelPattern) {
3332
+ if (!model && options.modelPattern && !startupCredentialModelRejected) {
3259
3333
  const availableModels = modelRegistry.getAll();
3260
3334
  const matchPreferences = {
3261
3335
  usageOrder: settings.getStorage()?.getModelUsageOrder(),
@@ -3290,7 +3364,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3290
3364
  // Fall back to first available model with a valid API key, honoring the
3291
3365
  // path-scoped `enabledModels` allow-list when configured. Skip when the
3292
3366
  // user explicitly requested a model via --model that wasn't found.
3293
- if (!model && !options.modelPattern) {
3367
+ if (!model && !options.modelPattern && !startupCredentialModelRejected) {
3294
3368
  // Re-resolve the allowed set: extension factories above may have
3295
3369
  // registered providers/models that weren't visible at startup.
3296
3370
  const fallbackCandidates = await resolveAllowedModels(modelRegistry, settings, modelMatchPreferences);
@@ -3660,11 +3734,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3660
3734
 
3661
3735
  const toolNamesFromRegistry = Array.from(toolRegistry.keys());
3662
3736
  const hasExplicitToolNames = options.toolNames !== undefined;
3737
+ const hasExplicitEmptyToolSelection = hasExplicitToolNames && options.toolNames!.length === 0;
3738
+ // `goal` is session state rather than a work tool, so it is folded into an
3739
+ // explicit selection that already asks for tools. An *empty* explicit
3740
+ // selection is the `--no-tools` contract ("Disable all built-in tools", see
3741
+ // cli/root-flags.ts), and re-populating it with `goal` would leave exactly one
3742
+ // built-in tool live against that promise.
3743
+ const wantsGoalStateTool = hasExplicitToolNames && options.toolNames!.length > 0;
3663
3744
  const requestedToolNames = hasExplicitToolNames
3664
3745
  ? [
3665
3746
  ...new Set([
3666
3747
  ...options.toolNames!.map(name => name.toLowerCase()),
3667
- ...(settings.get("goal.enabled") ? ["goal"] : []),
3748
+ ...(wantsGoalStateTool && settings.get("goal.enabled") ? ["goal"] : []),
3668
3749
  ]),
3669
3750
  ]
3670
3751
  : toolNamesFromRegistry;
@@ -3804,9 +3885,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3804
3885
  allowedDiscoveredBuiltinNames,
3805
3886
  essentialBuiltinNames,
3806
3887
  );
3807
- initialSelectedDiscoveredBuiltinToolNames = existingSession.hasPersistedDiscoveredBuiltinToolSelection
3808
- ? restoredDiscoveredNames
3809
- : explicitlyRequestedDiscoveredBuiltinToolNames;
3888
+ initialSelectedDiscoveredBuiltinToolNames = hasExplicitEmptyToolSelection
3889
+ ? []
3890
+ : existingSession.hasPersistedDiscoveredBuiltinToolSelection
3891
+ ? restoredDiscoveredNames
3892
+ : explicitlyRequestedDiscoveredBuiltinToolNames;
3810
3893
  initialToolNames = [...new Set([...baselineInitialToolNames, ...initialSelectedDiscoveredBuiltinToolNames])];
3811
3894
  hasExplicitDiscoveredBuiltinToolSelection =
3812
3895
  hasExplicitToolNames &&
@@ -4163,6 +4246,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4163
4246
  },
4164
4247
  reloadSshTool,
4165
4248
  requestedToolNames: requestedToolNameSet,
4249
+ explicitEmptyToolSelection: hasExplicitEmptyToolSelection,
4166
4250
  discoverableToolAllowedNames: options.discoverableToolAllowedNames,
4167
4251
  mcpDiscoveryEnabled,
4168
4252
  discoveryMode: effectiveDiscoveryMode,