@gajae-code/coding-agent 0.9.2 → 0.9.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/types/build-metadata.d.ts +7 -0
  3. package/dist/types/capability/mcp.d.ts +3 -1
  4. package/dist/types/commands/mcp.d.ts +1 -1
  5. package/dist/types/config/keybindings.d.ts +2 -2
  6. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  7. package/dist/types/modes/components/status-line/types.d.ts +6 -0
  8. package/dist/types/modes/components/status-line.d.ts +1 -0
  9. package/dist/types/modes/components/welcome.d.ts +1 -0
  10. package/dist/types/modes/rpc/rpc-client.d.ts +7 -2
  11. package/dist/types/modes/utils/ui-helpers.d.ts +12 -0
  12. package/dist/types/notifications/telegram-daemon.d.ts +2 -0
  13. package/dist/types/runtime-mcp/config-writer.d.ts +4 -3
  14. package/dist/types/runtime-mcp/types.d.ts +5 -4
  15. package/dist/types/session/agent-session.d.ts +16 -0
  16. package/dist/types/session/session-manager.d.ts +1 -0
  17. package/dist/types/tools/skill.d.ts +9 -8
  18. package/dist/types/utils/linkedom.d.ts +4 -0
  19. package/dist/types/utils/pasted-image-path.d.ts +5 -3
  20. package/package.json +11 -7
  21. package/scripts/build-binary.ts +2 -40
  22. package/scripts/compile-args.ts +96 -0
  23. package/scripts/generate-docs-index.ts +1 -0
  24. package/src/build-metadata.ts +104 -0
  25. package/src/capability/mcp.ts +3 -1
  26. package/src/cli/mcp-cli.ts +108 -26
  27. package/src/cli.ts +21 -0
  28. package/src/commands/mcp.ts +8 -8
  29. package/src/commands/stats.ts +6 -2
  30. package/src/config/keybindings.ts +2 -2
  31. package/src/config/mcp-schema.json +21 -17
  32. package/src/coordinator-mcp/server.ts +7 -4
  33. package/src/discovery/builtin.ts +14 -0
  34. package/src/discovery/mcp-json.ts +14 -0
  35. package/src/eval/js/executor.ts +7 -1
  36. package/src/gjc-runtime/state-runtime.ts +105 -12
  37. package/src/gjc-runtime/workflow-manifest.generated.json +4 -28
  38. package/src/gjc-runtime/workflow-manifest.ts +1 -7
  39. package/src/internal-urls/docs-index.generated.ts +3 -2
  40. package/src/internal-urls/gjc-protocol.ts +14 -1
  41. package/src/modes/components/assistant-message.ts +1 -1
  42. package/src/modes/components/hook-selector.ts +4 -0
  43. package/src/modes/components/session-selector.ts +2 -2
  44. package/src/modes/components/status-line/presets.ts +5 -7
  45. package/src/modes/components/status-line/segments.ts +14 -0
  46. package/src/modes/components/status-line/types.ts +6 -0
  47. package/src/modes/components/status-line.ts +13 -6
  48. package/src/modes/components/tool-execution.ts +1 -1
  49. package/src/modes/components/welcome.ts +4 -1
  50. package/src/modes/controllers/command-controller.ts +1 -1
  51. package/src/modes/controllers/event-controller.ts +9 -9
  52. package/src/modes/controllers/input-controller.ts +8 -2
  53. package/src/modes/controllers/selector-controller.ts +7 -3
  54. package/src/modes/interactive-mode.ts +12 -4
  55. package/src/modes/rpc/rpc-client.ts +30 -6
  56. package/src/modes/utils/ui-helpers.ts +154 -32
  57. package/src/notifications/telegram-daemon.ts +28 -2
  58. package/src/prompts/tools/skill.md +2 -2
  59. package/src/runtime-mcp/config-writer.ts +4 -3
  60. package/src/runtime-mcp/config.ts +1 -0
  61. package/src/runtime-mcp/types.ts +5 -4
  62. package/src/session/agent-session.ts +117 -3
  63. package/src/session/session-manager.ts +68 -40
  64. package/src/slash-commands/builtin-registry.ts +9 -4
  65. package/src/tools/bash-interactive.ts +9 -4
  66. package/src/tools/browser/readable.ts +2 -2
  67. package/src/tools/fetch.ts +5 -5
  68. package/src/tools/skill.ts +14 -11
  69. package/src/utils/linkedom.ts +14 -0
  70. package/src/utils/pasted-image-path.ts +23 -9
  71. package/src/web/scrapers/arxiv.ts +2 -2
  72. package/src/web/scrapers/go-pkg.ts +2 -2
  73. package/src/web/scrapers/iacr.ts +2 -2
  74. package/src/web/scrapers/readthedocs.ts +2 -2
  75. package/src/web/scrapers/twitter.ts +3 -2
  76. package/src/web/scrapers/wikipedia.ts +2 -2
  77. package/src/export/html/template.css +0 -1060
  78. package/src/export/html/template.html +0 -47
  79. package/src/export/html/template.js +0 -2314
  80. package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
  81. package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
  82. package/vendor/insane-search/engine/tests/test_u1.py +0 -200
  83. package/vendor/insane-search/engine/tests/test_u4.py +0 -131
  84. package/vendor/insane-search/engine/tests/test_u5.py +0 -163
  85. package/vendor/insane-search/engine/tests/test_u7.py +0 -124
  86. package/vendor/insane-search/engine/tests/test_u8.py +0 -216
