@gajae-code/coding-agent 0.12.5 → 0.12.7

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 (91) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +4 -1
  3. package/dist/types/async/job-manager.d.ts +36 -1
  4. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  5. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  6. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  7. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  8. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  9. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  10. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  11. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  12. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  13. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  14. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  15. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
  16. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  17. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  18. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  19. package/dist/types/sdk/broker/broker.d.ts +18 -0
  20. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  21. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  22. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -3
  23. package/dist/types/sdk/bus/index.d.ts +4 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  26. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  27. package/dist/types/session/agent-session.d.ts +5 -4
  28. package/dist/types/session/attempt-record-store.d.ts +19 -0
  29. package/dist/types/session/session-manager.d.ts +1 -0
  30. package/dist/types/session/session-storage.d.ts +8 -0
  31. package/dist/types/task/receipt.d.ts +1 -0
  32. package/dist/types/task/types.d.ts +43 -0
  33. package/dist/types/tools/subagent.d.ts +15 -0
  34. package/dist/types/tools/todo-write.d.ts +3 -0
  35. package/package.json +8 -8
  36. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  37. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  38. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  39. package/src/async/job-manager.ts +430 -46
  40. package/src/config/model-profiles.ts +8 -0
  41. package/src/config/settings-schema.ts +17 -3
  42. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  43. package/src/extensibility/extensions/runner.ts +80 -6
  44. package/src/extensibility/extensions/wrapper.ts +22 -15
  45. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  46. package/src/gjc-runtime/launch-tmux.ts +309 -293
  47. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  48. package/src/gjc-runtime/psmux-detect.ts +52 -6
  49. package/src/gjc-runtime/team-launch.ts +50 -2
  50. package/src/gjc-runtime/team-runtime.ts +716 -251
  51. package/src/gjc-runtime/team-store.ts +25 -7
  52. package/src/gjc-runtime/team-workers.ts +161 -1
  53. package/src/gjc-runtime/tmux-common.ts +16 -4
  54. package/src/gjc-runtime/tmux-gc.ts +37 -5
  55. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  56. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  57. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  58. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  59. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  60. package/src/gjc-runtime/ultragoal-runtime.ts +360 -156
  61. package/src/internal-urls/docs-index.generated.ts +4 -4
  62. package/src/internal-urls/local-protocol.ts +39 -18
  63. package/src/modes/acp/acp-agent.ts +27 -13
  64. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  65. package/src/modes/components/plugin-settings.ts +5 -0
  66. package/src/modes/components/settings-selector.ts +4 -0
  67. package/src/modes/controllers/selector-controller.ts +1 -0
  68. package/src/prompts/tools/monitor.md +4 -4
  69. package/src/sdk/broker/broker.ts +93 -10
  70. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  71. package/src/sdk/broker/lifecycle.ts +691 -92
  72. package/src/sdk/bus/chat-daemon-control.ts +30 -6
  73. package/src/sdk/bus/index.ts +23 -6
  74. package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
  75. package/src/sdk/bus/telegram-daemon.ts +283 -44
  76. package/src/sdk/bus/topic-registry.ts +133 -10
  77. package/src/sdk/session.ts +7 -6
  78. package/src/session/agent-session.ts +752 -339
  79. package/src/session/attempt-record-store.ts +106 -0
  80. package/src/session/internal/managed-session-scope.ts +252 -67
  81. package/src/session/internal/managed-session-storage.ts +1 -0
  82. package/src/session/session-manager.ts +7 -0
  83. package/src/session/session-storage.ts +146 -24
  84. package/src/task/index.ts +230 -19
  85. package/src/task/provider-retry-status.ts +4 -1
  86. package/src/task/receipt.ts +2 -0
  87. package/src/task/types.ts +10 -0
  88. package/src/tools/monitor.ts +7 -1
  89. package/src/tools/output-meta.ts +10 -2
  90. package/src/tools/subagent.ts +97 -75
  91. package/src/tools/todo-write.ts +47 -0
@@ -80,6 +80,11 @@ export interface LifecycleLedgerEntry {
80
80
  endpointGeneration?: number;
81
81
  responseDigest?: string;
82
82
  response?: unknown;
83
+ unresolvedCleanupResponse?: unknown;
84
+ unresolvedCleanupResponseDigest?: string;
85
+ uncertainCleanupSessionId?: string;
86
+ uncertainCleanupSessionIds?: string[];
87
+ uncertainCleanupAllSessions?: true;
83
88
  ts: number;
84
89
  }
85
90
  export type BeginResult =
@@ -95,6 +100,10 @@ export interface LifecycleLedgerLimits {
95
100
  maxLineBytes?: number;
96
101
  maxRows?: number;
97
102
  }
