@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
@@ -94,6 +94,18 @@ export interface TopicDeleteAuthoritySnapshot {
94
94
  record?: TopicRecord;
95
95
  }
96
96
 
97
+ /**
98
+ * Proof that a definite delete was settled in memory and still owes its durable
99
+ * commit. Handed back only by a settlement that actually happened, so a refused
100
+ * settlement structurally cannot be followed by a rollback.
101
+ */
102
+ export interface TopicSettledDelete {
103
+ sessionId: string;
104
+ topicId: string;
105
+ /** Authority epoch proven current when the record was removed. */
106
+ settledEpoch: number;
107
+ }
108
+
97
109
  function isValidBindingString(value: unknown): value is string {
98
110
  return typeof value === "string" && value.trim().length > 0;
99
111
  }
@@ -136,6 +148,16 @@ function isValidTopicId(value: unknown): value is string {
136
148
  );
137
149
  }
138
150
 
151
+ /**
152
+ * Monotonic epoch successor that saturates instead of leaving the safe-integer
153
+ * range. Past `Number.MAX_SAFE_INTEGER` two distinct generations collapse onto
154
+ * the same IEEE-754 double, which would let a stale settlement clear a newer
155
+ * fence; settlement refuses the saturated value instead of overflowing past it.
156
+ */
157
+ function nextAuthorityEpoch(current: number): number {
158
+ return current >= Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : current + 1;
159
+ }
160
+
139
161
  export function emptyTopicRegistryState(): TopicRegistryState {
140
162
  return { topics: {} };
141
163
  }