@@ -64,6 +64,127 @@ type QueuedMessages = {
64
64
  followUp: string[];
65
65
  };
66
66
 
67
+ const CHAT_CHILD_CAP = 400;
68
+ const CHAT_COLLAPSE_BATCH_SIZE = 300;
69
+
70
+ const chatChildAddedAt = new WeakMap<Component, number>();
71
+
72
+ export class CollapsedChatHistoryComponent implements Component {
73
+ readonly isCollapsedChatHistory = true;
74
+ count: number;
75
+ startTime: number;
76
+ endTime: number;
77
+
78
+ constructor(count: number, startTime: number, endTime: number) {
79
+ this.count = count;
80
+ this.startTime = startTime;
81
+ this.endTime = endTime;
82
+ }
83
+
84
+ merge(count: number, startTime: number, endTime: number): void {
85
+ this.count += count;
86
+ this.startTime = Math.min(this.startTime, startTime);
87
+ this.endTime = Math.max(this.endTime, endTime);
88
+ }
89
+
90
+ render(_width: number): string[] {
91
+ const label = `[${this.count} earlier messages collapsed — ${formatChatCollapseTime(this.startTime)} to ${formatChatCollapseTime(this.endTime)}]`;
92
+ return [theme.fg("dim", label)];
93
+ }
94
+
95
+ invalidate(): void {}
96
+ }
97
+
98
+ function formatChatCollapseTime(timestamp: number): string {
99
+ return new Date(timestamp).toLocaleTimeString(undefined, {
100
+ hour: "2-digit",
101
+ minute: "2-digit",
102
+ hour12: false,
103
+ });
104
+ }
105
+
106
+ function isCollapsedChatHistory(component: Component): component is CollapsedChatHistoryComponent {
107
+ return component instanceof CollapsedChatHistoryComponent;
108
+ }
109
+
110
+ function isActiveChatChild(ctx: InteractiveModeContext, component: Component): boolean {
111
+ if ([...ctx.pendingTools.values()].includes(component as never)) return true;
112
+ if (ctx.pendingBashComponents.includes(component as never)) return true;
113
+ if (ctx.pendingPythonComponents.includes(component as never)) return true;
114
+ return component === ctx.bashComponent || component === ctx.pythonComponent || component === ctx.streamingComponent;
115
+ }
116
+
117
+ function getChatChildTime(component: Component): number {
118
+ return chatChildAddedAt.get(component) ?? Date.now();
119
+ }
120
+
121
+ export function addChatChild(ctx: InteractiveModeContext, component: Component): void {
122
+ ctx.chatContainer.addChild(component);
123
+ chatChildAddedAt.set(component, Date.now());
124
+ trimChatChildren(ctx);
125
+ }
126
+
127
+ export function trimChatChildren(ctx: InteractiveModeContext): void {
128
+ const children = ctx.chatContainer.children;
129
+
130
+ // Collapse the oldest completed children into history placeholders until the cap is
131
+ // satisfied or no further reduction is structurally possible. Active (pending/running)
132
+ // children and existing placeholders are never collapsed, so a conversation whose
133
+ // top-level children are dominated by active components may legitimately exceed the cap.
134
+ let scanFrom = 0;
135
+ while (children.length > CHAT_CHILD_CAP) {
136
+ // Advance to the oldest collapsible completed child, skipping active children and
137
+ // existing placeholders so the cap is enforced even when the oldest child is active.
138
+ while (
139
+ scanFrom < children.length &&
140
+ (isActiveChatChild(ctx, children[scanFrom]) || isCollapsedChatHistory(children[scanFrom]))
141
+ ) {
142
+ scanFrom++;
143
+ }
144
+ if (scanFrom >= children.length) return; // only active/placeholder children remain
145
+
146
+ // Extend over the oldest contiguous run of completed children (up to the batch size).
147
+ let collapseEndExclusive = scanFrom;
148
+ const maxEnd = Math.min(children.length, scanFrom + CHAT_COLLAPSE_BATCH_SIZE);
149
+ while (
150
+ collapseEndExclusive < maxEnd &&
151
+ !isActiveChatChild(ctx, children[collapseEndExclusive]) &&
152
+ !isCollapsedChatHistory(children[collapseEndExclusive])
153
+ ) {
154
+ collapseEndExclusive++;
155
+ }
156
+ const collapseCount = collapseEndExclusive - scanFrom;
157
+
158
+ const prev = scanFrom > 0 ? children[scanFrom - 1] : undefined;
159
+ const mergeTarget = prev && isCollapsedChatHistory(prev) ? prev : undefined;
160
+ // A lone completed child with no adjacent placeholder to merge into cannot reduce
161
+ // the child count (collapsing it yields a 1-count placeholder). Skip past it.
162
+ if (!mergeTarget && collapseCount <= 1) {
163
+ scanFrom += Math.max(collapseCount, 1);
164
+ continue;
165
+ }
166
+
167
+ const collapsed = children.slice(scanFrom, collapseEndExclusive);
168
+ const startTime = getChatChildTime(collapsed[0]);
169
+ const endTime = getChatChildTime(collapsed[collapsed.length - 1]);
170
+ for (const child of collapsed) {
171
+ child.dispose?.();
172
+ }
173
+
174
+ if (mergeTarget) {
175
+ mergeTarget.merge(collapseCount, startTime, endTime);
176
+ children.splice(scanFrom, collapseCount);
177
+ chatChildAddedAt.set(mergeTarget, mergeTarget.startTime);
178
+ // scanFrom stays; whatever follows the merged run is next.
179
+ } else {
180
+ const placeholder = new CollapsedChatHistoryComponent(collapseCount, startTime, endTime);
181
+ children.splice(scanFrom, collapseCount, placeholder);
182
+ chatChildAddedAt.set(placeholder, startTime);
183
+ scanFrom++; // step past the freshly created placeholder
184
+ }
185
+ }
186
+ }
187
+
67
188
  export class UiHelpers {
68
189
  constructor(private ctx: InteractiveModeContext) {}
69
190
 
@@ -101,8 +222,8 @@ export class UiHelpers {
101
222
 
102
223
  const spacer = new Spacer(1);
103
224
  const text = new Text(rendered, 1, 0);
104
- this.ctx.chatContainer.addChild(spacer);
105
- this.ctx.chatContainer.addChild(text);
225
+ addChatChild(this.ctx, spacer);
226
+ addChatChild(this.ctx, text);
106
227
  this.ctx.lastStatusSpacer = spacer;
107
228
  this.ctx.lastStatusText = text;
108
229
  this.ctx.ui.requestRender();
@@ -118,7 +239,7 @@ export class UiHelpers {
118
239
  component.setComplete(message.exitCode, message.cancelled, {
119
240
  truncation: message.meta?.truncation,
120
241
  });
121
- this.ctx.chatContainer.addChild(component);
242
+ addChatChild(this.ctx, component);
122
243
  break;
123
244
  }
124
245
  case "pythonExecution": {
@@ -129,7 +250,7 @@ export class UiHelpers {
129
250
  component.setComplete(message.exitCode, message.cancelled, {
130
251
  truncation: message.meta?.truncation,
131
252
  });
132
- this.ctx.chatContainer.addChild(component);
253
+ addChatChild(this.ctx, component);
133
254
  break;
134
255
  }
135
256
  case "hookMessage":
@@ -173,14 +294,14 @@ export class UiHelpers {
173
294
  ]
174
295
  .filter(Boolean)
175
296
  .join(" ");
176
- this.ctx.chatContainer.addChild(new Text(line, 1, 0));
297
+ addChatChild(this.ctx, new Text(line, 1, 0));
177
298
  }
178
299
  break;
179
300
  }
180
301
  if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
181
302
  const component = new SkillMessageComponent(message as CustomMessage<SkillPromptDetails>);
182
303
  component.setExpanded(this.ctx.toolOutputExpanded);
183
- this.ctx.chatContainer.addChild(component);
304
+ addChatChild(this.ctx, component);
184
305
  break;
185
306
  }
