@gajae-code/coding-agent 0.12.7 → 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 (95) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/types/commands/notify.d.ts +4 -0
  3. package/dist/types/config/file-lock.d.ts +3 -0
  4. package/dist/types/config/model-discovery-manager.d.ts +6 -1
  5. package/dist/types/config/model-registry.d.ts +2 -0
  6. package/dist/types/config/resolve-config-value.d.ts +2 -1
  7. package/dist/types/config/settings-schema.d.ts +17 -1
  8. package/dist/types/main.d.ts +1 -0
  9. package/dist/types/modes/components/hook-selector.d.ts +20 -0
  10. package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
  11. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  12. package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
  13. package/dist/types/sdk/broker/broker.d.ts +5 -1
  14. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  15. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  16. package/dist/types/sdk/bus/config.d.ts +75 -113
  17. package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
  18. package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
  19. package/dist/types/sdk/bus/index.d.ts +1 -1
  20. package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
  21. package/dist/types/sdk/bus/notification-service.d.ts +31 -4
  22. package/dist/types/sdk/bus/session-control.d.ts +8 -3
  23. package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
  24. package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
  25. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  26. package/dist/types/sdk/client/discovery.d.ts +2 -1
  27. package/dist/types/sdk/host/query/handlers.d.ts +2 -0
  28. package/dist/types/sdk/index.d.ts +1 -0
  29. package/dist/types/sdk/providers.d.ts +22 -0
  30. package/dist/types/sdk/session-directory.d.ts +1 -1
  31. package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
  32. package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
  33. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
  34. package/dist/types/session/internal/session-open-errors.d.ts +5 -0
  35. package/dist/types/session/session-manager.d.ts +3 -1
  36. package/dist/types/session/session-storage.d.ts +2 -0
  37. package/package.json +10 -8
  38. package/scripts/build-sdk-package-smoke.ts +51 -0
  39. package/scripts/capture-notifications-settings-showcase.ts +1 -1
  40. package/src/cli/notify-cli.ts +204 -94
  41. package/src/commands/notify.ts +19 -8
  42. package/src/config/file-lock.ts +6 -0
  43. package/src/config/model-discovery-manager.ts +50 -8
  44. package/src/config/model-registry.ts +862 -75
  45. package/src/config/resolve-config-value.ts +10 -8
  46. package/src/config/settings-schema.ts +25 -15
  47. package/src/config/settings.ts +15 -15
  48. package/src/gjc-runtime/session-state-sidecar.ts +12 -2
  49. package/src/internal-urls/docs-index.generated.ts +6 -6
  50. package/src/main.ts +43 -7
  51. package/src/modes/DESIGN.md +11 -11
  52. package/src/modes/components/hook-selector.ts +155 -44
  53. package/src/modes/components/model-selector.ts +242 -49
  54. package/src/modes/components/notifications-settings-editor.ts +631 -85
  55. package/src/modes/controllers/extension-ui-controller.ts +114 -15
  56. package/src/modes/controllers/selector-controller.ts +464 -50
  57. package/src/sdk/broker/broker.ts +54 -10
  58. package/src/sdk/broker/lifecycle-ledger.ts +34 -1
  59. package/src/sdk/broker/lifecycle.ts +47 -50
  60. package/src/sdk/bus/chat-daemon-cli.ts +12 -5
  61. package/src/sdk/bus/chat-daemon-control.ts +35 -6
  62. package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
  63. package/src/sdk/bus/config.ts +534 -286
  64. package/src/sdk/bus/discord-live-provider.ts +57 -2
  65. package/src/sdk/bus/discord-provider.ts +21 -0
  66. package/src/sdk/bus/index.ts +97 -35
  67. package/src/sdk/bus/notification-orchestration.ts +371 -68
  68. package/src/sdk/bus/notification-service.ts +431 -130
  69. package/src/sdk/bus/session-control.ts +119 -44
  70. package/src/sdk/bus/slack-live-provider.ts +78 -2
  71. package/src/sdk/bus/slack-provider.ts +24 -0
  72. package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
  73. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  74. package/src/sdk/bus/telegram-daemon-control.ts +3 -3
  75. package/src/sdk/bus/telegram-daemon.ts +23 -15
  76. package/src/sdk/client/discovery.ts +10 -4
  77. package/src/sdk/host/query/handlers.ts +20 -1
  78. package/src/sdk/index.ts +1 -0
  79. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  80. package/src/sdk/protocol/operation-registry.ts +5 -2
  81. package/src/sdk/providers.ts +51 -0
  82. package/src/sdk/session-directory.ts +1 -0
  83. package/src/sdk/session.ts +10 -7
  84. package/src/session/agent-session.ts +44 -24
  85. package/src/session/internal/managed-session-scope.ts +981 -450
  86. package/src/session/internal/managed-session-storage.ts +382 -45
  87. package/src/session/internal/native-publish-outcome.ts +3 -1
  88. package/src/session/internal/session-open-errors.ts +11 -0
  89. package/src/session/session-manager.ts +27 -3
  90. package/src/session/session-storage.ts +72 -5
  91. package/src/slash-commands/builtin-registry.ts +82 -4
  92. package/src/task/index.ts +9 -3
  93. package/src/tools/ask.ts +1 -1
  94. package/src/tools/bash.ts +1 -1
  95. package/src/tools/telegram-send.ts +4 -2