103
+ function canonicalCleanupSessionId(value: unknown): value is string {
104
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
105
+ }
106
+
98
107
  const DEFAULT_LIFECYCLE_LEDGER_LIMITS: Required<LifecycleLedgerLimits> = {
99
108
  maxBytes: 64 * 1024 * 1024,
100
109
  maxLineBytes: 8 * 1024 * 1024,
@@ -143,20 +152,71 @@ function canonicalJson(value: unknown): string {
143
152
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
144
153
  const record = value as Record<string, unknown>;
145
154
  return `{${Object.keys(record)
155
+ .filter(key => record[key] !== undefined)
146
156
  .sort()
147
157
  .map(key => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
148
158
  .join(",")}}`;
149
159
  }
160
+ function pendingCleanupSessionId(response: unknown): string | undefined {
161
+ if (!response || typeof response !== "object") return undefined;
162
+ const cleanup = (response as { error?: { cleanup?: { sessionId?: unknown } } }).error?.cleanup;
163
+ return canonicalCleanupSessionId(cleanup?.sessionId) ? cleanup.sessionId : undefined;
164
+ }
165
+
166
+ function hasCleanupAuthorityShape(response: unknown): boolean {
167
+ if (!response || typeof response !== "object") return false;
168
+ const error = (response as { error?: unknown }).error;
169
+ return Boolean(error && typeof error === "object" && "cleanup" in error);
170
+ }
150
171
 
151
172
  function hasValidTerminalDigests(entry: LifecycleLedgerEntry): boolean {
152
- if (!terminal(entry.state) && entry.state !== "terminal_uncertain") return true;
153
- if (
154
- (entry.state !== "terminal_uncertain" || entry.response !== undefined) &&
155
- (entry.response === undefined ||
156
- typeof entry.responseDigest !== "string" ||
157
- entry.responseDigest !== createHash("sha256").update(canonicalJson(entry.response)).digest("hex"))
158
- )
159
- return false;
173
+ const response = entry.response as { ok?: unknown; error?: { code?: unknown; cleanup?: unknown } } | undefined;
174
+ const cleanupPendingResponse =
175
+ response?.ok === false && response.error?.code === "cleanup_pending" && response.error.cleanup !== undefined;
176
+ const responseDigestRequired =
177
+ cleanupPendingResponse ||
178
+ ((terminal(entry.state) || entry.state === "terminal_uncertain") &&
179
+ (entry.state !== "terminal_uncertain" || entry.response !== undefined));
180
+ if (cleanupPendingResponse) {
181
+ const cleanupSessionId = (response.error?.cleanup as { sessionId?: unknown }).sessionId;
182
+ if (!canonicalCleanupSessionId(entry.intendedSessionId) || cleanupSessionId !== entry.intendedSessionId)
183
+ return false;
184
+ }
185
+ const expectedResponseDigest =
186
+ entry.response === undefined
187
+ ? undefined
188
+ : createHash("sha256")
189
+ .update(
190
+ canonicalJson(
191
+ cleanupPendingResponse
192
+ ? { intendedSessionId: entry.intendedSessionId, response: entry.response }
193
+ : entry.response,
194
+ ),
195
+ )
196
+ .digest("hex");
197
+ if (entry.responseDigest !== undefined) {
198
+ if (entry.response === undefined || entry.responseDigest !== expectedResponseDigest) return false;
199
+ } else if (responseDigestRequired) return false;
200
+ if (entry.unresolvedCleanupResponse !== undefined || entry.unresolvedCleanupResponseDigest !== undefined) {
201
+ const unresolvedResponse = entry.unresolvedCleanupResponse as {
202
+ error?: { cleanup?: { sessionId?: unknown } };
203
+ };
204
+ if (
205
+ entry.unresolvedCleanupResponse === undefined ||
206
+ typeof entry.unresolvedCleanupResponseDigest !== "string" ||
207
+ unresolvedResponse.error?.cleanup?.sessionId !== entry.intendedSessionId ||
208
+ entry.unresolvedCleanupResponseDigest !==
209
+ createHash("sha256")
210
+ .update(
211
+ canonicalJson({
212
+ intendedSessionId: entry.intendedSessionId,
213
+ response: entry.unresolvedCleanupResponse,
214
+ }),
215
+ )
216
+ .digest("hex")
217
+ )
218
+ return false;
219
+ }
160
220
  if (!entry.durableEffects) return true;
161
221
  const { digest, ...body } = entry.durableEffects;
162
222
  return typeof digest === "string" && digest === createHash("sha256").update(canonicalJson(body)).digest("hex");
@@ -237,8 +297,31 @@ export class LifecycleLedger {
237
297
  if (invalidHistory) {
238
298
  await this.#quarantine(line);
239
299
  invalidIdentities.add(entry.identity);
240
- if (!syntheticUncertain.has(entry.identity))
241
- syntheticUncertain.set(entry.identity, this.#uncertainFrom(prior ?? entry, prior !== undefined));
300
+ if (!syntheticUncertain.has(entry.identity)) {
301
+ const uncertain = this.#uncertainFrom(prior ?? entry, prior !== undefined);
302
+ const nestedCleanupSessionId =
303
+ pendingCleanupSessionId(entry.unresolvedCleanupResponse) ??
304
+ pendingCleanupSessionId(entry.response) ??
305
+ entry.intendedSessionId;
306
+ if (canonicalCleanupSessionId(nestedCleanupSessionId))
307
+ uncertain.uncertainCleanupSessionId = nestedCleanupSessionId;
308
+ uncertain.uncertainCleanupSessionIds = [
309
+ entry.intendedSessionId,
310
+ pendingCleanupSessionId(entry.response),
311
+ pendingCleanupSessionId(entry.unresolvedCleanupResponse),
312
+ nestedCleanupSessionId,
313
+ ].filter(
314
+ (candidate, index, candidates): candidate is string =>
315
+ canonicalCleanupSessionId(candidate) && candidates.indexOf(candidate) === index,
316
+ );
317
+ if (
318
+ hasCleanupAuthorityShape(entry.response) ||
319
+ hasCleanupAuthorityShape(entry.unresolvedCleanupResponse) ||
320
+ (entry.state === "terminal_uncertain" && entry.response === undefined)
321
+ )
322
+ uncertain.uncertainCleanupAllSessions = true;
323
+ syntheticUncertain.set(entry.identity, uncertain);
324
+ }
242
325
  continue;
243
326
  }
244
327
  this.#entries.push(entry);
@@ -336,6 +419,45 @@ export class LifecycleLedger {
336
419
  response.ok === false && response.error?.code === "cleanup_pending" && response.error.cleanup !== undefined
337
420
  );
338
421
  }
422
+ #isSessionDeleteCleanupPending(entry: LifecycleLedgerEntry): boolean {
423
+ if (!this.#isCleanupPending(entry)) return false;
424
+ const response = entry.response as {
425
+ error?: {
426
+ cleanup?: {
427
+ phase?: unknown;
428
+ sessionId?: unknown;
429
+ sessionsRoot?: unknown;
430
+ transcriptPath?: unknown;
431
+ cwd?: unknown;
432
+ };
433
+ };
434
+ };
435
+ const cleanup = response.error?.cleanup;
436
+ return (
437
+ (cleanup?.phase === "artifacts" || cleanup?.phase === "transcript" || cleanup?.phase === "metadata") &&
438
+ typeof cleanup.sessionId === "string" &&
439
+ typeof cleanup.sessionsRoot === "string" &&
440
+ typeof cleanup.transcriptPath === "string" &&
441
+ typeof cleanup.cwd === "string"
442
+ );
443
+ }
444
+
445
+ #matchesSessionDeleteTarget(
446
+ entry: LifecycleLedgerEntry,
447
+ target: { sessionId: string; sessionsRoot?: string; transcriptPath: string; cwd: string },
448
+ ): boolean {
449
+ if (!this.#isSessionDeleteCleanupPending(entry)) return false;
450
+ const response = entry.response as {
451
+ error: { cleanup: { sessionId: string; sessionsRoot: string; transcriptPath: string; cwd: string } };
452
+ };
453
+ const cleanup = response.error.cleanup;
454
+ return (
455
+ cleanup.sessionId === target.sessionId &&
456
+ (target.sessionsRoot === undefined || cleanup.sessionsRoot === target.sessionsRoot) &&
457
+ cleanup.transcriptPath === target.transcriptPath &&
458
+ cleanup.cwd === target.cwd
459
+ );
460
+ }
339
461
 
340
462
  #uncertainFrom(entry: LifecycleLedgerEntry, trusted = true): LifecycleLedgerEntry {
341
463
  if (trusted) return { ...entry, state: "terminal_uncertain", ts: Date.now() };
@@ -545,7 +667,37 @@ export class LifecycleLedger {
545
667
  const previous = this.#byIdentity.get(identity);
546
668
  if (!previous) throw new Error("Unknown lifecycle identity");
547
669
  const next = { ...previous, ...fields, state, ts: Date.now() };
548
- if (
670
+ if (this.#isCleanupPending(next)) {
671
+ next.unresolvedCleanupResponse = undefined;
672
+ next.unresolvedCleanupResponseDigest = undefined;
673
+ } else if (state === "terminal_ok") {
674
+ next.unresolvedCleanupResponse = undefined;
675
+ next.unresolvedCleanupResponseDigest = undefined;
676
+ } else if (this.#isCleanupPending(previous)) {
677
+ next.unresolvedCleanupResponse = previous.response;
678
+ next.unresolvedCleanupResponseDigest = createHash("sha256")
679
+ .update(
680
+ canonicalJson({
681
+ intendedSessionId: previous.intendedSessionId,
682
+ response: previous.response,
683
+ }),
684
+ )
685
+ .digest("hex");
686
+ }
687
+ if (this.#isCleanupPending(next)) {
688
+ const cleanupSessionId = (next.response as { error?: { cleanup?: { sessionId?: unknown } } }).error?.cleanup
689
+ ?.sessionId;
690
+ if (!canonicalCleanupSessionId(cleanupSessionId))
691
+ throw new Error("Cleanup response lacks a canonical session fence");
692
+ if (next.intendedSessionId === undefined) next.intendedSessionId = cleanupSessionId;
693
+ else if (next.intendedSessionId !== cleanupSessionId)
694
+ throw new Error("Cleanup response session does not match its outer lifecycle fence");
695
+ next.responseDigest = createHash("sha256")
696
+ .update(canonicalJson({ intendedSessionId: next.intendedSessionId, response: next.response }))
697
+ .digest("hex");
698
+ } else if (fields.response !== undefined && fields.responseDigest === undefined)
699
+ next.responseDigest = createHash("sha256").update(canonicalJson(next.response)).digest("hex");
700
+ else if (
549
701
  (terminal(state) || state === "terminal_uncertain") &&
550
702
  next.response !== undefined &&
551
703
  next.responseDigest === undefined
@@ -577,6 +729,70 @@ export class LifecycleLedger {
577
729
  }
578
730
  }
579
731
  }
732
+ findCleanupPendingByDeleteTarget(
733
+ target: { sessionId: string; sessionsRoot?: string; transcriptPath: string; cwd: string },
734
+ excludingIdentity: string,
735
+ ): LifecycleLedgerEntry | undefined {
736
+ let latestPending: LifecycleLedgerEntry | undefined;
737
+ for (const current of this.#byIdentity.values()) {
738
+ if (
739
+ current.identity === excludingIdentity ||
740
+ current.state === "terminal_ok" ||
741
+ current.intendedSessionId !== target.sessionId
742
+ )
743
+ continue;
744
+ if (current.unresolvedCleanupResponse !== undefined) {
745
+ const retained = { ...current, response: current.unresolvedCleanupResponse };
746
+ if (this.#matchesSessionDeleteTarget(retained, target)) {
747
+ if (!latestPending || retained.ts > latestPending.ts) latestPending = retained;
748
+ continue;
749
+ }
750
+ }
751
+ for (let index = this.#entries.length - 1; index >= 0; index -= 1) {
752
+ const historical = this.#entries[index];
753
+ if (
754
+ !historical ||
755
+ historical.identity !== current.identity ||
756
+ historical.intendedSessionId !== target.sessionId ||
757
+ !this.#matchesSessionDeleteTarget(historical, target)
758
+ )
759
+ continue;
760
+ if (!latestPending || historical.ts > latestPending.ts) latestPending = historical;
761
+ break;
762
+ }
763
+ }
764
+ return latestPending;
765
+ }
766
+
767
+ findCleanupPendingBySessionId(sessionId: string, excludingIdentity: string): LifecycleLedgerEntry | undefined {
768
+ for (const current of this.#byIdentity.values()) {
769
+ if (current.identity === excludingIdentity) continue;
770
+ if (pendingCleanupSessionId(current.response) === sessionId) return current;
771
+ if (pendingCleanupSessionId(current.unresolvedCleanupResponse) === sessionId) return current;
772
+ }
773
+ return undefined;
774
+ }
775
+
776
+ hasUncertainCleanupForSession(sessionId: string, excludingIdentity: string): boolean {
777
+ for (const current of this.#byIdentity.values()) {
778
+ if (current.state !== "terminal_uncertain") continue;
779
+ if (current.uncertainCleanupAllSessions === true) return true;
780
+ if (current.response === undefined) return true;
781
+ if (current.identity === excludingIdentity) continue;
782
+ const fencedSessions = [
783
+ current.uncertainCleanupSessionId,
784
+ current.intendedSessionId,
785
+ pendingCleanupSessionId(current.response),
786
+ pendingCleanupSessionId(current.unresolvedCleanupResponse),
787
+ ...(current.uncertainCleanupSessionIds ?? []),
788
+ ];
789
+ const boundSessions = fencedSessions.filter((value): value is string => typeof value === "string");
790
+ if (boundSessions.length === 0) return true;
791
+ if (fencedSessions.includes(sessionId)) return true;
792
+ }
793
+ return false;
794
+ }
795
+
580
796
  get(identity: string): LifecycleLedgerEntry | undefined {
581
797
  return this.#byIdentity.get(identity);
582
798
  }