186
307
  if (
@@ -217,12 +338,12 @@ export class UiHelpers {
217
338
  const components: Component[] = [];
218
339
  const header = `${theme.fg("accent", `[IRC] ${arrow}`)}`;
219
340
  const headerComponent = new Text(header, 1, 0);
220
- this.ctx.chatContainer.addChild(headerComponent);
341
+ addChatChild(this.ctx, headerComponent);
221
342
  components.push(headerComponent);
222
343
  if (body) {
223
344
  for (const line of body.split("\n")) {
224
345
  const lineComponent = new Text(theme.fg("muted", ` ${line}`), 0, 0);
225
- this.ctx.chatContainer.addChild(lineComponent);
346
+ addChatChild(this.ctx, lineComponent);
226
347
  components.push(lineComponent);
227
348
  }
228
349
  }
@@ -240,12 +361,12 @@ export class UiHelpers {
240
361
  const components: Component[] = [];
241
362
  const header = `${theme.fg("accent", `[Steer ${details?.state ?? "queued"}] ${details?.from ?? "?"} ⇨ ${details?.to ?? "?"}`)}`;
242
363
  const headerComponent = new Text(header, 1, 0);
243
- this.ctx.chatContainer.addChild(headerComponent);
364
+ addChatChild(this.ctx, headerComponent);
244
365
  components.push(headerComponent);
245
366
  if (details?.body) {
246
367
  for (const line of details.body.split("\n")) {
247
368
  const lineComponent = new Text(theme.fg("muted", ` ${line}`), 0, 0);
248
- this.ctx.chatContainer.addChild(lineComponent);
369
+ addChatChild(this.ctx, lineComponent);
249
370
  components.push(lineComponent);
250
371
  }
251
372
  }
@@ -255,22 +376,22 @@ export class UiHelpers {
255
376
  // Both HookMessage and CustomMessage have the same structure, cast for compatibility
256
377
  const component = new CustomMessageComponent(message as CustomMessage<unknown>, renderer);
257
378
  component.setExpanded(this.ctx.toolOutputExpanded);
258
- this.ctx.chatContainer.addChild(component);
379
+ addChatChild(this.ctx, component);
259
380
  }
260
381
  break;
261
382
  }
262
383
  case "compactionSummary": {
263
- this.ctx.chatContainer.addChild(new Spacer(1));
384
+ addChatChild(this.ctx, new Spacer(1));
264
385
  const component = new CompactionSummaryMessageComponent(message);
265
386
  component.setExpanded(this.ctx.toolOutputExpanded);
266
- this.ctx.chatContainer.addChild(component);
387
+ addChatChild(this.ctx, component);
267
388
  break;
268
389
  }
269
390
  case "branchSummary": {
270
- this.ctx.chatContainer.addChild(new Spacer(1));
391
+ addChatChild(this.ctx, new Spacer(1));
271
392
  const component = new BranchSummaryMessageComponent(message);
272
393
  component.setExpanded(this.ctx.toolOutputExpanded);
273
- this.ctx.chatContainer.addChild(component);
394
+ addChatChild(this.ctx, component);
274
395
  break;
275
396
  }
276
397
  case "fileMention": {
@@ -291,7 +412,7 @@ export class UiHelpers {
291
412
  "accent",
292
413
  file.path,
293
414
  )} ${theme.fg("dim", suffix)}`;
294
- this.ctx.chatContainer.addChild(new Text(text, 0, 0));
415
+ addChatChild(this.ctx, new Text(text, 0, 0));
295
416
  }
296
417
  break;
297
418
  }
@@ -301,7 +422,7 @@ export class UiHelpers {
301
422
  if (textContent) {
302
423
  const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
303
424
  const userComponent = new UserMessageComponent(textContent, isSynthetic);
304
- this.ctx.chatContainer.addChild(userComponent);
425
+ addChatChild(this.ctx, userComponent);
305
426
  if (options?.populateHistory && message.role === "user" && !isSynthetic) {
306
427
  this.ctx.editor.addToHistory(textContent);
307
428
  }
@@ -312,7 +433,7 @@ export class UiHelpers {
312
433
  const assistantComponent = new AssistantMessageComponent(message, this.ctx.hideThinkingBlock, () =>
313
434
  this.ctx.ui.requestRender(),
314
435
  );
315
- this.ctx.chatContainer.addChild(assistantComponent);
436
+ addChatChild(this.ctx, assistantComponent);
316
437
  break;
317
438
  }
318
439
  case "toolResult": {
@@ -392,7 +513,7 @@ export class UiHelpers {
392
513
  showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
393
514
  });
394
515
  readGroup.setExpanded(this.ctx.toolOutputExpanded);
395
- this.ctx.chatContainer.addChild(readGroup);
516
+ addChatChild(this.ctx, readGroup);
396
517
  }
397
518
  readGroup.updateArgs(content.arguments, content.id);
398
519
  readGroup.updateResult(
@@ -434,7 +555,7 @@ export class UiHelpers {
434
555
  content.id,
435
556
  );
436
557
  component.setExpanded(this.ctx.toolOutputExpanded);
437
- this.ctx.chatContainer.addChild(component);
558
+ addChatChild(this.ctx, component);
438
559
 
439
560
  if (hasErrorStop && errorMessage) {
440
561
  component.updateResult(
@@ -472,7 +593,7 @@ export class UiHelpers {
472
593
  showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
473
594
  });
474
595
  readGroup.setExpanded(this.ctx.toolOutputExpanded);
475
- this.ctx.chatContainer.addChild(readGroup);
596
+ addChatChild(this.ctx, readGroup);
476
597
  }
477
598
  const args = readToolCallArgs.get(message.toolCallId);
478
599
  if (args) {
@@ -539,7 +660,7 @@ export class UiHelpers {
539
660
  }
540
661
  if (preservedChatChildren && preservedChatChildren.length > 0) {
541
662
  for (const child of preservedChatChildren) {
542
- this.ctx.chatContainer.addChild(child);
663
+ addChatChild(this.ctx, child);
543
664
  }
544
665
  this.ctx.ui.requestRender();
545
666
  }
@@ -559,8 +680,8 @@ export class UiHelpers {
559
680
  process.stderr.write(`Error: ${errorMessage}\n`);
560
681
  return;
561
682
  }
562
- this.ctx.chatContainer.addChild(new Spacer(1));
563
- this.ctx.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
683
+ addChatChild(this.ctx, new Spacer(1));
684
+ addChatChild(this.ctx, new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
564
685
  this.ctx.ui.requestRender();
565
686
  }
566
687
 
@@ -569,15 +690,16 @@ export class UiHelpers {
569
690
  process.stderr.write(`Warning: ${warningMessage}\n`);
570
691
  return;
571
692
  }
572
- this.ctx.chatContainer.addChild(new Spacer(1));
573
- this.ctx.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
693
+ addChatChild(this.ctx, new Spacer(1));
694
+ addChatChild(this.ctx, new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
574
695
  this.ctx.ui.requestRender();
575
696
  }
576
697
 
577
698
  showNewVersionNotification(newVersion: string): void {
578
- this.ctx.chatContainer.addChild(new Spacer(1));
579
- this.ctx.chatContainer.addChild(new DynamicBorder(text => theme.fg("warning", text)));
580
- this.ctx.chatContainer.addChild(
699
+ addChatChild(this.ctx, new Spacer(1));
700
+ addChatChild(this.ctx, new DynamicBorder(text => theme.fg("warning", text)));
701
+ addChatChild(
702
+ this.ctx,
581
703
  new Text(
582
704
  theme.bold(theme.fg("warning", "Update Available")) +
583
705
  "\n" +
@@ -587,7 +709,7 @@ export class UiHelpers {
587
709
  0,
588
710
  ),
589
711
  );
590
- this.ctx.chatContainer.addChild(new DynamicBorder(text => theme.fg("warning", text)));
712
+ addChatChild(this.ctx, new DynamicBorder(text => theme.fg("warning", text)));
591
713
  this.ctx.ui.requestRender();
592
714
  }
593
715
 
@@ -853,12 +975,12 @@ export class UiHelpers {
853
975
  // removeChild() would tear them down before re-adding.
854
976
  for (const component of this.ctx.pendingBashComponents) {
855
977
  this.ctx.pendingMessagesContainer.detachChild(component);
856
- this.ctx.chatContainer.addChild(component);
978
+ addChatChild(this.ctx, component);
857
979
  }
858
980
  this.ctx.pendingBashComponents = [];
859
981
  for (const component of this.ctx.pendingPythonComponents) {
860
982
  this.ctx.pendingMessagesContainer.detachChild(component);
861
- this.ctx.chatContainer.addChild(component);
983
+ addChatChild(this.ctx, component);
862
984
  }
863
985
  this.ctx.pendingPythonComponents = [];
864
986
  }
@@ -146,6 +146,7 @@ const TYPING_REFRESH_INTERVAL_MS = 4_000;
146
146
  const QUEUED_REACTION = "👀";
147
147
  const PENDING_TOPIC_FRAME_LIMIT = 20;
148
148
  const SEEN_UPDATE_ID_LIMIT = 1_000;
149
+ const ORPHAN_TOPIC_GRACE_MS = 60_000;
149
150
  const CONSUMED_REACTION = "✅";
150
151
 
151
152
  function splitTelegramPlainText(text: string, max = TELEGRAM_MESSAGE_LIMIT): string[] {
@@ -1414,24 +1415,33 @@ export class TelegramNotificationDaemon {
1414
1415
  async scanRoots(): Promise<void> {
1415
1416
  const paths = daemonPaths(this.opts.settings.getAgentDir());
1416
1417
  const rootState = await readJson<{ roots?: string[] }>(this.fsImpl, paths.roots);
1418
+ const endpointSessionIds = new Set<string>();
1419
+ let allRootsReadable = true;
1417
1420
  for (const root of rootState?.roots ?? []) {
1418
1421
  const dir = path.join(root, "notifications");
1419
1422
  let files: string[];
1420
1423
  try {
1421
1424
  files = await this.fsImpl.readdir(dir);
1422
1425
  } catch {
1426
+ allRootsReadable = false;
1423
1427
  continue;
1424
1428
  }
1425
1429
  for (const file of files.filter(item => item.endsWith(".json"))) {
1426
1430
  const sessionId = path.basename(file, ".json");
1431
+ endpointSessionIds.add(sessionId);
1427
1432
  if (this.sessions.has(sessionId)) continue;
1428
1433
  try {
1429
1434
  const endpoint = readEndpoint(path.join(dir, file));
1430
1435
  // Skip endpoint files whose owning process is gone or that are
1431
1436
  // explicitly stale (e.g. a hard-closed session): reconnecting
1432
- // would chase a dead, token-bearing record forever.
1437
+ // would chase a dead, token-bearing record forever. Once the
1438
+ // associated topic is past the grace window, reap it through the
1439
+ // same best-effort delete path as graceful session shutdown.
1433
1440
  const pidAlive = this.opts.pidAlive ?? defaultPidAlive;
1434
- if (endpoint.stale || (endpoint.pid !== undefined && !pidAlive(endpoint.pid))) continue;
1441
+ if (endpoint.stale || (endpoint.pid !== undefined && !pidAlive(endpoint.pid))) {
1442
+ await this.deleteOrphanedTopic(sessionId);
1443
+ continue;
1444
+ }
1435
1445
  const endpointKey = endpointGenerationKey(endpoint.url, endpoint.token);
1436
1446
  if (this.closedEndpointKeys.get(sessionId) === endpointKey) continue;
1437
1447
  this.closedEndpointKeys.delete(sessionId);
@@ -1439,6 +1449,12 @@ export class TelegramNotificationDaemon {
1439
1449
  } catch {}
1440
1450
  }
1441
1451
  }
1452
+ if (allRootsReadable) {
1453
+ for (const sessionId of this.topics.sessionIds()) {
1454
+ if (!this.sessions.has(sessionId) && !endpointSessionIds.has(sessionId))
1455
+ await this.deleteOrphanedTopic(sessionId);
1456
+ }
1457
+ }
1442
1458
  }
1443
1459
 
1444
1460
  connectSession(sessionId: string, url: string, token: string): void {
@@ -1683,6 +1699,16 @@ export class TelegramNotificationDaemon {
1683
1699
  }
1684
1700
  }
1685
1701
 
1702
+ private topicPastOrphanGrace(sessionId: string): boolean {
1703
+ const record = this.topics.get(sessionId);
1704
+ return record !== undefined && this.runtime.now() - record.createdAt >= ORPHAN_TOPIC_GRACE_MS;
1705
+ }
1706
+
1707
+ private async deleteOrphanedTopic(sessionId: string): Promise<void> {
1708
+ if (!this.topicPastOrphanGrace(sessionId)) return;
1709
+ await this.deleteTopic(sessionId);
1710
+ }
1711
+
1686
1712
  /** Best-effort delete of a session topic once its local notification endpoint shuts down. */
1687
1713
  private async deleteTopic(sessionId: string): Promise<void> {
1688
1714
  const record = this.topics.get(sessionId);
@@ -8,8 +8,8 @@ Invoke another available skill in the current turn.
8
8
  <instruction>
9
9
  - `name` is the skill name as it appears in `/skill:<name>` (e.g. `ralplan`, `ultragoal`, `team`, `deep-interview`)
10
10
  - `args` is the free-form argument string the skill would receive after `/skill:<name>` on the command line
11
- - The tool loads the callee's SKILL.md into the current turn and handles the caller→callee state handoff itself.
12
- - The chain is refused while the caller skill is still active. If your current skill has not yet reached a terminal phase, prepare it first with `gjc state <skill> write --input '{"current_phase":"handoff"}' --json`; no other handoff command is needed.
11
+ - The tool loads the callee's SKILL.md into the current turn and handles native workflow caller→callee state handoff when the caller is one of the built-in GJC workflows.
12
+ - The chain is refused while a native workflow caller is still active. If your current skill is one of `deep-interview`, `ralplan`, `ultragoal`, or `team` and has not yet reached a terminal phase, prepare it first with `gjc state <skill> write --input '{"current_phase":"handoff"}' --json`; no other handoff command is needed. Runtime project/user skills do not use `gjc state <skill>`.
13
13
  - Call once per chain step. To chain `A → B → C`, A calls `skill(B)`; B's next agent turn calls `skill(C)`.
14
14
  </instruction>
15
15
 
@@ -238,10 +238,11 @@ export async function listMCPServers(filePath: string): Promise<string[]> {
238
238
  }
239
239
 
240
240
  /**
241
- * Set a server's autoload flag (connect automatically at session startup).
241
+ * Set a server's autoload flag for explicit runtime MCP consumers.
242
242
  *
243
- * Autoload defaults to true, so turning it on removes the key to keep the
244
- * config file free of redundant fields; turning it off writes `autoload: false`.
243
+ * Autoload defaults to true for consumers that honor it, so turning it on
244
+ * removes the key to keep the config file free of redundant fields; turning it
245
+ * off writes `autoload: false`.
245
246
  *
246
247
  * @throws Error if the server doesn't exist
247
248
  */
@@ -56,6 +56,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
56
56
  };
57
57
  if (server.args) config.args = server.args;
58
58
  if (server.env) config.env = server.env;
59
+ if (server.noInheritEnv !== undefined) config.noInheritEnv = server.noInheritEnv;
59
60
  if (server.cwd) config.cwd = server.cwd;
60
61
  return config;
61
62
  }
@@ -62,9 +62,10 @@ interface MCPServerConfigBase {
62
62
  /** Whether this server is enabled (default: true) */
63
63
  enabled?: boolean;
64
64
  /**
65
- * Whether to connect this server automatically at session startup (default: true).
66
- * When false the server stays configured-but-disconnected until connected
67
- * explicitly (e.g. `/mcp connect <name>`).
65
+ * Whether an explicit runtime MCP consumer should connect this server
66
+ * automatically when that consumer starts (default: true). Normal standalone
67
+ * `gjc`, `gjc --tmux`, and print-mode sessions do not consume `gjc mcp`
68
+ * registrations today.
68
69
  */
69
70
  autoload?: boolean;
70
71
  /** Connection timeout in milliseconds (default: 30000) */
@@ -114,7 +115,7 @@ export interface MCPSseServerConfig extends MCPServerConfigBase {
114
115
  export type MCPServerConfig = MCPStdioServerConfig | MCPHttpServerConfig | MCPSseServerConfig;
115
116
 
116
117
  export const MCP_CONFIG_SCHEMA_URL =
117
- "https://raw.githubusercontent.com/can1357/gajae-code/main/packages/coding-agent/src/config/mcp-schema.json";
118
+ "https://raw.githubusercontent.com/Yeachan-Heo/gajae-code/main/packages/coding-agent/src/config/mcp-schema.json";
118
119
 
119
120
  /** Root mcp.json/.mcp.json file structure */
120
121
  export interface MCPConfigFile {