@@ -0,0 +1,11 @@
1
+ export const SESSION_MIGRATION_BUSY_MESSAGE =
2
+ "Another session migration is still active. Wait for it to finish, then retry.";
3
+
4
+ export class SessionMigrationBusyError extends Error {
5
+ readonly code = "migration_busy";
6
+
7
+ constructor() {
8
+ super(SESSION_MIGRATION_BUSY_MESSAGE);
9
+ this.name = "SessionMigrationBusyError";
10
+ }
11
+ }
@@ -83,8 +83,8 @@ import {
83
83
  mayCleanManagedTreeStaging,
84
84
  retainManagedDirectoryAuthority,
85
85
  } from "./internal/managed-session-storage";
86
-
87
86
  import { classifyNativePublishOutcome, formatNativePublishDiagnostic } from "./internal/native-publish-outcome";
87
+ import { SessionMigrationBusyError } from "./internal/session-open-errors";
88
88
  import {
89
89
  hasOnlyKeys as hasOnlyMemoryGuardKeys,
90
90
  isMemoryGuardDecimalString,
@@ -940,6 +940,8 @@ export class SessionMigrationPolicyError extends Error {
940
940
  }
941
941
 
942
942
  export class SessionArtifactCapacityError extends Error {
943
+ readonly code = "artifact_capacity_exceeded";
944
+
943
945
  constructor(message: string) {
944
946
  super(message);
945
947
  this.name = "SessionArtifactCapacityError";
@@ -954,7 +956,12 @@ export interface StrictSessionOpenSuccess {
954
956
 
955
957
  export interface StrictSessionOpenFailure {
956
958
  kind: "error";
957
- reason: ResumeTailError["reason"] | "identity-mismatch" | "migration-required" | "artifact_capacity_exceeded";
959
+ reason:
960
+ | ResumeTailError["reason"]
961
+ | "identity-mismatch"
962
+ | "migration-required"
963
+ | "artifact_capacity_exceeded"
964
+ | "migration_busy";
958
965
  message?: string;
959
966
  }
960
967
 
@@ -4707,6 +4714,7 @@ export const MATERIALIZED_CACHE_MAX_BYTES = 32 * 1024 * 1024;
4707
4714
  export const SessionManagerTestHooks: {
4708
4715
  materializedCacheMaxBytesOverride?: number;
4709
4716
  beforeResidentTransitionIndexBuild?: () => void;
4717
+ afterForkSnapshot?: () => void | Promise<void>;
4710
4718
  } = {};
4711
4719
 
4712
4720
  function materializedCacheMaxBytes(): number {
@@ -6165,6 +6173,7 @@ export class SessionManager {
6165
6173
  const candidate = listing.owned.find(candidate => path.resolve(candidate.path) === requestedPath);
6166
6174
  if (!candidate) throw new Error("Managed session deletion requires exact logical authorization.");
6167
6175
  const deleted = await deleteManagedSessionCandidate(resolved.scope, candidate);
6176
+ if (deleted.kind === "error" && deleted.code === "migration_busy") throw new SessionMigrationBusyError();
6168
6177
  if (deleted.kind !== "deleted" && deleted.kind !== "already_deleted")
6169
6178
  throw new Error(`Could not delete managed session: ${deleted.message}`);
6170
6179
  } else {
@@ -6248,6 +6257,8 @@ export class SessionManager {
6248
6257
  newHeader,
6249
6258
  ...materializedEntries.filter((entry): entry is SessionEntry => entry.type !== "session"),
6250
6259
  ];
6260
+ const forkSnapshotHook = SessionManagerTestHooks.afterForkSnapshot;
6261
+ if (forkSnapshotHook) await forkSnapshotHook();
6251
6262
 
6252
6263
  await this.#closePersistWriter();
6253
6264
  this.#persistChain = Promise.resolve();
@@ -9603,6 +9614,7 @@ export class SessionManager {
9603
9614
  authority,
9604
9615
  );
9605
9616
  } catch (error) {
9617
+ if (error instanceof Error && error.message === "migration_busy") throw new SessionMigrationBusyError();
9606
9618
  if (error instanceof Error && error.message.startsWith("Managed root authority changed"))
9607
9619
  managedDestinationStore.assertBound();
9608
9620
  throw error;
@@ -9611,6 +9623,7 @@ export class SessionManager {
9611
9623
  managedDestinationStore.assertBound();
9612
9624
  if (opened.code === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
9613
9625
  if (opened.code === "artifact_capacity_exceeded") throw new SessionArtifactCapacityError(opened.message);
9626
+ if (opened.code === "migration_busy") throw new SessionMigrationBusyError();
9614
9627
  throw new Error(`Could not open managed session: ${opened.message}`);
9615
9628
  }
9616
9629
  assertManagedDestinationBound();
@@ -9706,7 +9719,15 @@ export class SessionManager {
9706
9719
  throw new Error(`Could not open session: ${inspected.reason}`);
9707
9720
  }
9708
9721
  const opened = await SessionManager.openExistingStrict(inspected.identity, destination, storage, migrationPolicy);
9709
- if (opened.kind === "error") throw new Error(`Could not open session: ${opened.reason}`);
9722
+ if (opened.kind === "error") {
9723
+ if (opened.reason === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
9724
+ if (opened.reason === "artifact_capacity_exceeded")
9725
+ throw new SessionArtifactCapacityError(
9726
+ opened.message ?? "Session artifacts exceed the migration capacity.",
9727
+ );
9728
+ if (opened.reason === "migration_busy") throw new SessionMigrationBusyError();
9729
+ throw new Error(`Could not open session: ${opened.reason}`);
9730
+ }
9710
9731
  return opened.manager;
9711
9732
  }
9712
9733
 
@@ -9844,6 +9865,7 @@ export class SessionManager {
9844
9865
  const candidate = listing.owned.find(item => path.resolve(item.path) === path.resolve(sessionPath));
9845
9866
  if (!candidate) throw new Error("Session is not an authorized managed candidate.");
9846
9867
  const deleted = await deleteManagedSessionCandidate(resolved.scope, candidate);
9868
+ if (deleted.kind === "error" && deleted.code === "migration_busy") throw new SessionMigrationBusyError();
9847
9869
  if (deleted.kind !== "deleted" && deleted.kind !== "already_deleted")
9848
9870
  throw new Error(`Could not delete managed session: ${deleted.message}`);
9849
9871
  }
@@ -10389,6 +10411,7 @@ export class SessionManager {
10389
10411
  return { kind: "error", reason: "legacy_migration_disabled" };
10390
10412
  if (error instanceof SessionArtifactCapacityError)
10391
10413
  return { kind: "error", reason: "artifact_capacity_exceeded", message: error.message };
10414
+ if (error instanceof SessionMigrationBusyError) return { kind: "error", reason: "migration_busy" };
10392
10415
  if (
10393
10416
  error instanceof Error &&
10394
10417
  (error.message.includes("source_changed") || error.message.includes("changed before migration"))
@@ -10451,6 +10474,7 @@ export class SessionManager {
10451
10474
  throw new SessionArtifactCapacityError(
10452
10475
  opened.message ?? "Session artifacts exceed the migration capacity.",
10453
10476
  );
10477
+ if (opened.reason === "migration_busy") throw new SessionMigrationBusyError();
10454
10478
  return undefined;
10455
10479
  }
10456
10480
  return opened.manager;
@@ -312,6 +312,7 @@ export type VerifiedSessionDeleteResult =
312
312
  artifactsIdentity: SessionStorageFileIdentity | undefined;
313
313
  /** Identity-bound quarantine path retained when recursive cleanup failed. */
314
314
  detachedArtifactsPath: string;
315
+ artifactsPayloadDurable?: true;
315
316
  /** Native snapshot required for an identity-bound recursive retry. */
316
317
  artifactsTree: NativeDirectoryTreeSnapshot;
317
318
  /** Transcript identity (unchanged) for retry binding. */
@@ -328,6 +329,7 @@ export type VerifiedSessionDeleteResult =
328
329
  transcriptIdentity: SessionStorageFileIdentity;
329
330
  /** Optional identity-bound transcript quarantine path for restart cleanup. */
330
331
  detachedTranscriptPath?: string;
332
+ transcriptPayloadDurable?: true;
331
333
  retainedSuccessorPath?: string;
332
334
  retainedPlaceholderPath?: string;
333
335
  retainedUnknownPath?: string;
@@ -423,6 +425,7 @@ type NativeDirectoryTreeApi = {
423
425
  function nativeDirectoryTreeApi(): NativeDirectoryTreeApi {
424
426
  return native as unknown as NativeDirectoryTreeApi;
425
427
  }
428
+
426
429
  function snapshotDirectoryTree(pathname: string): NativeDirectoryTreeSnapshot {
427
430
  const result = nativeDirectoryTreeApi().snapshotDirectoryTree(pathname);
428
431
  if (!result.ok || !result.snapshot)
@@ -438,10 +441,21 @@ function retainedTreeDoesNotExpandAuthority(
438
441
  ): boolean {
439
442
  if (expected.rootDev !== retained.rootDev || expected.rootIno !== retained.rootIno) return false;
440
443
  const expectedEntries = new Map(expected.entries.map(entry => [entry.relativePath, entry]));
444
+ if (retained.entries.length > expected.entries.length) return false;
441
445
  return retained.entries.every(entry => {
442
446
  if (entry.relativePath === "") return entry.kind === "directory";
443
447
  const authorized = expectedEntries.get(entry.relativePath);
444
- return authorized !== undefined && JSON.stringify(authorized) === JSON.stringify(entry);
448
+ if (
449
+ authorized === undefined ||
450
+ authorized.kind !== entry.kind ||
451
+ authorized.dev !== entry.dev ||
452
+ authorized.ino !== entry.ino ||
453
+ authorized.nlink !== entry.nlink
454
+ )
455
+ return false;
456
+ if (entry.kind !== "file") return entry.size === authorized.size;
457
+ const scrubbed = entry.size === "0" && entry.sha256 === createHash("sha256").update("").digest("hex");
458
+ return scrubbed || (entry.size === authorized.size && entry.sha256 === authorized.sha256);
445
459
  });
446
460
  }
447
461
 
@@ -938,7 +952,7 @@ export class FileSessionStorage implements SessionStorage {
938
952
  "Transcript parent identity does not match authorization",
939
953
  );
940
954
  const authorizedTranscriptParentIdentity = transcriptParentIdentity ?? parentIdentity;
941
- if (detachedArtifactsPath) {
955
+ if (detachedArtifactsPath && !artifactsRemoved) {
942
956
  if (
943
957
  !expectedArtifactsIdentity ||
944
958
  path.dirname(detachedArtifactsPath) !== path.dirname(transcriptPath) ||
@@ -997,6 +1011,9 @@ export class FileSessionStorage implements SessionStorage {
997
1011
  artifactsIdentity: expectedArtifactsIdentity,
998
1012
  detachedArtifactsPath: retainedRoot,
999
1013
  artifactsTree: retainedTree,
1014
+ ...((removal as typeof removal & { payloadDurable?: boolean }).payloadDurable === true
1015
+ ? { artifactsPayloadDurable: true as const }
1016
+ : {}),
1000
1017
  ...((removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath)
1001
1018
  ? { retainedSuccessorPath: removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath }
1002
1019
  : {}),
@@ -1045,6 +1062,24 @@ export class FileSessionStorage implements SessionStorage {
1045
1062
  "Artifact path reappeared after durable artifact-phase completion",
1046
1063
  );
1047
1064
  }
1065
+ if (artifactsRemoved && detachedArtifactsPath && expectedArtifactsIdentity && expectedArtifactsTree) {
1066
+ const retainedIdentity = this.#optionalDirectoryIdentity(detachedArtifactsPath);
1067
+ if (
1068
+ !retainedIdentity ||
1069
+ retainedIdentity.dev !== expectedArtifactsIdentity.dev ||
1070
+ retainedIdentity.ino !== expectedArtifactsIdentity.ino
1071
+ )
1072
+ throw new SessionDeleteVerificationError(
1073
+ "artifacts",
1074
+ "Retained artifact root identity changed before transcript cleanup",
1075
+ );
1076
+ const retainedTree = snapshotDirectoryTree(detachedArtifactsPath);
1077
+ if (!retainedTreeDoesNotExpandAuthority(expectedArtifactsTree, retainedTree))
1078
+ throw new SessionDeleteVerificationError(
1079
+ "artifacts",
1080
+ "Partial artifact cleanup expanded retained tree authority",
1081
+ );
1082
+ }
1048
1083
  if (!artifactsIdentity && expectedArtifactsIdentity && !detachedArtifactsPath && !artifactsRemoved) {
1049
1084
  // Absence at the original path alone is not completion: native recursive removal
1050
1085
  // may retain the planned root or its deterministic `.removing` final-stage root.
@@ -1116,12 +1151,22 @@ export class FileSessionStorage implements SessionStorage {
1116
1151
  throw new Error("parent_changed");
1117
1152
  fs.fsyncSync(descriptor);
1118
1153
  } catch (error) {
1119
- throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
1154
+ return {
1155
+ kind: "cleanup_pending",
1156
+ phase: "artifacts",
1157
+ error: new SessionDeleteVerificationError("artifacts", "durability_failed", {
1158
+ cause: toError(error),
1159
+ }),
1160
+ artifactsIdentity,
1161
+ detachedArtifactsPath: detach.detachedPath,
1162
+ artifactsTree,
1163
+ transcriptIdentity,
1164
+ };
1120
1165
  } finally {
1121
1166
  if (descriptor !== undefined) fs.closeSync(descriptor);
1122
1167
  }
1123
1168
  }
1124
- if (!detach.ok) {
1169
+ if (!detach.ok && process.platform === "win32") {
1125
1170
  return {
1126
1171
  kind: "cleanup_pending",
1127
1172
  phase: "artifacts",
@@ -1132,7 +1177,6 @@ export class FileSessionStorage implements SessionStorage {
1132
1177
  ...(detach.retainedSuccessorPath ? { retainedSuccessorPath: detach.retainedSuccessorPath } : {}),
1133
1178
  ...(detach.retainedPlaceholderPath ? { retainedPlaceholderPath: detach.retainedPlaceholderPath } : {}),
1134
1179
  ...(detach.retainedUnknownPath ? { retainedUnknownPath: detach.retainedUnknownPath } : {}),
1135
-
1136
1180
  transcriptIdentity,
1137
1181
  };
1138
1182
  }
@@ -1173,6 +1217,9 @@ export class FileSessionStorage implements SessionStorage {
1173
1217
  artifactsIdentity,
1174
1218
  detachedArtifactsPath: retainedRoot,
1175
1219
  artifactsTree: retainedTree,
1220
+ ...((removal as typeof removal & { payloadDurable?: boolean }).payloadDurable === true
1221
+ ? { artifactsPayloadDurable: true as const }
1222
+ : {}),
1176
1223
  ...((removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath)
1177
1224
  ? { retainedSuccessorPath: removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath }
1178
1225
  : {}),
@@ -1223,6 +1270,13 @@ export class FileSessionStorage implements SessionStorage {
1223
1270
  quarantineName: path.basename(plannedTranscriptPath),
1224
1271
  });
1225
1272
  if (!deletion.ok) {
1273
+ if (
1274
+ deletion.code === "cleanup_pending" &&
1275
+ (deletion as typeof deletion & { payloadDurable?: boolean }).payloadDurable === true &&
1276
+ deletion.retainedSuccessorPath === undefined &&
1277
+ deletion.retainedUnknownPath === undefined
1278
+ )
1279
+ return { kind: "deleted" };
1226
1280
  const error = exactUnlinkFailure(deletion);
1227
1281
  const retainedAuthority =
1228
1282
  deletion.detachedPath ||
@@ -1234,6 +1288,9 @@ export class FileSessionStorage implements SessionStorage {
1234
1288
  kind: "cleanup_pending",
1235
1289
  phase: "transcript",
1236
1290
  error,
1291
+ ...((deletion as typeof deletion & { payloadDurable?: boolean }).payloadDurable === true
1292
+ ? { transcriptPayloadDurable: true as const }
1293
+ : {}),
1237
1294
  transcriptIdentity,
1238
1295
  detachedTranscriptPath: deletion.detachedPath ?? detachedTranscriptPath,
1239
1296
  ...((deletion.retainedSuccessorPath ?? retainedTranscriptSuccessorPath)
@@ -1288,6 +1345,13 @@ export class FileSessionStorage implements SessionStorage {
1288
1345
  quarantineName: path.basename(plannedTranscriptPath),
1289
1346
  });
1290
1347
  if (!deletion.ok) {
1348
+ if (
1349
+ deletion.code === "cleanup_pending" &&
1350
+ (deletion as typeof deletion & { payloadDurable?: boolean }).payloadDurable === true &&
1351
+ deletion.retainedSuccessorPath === undefined &&
1352
+ deletion.retainedUnknownPath === undefined
1353
+ )
1354
+ return { kind: "deleted" };
1291
1355
  const error = exactUnlinkFailure(deletion);
1292
1356
  const retainedAuthority =
1293
1357
  deletion.detachedPath ||
@@ -1299,6 +1363,9 @@ export class FileSessionStorage implements SessionStorage {
1299
1363
  kind: "cleanup_pending",
1300
1364
  phase: "transcript",
1301
1365
  error,
1366
+ ...((deletion as typeof deletion & { payloadDurable?: boolean }).payloadDurable === true
1367
+ ? { transcriptPayloadDurable: true as const }
1368
+ : {}),
1302
1369
  transcriptIdentity,
1303
1370
  detachedTranscriptPath: deletion.detachedPath,
1304
1371
  ...((deletion.retainedSuccessorPath ?? retainedTranscriptSuccessorPath)
@@ -29,6 +29,8 @@ import {
29
29
  canApplyComposerSubmission,
30
30
  type InteractiveModeContext,
31
31
  } from "../modes/types";
32
+ import { ChatDaemonController } from "../sdk/bus/chat-daemon-control";
33
+ import type { NotificationProvider } from "../sdk/bus/config";
32
34
  import {
33
35
  buildNotificationStatusReport,
34
36
  checkNotificationHealth,
@@ -39,6 +41,7 @@ import {
39
41
  recoverNotifications,
40
42
  sendNotificationTest,
41
43
  } from "../sdk/bus/notification-service";
44
+ import { TelegramDaemonController } from "../sdk/bus/telegram-daemon-control";
42
45
  import { computeCacheMissCostSummary, formatCacheMissSummaryLines } from "../session/cache-economics";
43
46
  import { formatModelOnboardingGuidance } from "../setup/model-onboarding-guidance";
44
47
  import {
@@ -498,6 +501,48 @@ function buildChangelogCommandOutput(showFull: boolean): string {
498
501
  return `${title}\n\n${changelogMarkdown}${hint}`;
499
502
  }
500
503
 
504
+ type NotifyServiceArgs = { provider?: NotificationProvider; probe: boolean; message?: string } | { error: string };
505
+
506
+ function isNotificationProvider(value: string): value is NotificationProvider {
507
+ return value === "telegram" || value === "discord" || value === "slack";
508
+ }
509
+
510
+ function parseNotifyServiceArgs(input: string, allowMessage: boolean): NotifyServiceArgs {
511
+ const tokens = input.trim().split(/\s+/).filter(Boolean);
512
+ let provider: NotificationProvider | undefined;
513
+ let probe = false;
514
+ const message: string[] = [];
515
+ for (let index = 0; index < tokens.length; index += 1) {
516
+ const token = tokens[index]!;
517
+ if (token === "--probe") {
518
+ if (allowMessage) return { error: "--probe is valid only for /notify health." };
519
+ probe = true;
520
+ continue;
521
+ }
522
+ if (token === "--provider" || token.startsWith("--provider=")) {
523
+ const value = token === "--provider" ? tokens[++index] : token.slice("--provider=".length);
524
+ if (!value || !isNotificationProvider(value)) {
525
+ return { error: "--provider must be telegram, discord, or slack." };
526
+ }
527
+ if (provider && provider !== value) return { error: "Conflicting notification providers were supplied." };
528
+ provider = value;
529
+ continue;
530
+ }
531
+ if (!provider && message.length === 0 && isNotificationProvider(token)) {
532
+ provider = token;
533
+ continue;
534
+ }
535
+ if (token.startsWith("--")) return { error: `Unknown notification option: ${token}` };
536
+ message.push(token);
537
+ }
538
+ if (!allowMessage && message.length > 0) return { error: "Health accepts only a provider and --probe." };
539
+ return {
540
+ ...(provider ? { provider } : {}),
541
+ probe,
542
+ ...(message.length > 0 ? { message: message.join(" ") } : {}),
543
+ };
544
+ }
545
+
501
546
  const shutdownHandlerTui = (_command: ParsedSlashCommand, runtime: TuiSlashCommandRuntime): SlashCommandResult => {
502
547
  runtime.ctx.editor.setText("");
503
548
  void runtime.ctx.shutdown();
@@ -514,8 +559,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
514
559
  { name: "on", description: "Enable notifications for this session" },
515
560
  { name: "off", description: "Disable notifications for this session" },
516
561
  { name: "status", description: "Show notification configuration (no secrets)" },
517
- { name: "health", description: "Config, daemon-ownership and endpoint health" },
518
- { name: "test", description: "Send a test notification", usage: "[message]" },
562
+ {
563
+ name: "health",
564
+ description: "Config, ownership, endpoint, and selected-provider health",
565
+ usage: "[provider] [--probe]",
566
+ },
567
+ { name: "test", description: "Send a test notification", usage: "[provider|--provider provider] [message]" },
519
568
  { name: "recovery", description: "Clear dead-owner locks and stale endpoint files" },
520
569
  { name: "setup", description: "How to pair a Telegram bot (run in a terminal)" },
521
570
  ],
@@ -541,12 +590,41 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
541
590
  await runtime.output(formatNotificationStatusReport(buildNotificationStatusReport(runtime.settings)));
542
591
  return commandConsumed();
543
592
  case "health": {
544
- const report = await checkNotificationHealth({ settings: runtime.settings, stateRoot });
593
+ const parsed = parseNotifyServiceArgs(rest, false);
594
+ if ("error" in parsed) {
595
+ return usage(`Usage: /notify health [telegram|discord|slack] [--probe]\n${parsed.error}`, runtime);
596
+ }
597
+ const report = await checkNotificationHealth({
598
+ settings: runtime.settings,
599
+ stateRoot,
600
+ provider: parsed.provider,
601
+ probe: parsed.probe,
602
+ });
545
603
  await runtime.output(formatNotificationHealthReport(report));
546
604
  return commandConsumed();
547
605
  }
548
606
  case "test": {
549
- const result = await sendNotificationTest({ settings: runtime.settings, text: rest || undefined });
607
+ const parsed = parseNotifyServiceArgs(rest, true);
608
+ if ("error" in parsed) {
609
+ return usage(
610
+ `Usage: /notify test [telegram|discord|slack|--provider provider] [message]\n${parsed.error}`,
611
+ runtime,
612
+ );
613
+ }
614
+ const result = await sendNotificationTest({
615
+ settings: runtime.settings,
616
+ provider: parsed.provider,
617
+ text: parsed.message,
618
+ deps: {
619
+ providerRuntimeStatus: async provider => {
620
+ const status =
621
+ provider === "telegram"
622
+ ? await new TelegramDaemonController(runtime.settings).status()
623
+ : await new ChatDaemonController(runtime.settings, provider).status();
624
+ return status.health === "running" ? "ready" : "inactive";
625
+ },
626
+ },
627
+ });
550
628
  await runtime.output(formatNotificationTestResult(result));
551
629
  return commandConsumed();
552
630
  }
package/src/task/index.ts CHANGED
@@ -935,8 +935,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
935
935
  ): { ok: boolean; disposition?: DuplicateDisposition; error?: string; identity: DuplicateIdentity } => {
936
936
  const identity = duplicateIdentityForTask(task, params.agent, ownerId, parentSession);
937
937
  const key = duplicateIdentityKey(identity);
938
- const predecessors = manager
939
- .getSubagentRecords({ ownerId })
938
+ const predecessors = (manager.getSubagentRecords?.({ ownerId }) ?? [])
940
939
  .filter(record => {
941
940
  if (record.subagentId === selfSubagentId || record.subagentId === subagentId) return false;
942
941
  if (record.status !== "running" && record.status !== "paused" && record.status !== "queued")
@@ -1139,7 +1138,14 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1139
1138
  }
1140
1139
  break;
1141
1140
  }
1142
- if (frozenForkSeed) frozenForkSeeds.set(uniqueId, frozenForkSeed);
1141
+ if (frozenForkSeed) {
1142
+ frozenForkSeeds.set(uniqueId, frozenForkSeed);
1143
+ // The async callback may execute through a compatibility path that retains
1144
+ // the caller's task id instead of the allocated artifact id. Preserve the
1145
+ // same immutable seed under both identities so detached execution never
1146
+ // rebuilds context after dispatch.
1147
+ frozenForkSeeds.set(taskItem.id, frozenForkSeed);
1148
+ }
1143
1149
  const singleParams: TaskParams = { ...params, tasks: [taskItem] };
1144
1150
  const label = uniqueId;
1145
1151
  try {
package/src/tools/ask.ts CHANGED
@@ -1613,7 +1613,7 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1613
1613
  signal,
1614
1614
  initialSelection,
1615
1615
  navigation: options?.navigation,
1616
- scrollTitleRows: isDeepInterviewQuestion ? DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS : undefined,
1616
+ scrollTitleRows: DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS,
1617
1617
  otherOptionLabel,
1618
1618
  autoSelectOnTimeout: !intentContract(q.deepInterview) && !intentReview(q.deepInterview),
1619
1619
  clarificationOptionLabel,
package/src/tools/bash.ts CHANGED
@@ -201,7 +201,7 @@ function formatBashFailureMessage(
201
201
  const bodyText = removeTrailingFailureCause(text, statusCause);
202
202
  const suffixParts: string[] = [];
203
203
  const reference = artifactReferenceForResult(result);
204
- if (reference) suffixParts.push(reference);
204
+ if (reference && !artifactReferenceIsReachable(text, result)) suffixParts.push(reference);
205
205
  const writerNotice = artifactWriterFailureNotice(result);
206
206
  if (writerNotice) suffixParts.push(writerNotice);
207
207
  if (statusCause) suffixParts.push(statusCause);
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
4
4
  import { z } from "zod/v4";
5
5
  import { getTelegramFileSink } from "../sdk/bus/attachment-registry";
6
- import { getNotificationConfig, isGloballyConfigured } from "../sdk/bus/config";
6
+ import { getNotificationConfig, isProviderEffectivelyEnabled } from "../sdk/bus/config";
7
7
  import type { ToolSession } from "./index";
8
8
 
9
9
  const TELEGRAM_SEND_MAX_FILE_BYTES = 50 * 1024 * 1024;
@@ -41,7 +41,9 @@ export class TelegramSendTool implements AgentTool<typeof telegramSendSchema, Te
41
41
  constructor(private readonly session: ToolSession) {}
42
42
 
43
43
  static createIf(session: ToolSession): TelegramSendTool | null {
44
- return isGloballyConfigured(getNotificationConfig(session.settings)) ? new TelegramSendTool(session) : null;
44
+ return isProviderEffectivelyEnabled(getNotificationConfig(session.settings), "telegram")
45
+ ? new TelegramSendTool(session)
46
+ : null;
45
47
  }
46
48
 
47
49
  /**