@@ -161,6 +183,8 @@ export class TopicRegistry {
161
183
  private readonly creatingBindings = new Map<string, TopicEndpointBinding>();
162
184
  /** Monotonic authority epochs, including deletion fences for absent records. */
163
185
  private readonly epochs = new Map<string, number>();
186
+ /** Phase-1 settlements awaiting their durable clear; their topic ids stay quarantined. */
187
+ readonly #settling = new Map<string, { topicId: string; settledEpoch: number; record: TopicRecord }>();
164
188
 
165
189
  constructor(state: TopicRegistryState = emptyTopicRegistryState()) {
166
190
  this.topics = new Map();
@@ -173,6 +197,7 @@ export class TopicRegistry {
173
197
  this.byTopic.clear();
174
198
  this.#ambiguousTopicIds.clear();
175
199
  this.epochs.clear();
200
+ this.#settling.clear();
176
201
  this.load(state);
177
202
  }
178
203
 
@@ -259,6 +284,10 @@ export class TopicRegistry {
259
284
  private rebuildInboundRoutes(): void {
260
285
  this.byTopic.clear();
261
286
  this.#ambiguousTopicIds.clear();
287
+ // A settled-but-not-yet-durable clear keeps its topic id quarantined: the
288
+ // clear is not authoritative until persisted, so no colliding survivor may
289
+ // become routable and no settled id may become adoptable during that write.
290
+ for (const settling of this.#settling.values()) this.#ambiguousTopicIds.add(settling.topicId);
262
291
  const activeByTopic = new Map<string, string>();
263
292
 
264
293
  for (const [sessionId, record] of this.topics) {
@@ -278,6 +307,13 @@ export class TopicRegistry {
278
307
  }
279
308
  }
280
309
 
310
+ /** Advance and publish the session authority epoch, saturating at the safe-integer bound. */
311
+ #advanceAuthorityEpoch(sessionId: string, base: number): number {
312
+ const epoch = nextAuthorityEpoch(base);
313
+ this.epochs.set(sessionId, epoch);
314
+ return epoch;
315
+ }
316
+
281
317
  /** Resolve the owning session for a topic id (for fail-closed inbound routing). */
282
318
  sessionForTopic(topicId: string): string | undefined {
283
319
  return this.byTopic.get(topicId);
@@ -482,6 +518,7 @@ export class TopicRegistry {
482
518
  const pending = this.inflight.get(sessionId);
483
519
  if (pending) return pending;
484
520
  const epoch = this.epochs.get(sessionId) ?? 0;
521
+ if (epoch >= Number.MAX_SAFE_INTEGER) throw new Error("topic authority epoch exhausted");
485
522
  // Publish the compatible endpoint claim before invoking `create`: the callback
486
523
  // may immediately begin a remote create and identity-less recovery must never
487
524
  // observe a false absence during that await.
@@ -676,7 +713,9 @@ export class TopicRegistry {
676
713
  /** Restore a failed delete fence only while its exact authority mutation remains current. */
677
714
  restoreDeleteAuthority(snapshot: TopicDeleteAuthoritySnapshot): boolean {
678
715
  const record = this.topics.get(snapshot.sessionId);
679
- const deleteEpoch = Math.max(snapshot.fenceEpoch ?? 0, snapshot.authorityEpoch ?? 0) + 1;
716
+ const authorityBase = Math.max(snapshot.fenceEpoch ?? 0, snapshot.authorityEpoch ?? 0);
717
+ if (authorityBase >= Number.MAX_SAFE_INTEGER) return false;
718
+ const deleteEpoch = nextAuthorityEpoch(authorityBase);
680
719
  if (this.epochs.get(snapshot.sessionId) !== deleteEpoch) return false;
681
720
  if (snapshot.topicId === undefined) {
682
721
  if (record) return false;
@@ -697,10 +736,17 @@ export class TopicRegistry {
697
736
  return true;
698
737
  }
699
738
 
700
- /** Restore the exact delete fence after a failed compensation publication. */
739
+ /**
740
+ * Restore the exact delete fence after a failed compensation publication.
741
+ *
742
+ * Settlement rebuilds derived routes, which can make a surviving colliding
743
+ * record routable. Rebuild again on successful restoration so the reinstated
744
+ * fence re-quarantines the topic id instead of leaving inbound routing open to
745
+ * the collision partner despite the restored fence.
746
+ */
701
747
  restoreDeleteFence(snapshot: TopicDeleteAuthoritySnapshot): boolean {
702
748
  const record = this.topics.get(snapshot.sessionId);
703
- const deleteEpoch = Math.max(snapshot.fenceEpoch ?? 0, snapshot.authorityEpoch ?? 0) + 1;
749
+ const deleteEpoch = nextAuthorityEpoch(Math.max(snapshot.fenceEpoch ?? 0, snapshot.authorityEpoch ?? 0));
704
750
  if (snapshot.topicId === undefined) {
705
751
  if (record) return false;
706
752
  } else if (!record) {
@@ -715,17 +761,19 @@ export class TopicRegistry {
715
761
  } else {
716
762
  record.authorityEpoch = deleteEpoch;
717
763
  record.authorityState = "delete_pending";
718
- if (this.byTopic.get(record.topicId) === snapshot.sessionId) this.byTopic.delete(record.topicId);
719
764
  }
720
765
  this.epochs.set(snapshot.sessionId, deleteEpoch);
766
+ this.rebuildInboundRoutes();
721
767
  return true;
722
768
  }
723
769
 
724
770
  /** Fence new work before the remote delete starts, including an absent in-flight create. */
725
771
  beginDelete(sessionId: string): TopicRecord | undefined {
726
772
  const record = this.topics.get(sessionId);
727
- const epoch = Math.max(this.epochs.get(sessionId) ?? 0, record?.authorityEpoch ?? 0) + 1;
728
- this.epochs.set(sessionId, epoch);
773
+ const epoch = this.#advanceAuthorityEpoch(
774
+ sessionId,
775
+ Math.max(this.epochs.get(sessionId) ?? 0, record?.authorityEpoch ?? 0),
776
+ );
729
777
  if (!record) return undefined;
730
778
  record.authorityEpoch = epoch;
731
779
  record.authorityState = "delete_pending";
@@ -799,11 +847,86 @@ export class TopicRegistry {
799
847
  await this.inflight.get(sessionId)?.catch(() => undefined);
800
848
  }
801
849
 
802
- /** Remove only after a definite remote deletion; ambiguity deliberately retains its fence. */
803
- settleDelete(sessionId: string, topicId: string): boolean {
850
+ /**
851
+ * Phase 1 of settling a definite remote delete: remove the record while
852
+ * deliberately RETAINING its topic-id quarantine.
853
+ *
854
+ * `dispatchedAuthorityEpoch` is the authority epoch the caller held when it
855
+ * dispatched the remote delete. Settlement requires that epoch to still equal
856
+ * both the record's own authority epoch and the registry's current epoch for
857
+ * the session, so a held earlier delete can never settle a newer fence: if a
858
+ * scan or close-started delete re-fenced the same session/topic after this
859
+ * delete was dispatched, the stale definite result is refused and the newer
860
+ * `delete_pending` record plus its topic-id quarantine stay intact.
861
+ *
862
+ * An epoch outside the safe-integer range cannot identify a generation, and at
863
+ * `Number.MAX_SAFE_INTEGER` epoch advancement has saturated so distinct
864
+ * generations are no longer distinguishable. Both fail closed: settlement can
865
+ * no longer be proven fresh, so the fence is kept.
866
+ *
867
+ * Derived routing tables are deliberately NOT rebuilt here. Until the cleared
868
+ * snapshot is durably persisted the clear is not authoritative, so publishing
869
+ * routes now would make a colliding survivor routable (and the settled id
870
+ * adoptable) during the held write, and a failed persist would re-quarantine
871
+ * too late. Publish with {@link commitSettledDelete} once the persist
872
+ * succeeds, or undo with {@link rollbackSettledDelete} when it fails.
873
+ */
874
+ settleDelete(sessionId: string, topicId: string, dispatchedAuthorityEpoch: number): TopicSettledDelete | undefined {
875
+ if (!Number.isSafeInteger(dispatchedAuthorityEpoch) || dispatchedAuthorityEpoch < 0) return undefined;
876
+ if (this.#settling.has(sessionId)) return undefined;
804
877
  const record = this.topics.get(sessionId);
805
- if (!record || record.topicId !== topicId || record.authorityState !== "delete_pending") return false;
878
+ if (!record || record.topicId !== topicId || record.authorityState !== "delete_pending") return undefined;
879
+ if ((record.authorityEpoch ?? 0) !== dispatchedAuthorityEpoch) return undefined;
880
+ const currentEpoch = this.authorityEpoch(sessionId);
881
+ if (currentEpoch !== dispatchedAuthorityEpoch) return undefined;
882
+ if (currentEpoch >= Number.MAX_SAFE_INTEGER) return undefined;
806
883
  this.topics.delete(sessionId);
884
+ this.#settling.set(sessionId, { topicId, settledEpoch: dispatchedAuthorityEpoch, record: { ...record } });
885
+ this.#ambiguousTopicIds.add(topicId);
886
+ if (this.byTopic.get(topicId) === sessionId) this.byTopic.delete(topicId);
887
+ return { sessionId, topicId, settledEpoch: dispatchedAuthorityEpoch };
888
+ }
889
+
890
+ /**
891
+ * Phase 2: publish derived routing tables once the cleared state is durable.
892
+ *
893
+ * Only here does the settled topic id lose its quarantine, so a surviving
894
+ * colliding record becomes routable and a settled id becomes adoptable without
895
+ * waiting for a daemon restart.
896
+ */
897
+ commitSettledDelete(settled: TopicSettledDelete): boolean {
898
+ const pending = this.#settling.get(settled.sessionId);
899
+ if (!pending || pending.topicId !== settled.topicId || pending.settledEpoch !== settled.settledEpoch)
900
+ return false;
901
+ this.#settling.delete(settled.sessionId);
902
+ this.rebuildInboundRoutes();
903
+ return true;
904
+ }
905
+
906
+ /**
907
+ * Compare-and-swap undo for a settlement whose durable clear failed.
908
+ *
909
+ * Restoration applies only while the registry still holds exactly the state
910
+ * that this settlement produced: the settlement is still awaiting commit, the
911
+ * session still has no record, and the session epoch is still the settled
912
+ * epoch. Any mismatch means a newer generation intervened, so the restore is
913
+ * refused and the newer state is left untouched. A refused settlement produced
914
+ * no token, so it can never reach this path.
915
+ */
916
+ rollbackSettledDelete(settled: TopicSettledDelete): boolean {
917
+ const pending = this.#settling.get(settled.sessionId);
918
+ if (!pending || pending.topicId !== settled.topicId || pending.settledEpoch !== settled.settledEpoch)
919
+ return false;
920
+ if (this.topics.has(settled.sessionId)) return false;
921
+ if (this.authorityEpoch(settled.sessionId) !== settled.settledEpoch) return false;
922
+ this.#settling.delete(settled.sessionId);
923
+ this.topics.set(settled.sessionId, {
924
+ ...pending.record,
925
+ authorityEpoch: settled.settledEpoch,
926
+ authorityState: "delete_pending",
927
+ });
928
+ this.epochs.set(settled.sessionId, settled.settledEpoch);
929
+ this.rebuildInboundRoutes();
807
930
  return true;
808
931
  }
809
932
 
@@ -811,7 +934,7 @@ export class TopicRegistry {
811
934
  delete(sessionId: string): boolean {
812
935
  const record = this.topics.get(sessionId);
813
936
  if (!record) return false;
814
- this.epochs.set(sessionId, Math.max(this.epochs.get(sessionId) ?? 0, record.authorityEpoch ?? 0) + 1);
937
+ this.#advanceAuthorityEpoch(sessionId, Math.max(this.epochs.get(sessionId) ?? 0, record.authorityEpoch ?? 0));
815
938
  if (this.byTopic.get(record.topicId) === sessionId) this.byTopic.delete(record.topicId);
816
939
  return this.topics.delete(sessionId);
817
940
  }
@@ -11,6 +11,7 @@ import {
11
11
  } from "@gajae-code/agent-core";
12
12
  import {
13
13
  type AssistantMessage,
14
+ type AttemptScopeRef,
14
15
  type AuthCredentialSelector,
15
16
  type CredentialDisabledEvent,
16
17
  type Message,
@@ -2538,21 +2539,21 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2538
2539
  if (!obfuscator?.hasSecrets()) return converted;
2539
2540
  return obfuscateMessages(obfuscator, converted);
2540
2541
  };
2541
- const transformContext = async (messages: AgentMessage[], _signal?: AbortSignal) => {
2542
+ const transformContext = async (messages: AgentMessage[], _signal?: AbortSignal, scope?: AttemptScopeRef) => {
2542
2543
  // External Agent events dispatch listeners without awaiting them. The
2543
2544
  // session-owned barrier makes any pre-admission artifact transformation
2544
2545
  // visible before this provider context is normalized.
2545
2546
  await session?.awaitPendingContextTransformations();
2546
- return extensionRunner ? await extensionRunner.emitContext(messages) : messages;
2547
+ return extensionRunner ? await extensionRunner.emitContext(messages, scope) : messages;
2547
2548
  };
2548
2549
  const onPayload = extensionRunner
2549
- ? async (payload: unknown, _model?: Model) => {
2550
- return await extensionRunner.emitBeforeProviderRequest(payload);
2550
+ ? async (payload: unknown, _model?: Model, scope?: AttemptScopeRef) => {
2551
+ return await extensionRunner.emitBeforeProviderRequest(payload, scope);
2551
2552
  }
2552
2553
  : undefined;
2553
2554
  const onResponse: SimpleStreamOptions["onResponse"] | undefined = extensionRunner
2554
- ? async (response, model) => {
2555
- await extensionRunner.emitAfterProviderResponse(response, model);
2555
+ ? async (response, model, scope) => {
2556
+ await extensionRunner.emitAfterProviderResponse(response, model, scope);
2556
2557
  }
2557
2558
  : undefined;
2558
2559