@gajae-code/coding-agent 0.12.11 → 0.12.12

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 (112) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/types/cli/notify-cli.d.ts +42 -1
  3. package/dist/types/cli/update-cli.d.ts +9 -3
  4. package/dist/types/commands/notify.d.ts +6 -0
  5. package/dist/types/config/file-lock.d.ts +3 -0
  6. package/dist/types/coordinator/contract.d.ts +1 -1
  7. package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
  8. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  9. package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
  10. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
  11. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  12. package/dist/types/main.d.ts +6 -2
  13. package/dist/types/modes/components/session-selector.d.ts +1 -1
  14. package/dist/types/modes/utils/host-status.d.ts +23 -0
  15. package/dist/types/runtime-mcp/types.d.ts +2 -0
  16. package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
  17. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
  18. package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
  19. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  20. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
  21. package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
  22. package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
  23. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
  24. package/dist/types/sdk/bus/notification-service.d.ts +10 -0
  25. package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
  26. package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
  27. package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
  28. package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
  29. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
  30. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +24 -7
  31. package/dist/types/sdk/bus/telegram-daemon.d.ts +104 -16
  32. package/dist/types/sdk/bus/telegram-reference.d.ts +10 -1
  33. package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
  34. package/dist/types/sdk/host/host.d.ts +52 -0
  35. package/dist/types/sdk/lifecycle-session.d.ts +6 -0
  36. package/dist/types/sdk/session-activation.d.ts +41 -0
  37. package/dist/types/sdk/session.d.ts +18 -0
  38. package/dist/types/sdk/startup-capability.d.ts +8 -1
  39. package/dist/types/session/agent-session.d.ts +7 -0
  40. package/dist/types/session/artifacts.d.ts +2 -2
  41. package/dist/types/session/session-manager.d.ts +23 -5
  42. package/dist/types/tools/index.d.ts +8 -0
  43. package/dist/types/utils/npm-registry.d.ts +71 -0
  44. package/examples/extensions/README.md +38 -4
  45. package/examples/extensions/ooo-bridge.ts +15 -0
  46. package/package.json +8 -8
  47. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  48. package/src/async/job-manager.ts +9 -2
  49. package/src/cli/notify-cli.ts +226 -2
  50. package/src/cli/update-cli.ts +90 -23
  51. package/src/commands/notify.ts +27 -3
  52. package/src/commands/sdk.ts +1 -0
  53. package/src/config/file-lock-gc.ts +18 -0
  54. package/src/config/file-lock.ts +61 -16
  55. package/src/config/model-profiles.ts +31 -15
  56. package/src/coordinator/contract.ts +1 -0
  57. package/src/coordinator-mcp/question-state.ts +2 -0
  58. package/src/coordinator-mcp/server.ts +358 -7
  59. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  60. package/src/extensibility/extensions/index.ts +1 -0
  61. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
  62. package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
  63. package/src/extensibility/extensions/runner.ts +15 -2
  64. package/src/extensibility/extensions/types.ts +2 -0
  65. package/src/gjc-runtime/launch-tmux.ts +53 -1
  66. package/src/internal-urls/docs-index.generated.ts +11 -10
  67. package/src/main.ts +203 -67
  68. package/src/modes/DESIGN.md +8 -5
  69. package/src/modes/components/footer.ts +10 -18
  70. package/src/modes/components/session-selector.ts +26 -3
  71. package/src/modes/components/status-line/gh.ts +23 -2
  72. package/src/modes/controllers/event-controller.ts +3 -0
  73. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  74. package/src/modes/controllers/input-controller.ts +11 -0
  75. package/src/modes/controllers/selector-controller.ts +8 -2
  76. package/src/modes/interactive-mode.ts +7 -1
  77. package/src/modes/utils/host-status.ts +38 -0
  78. package/src/runtime-mcp/types.ts +2 -0
  79. package/src/sdk/broker/lifecycle.ts +60 -3
  80. package/src/sdk/bus/chat-daemon-cli.ts +42 -0
  81. package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
  82. package/src/sdk/bus/chat-daemon-control.ts +16 -3
  83. package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
  84. package/src/sdk/bus/control-plane-frames.ts +51 -0
  85. package/src/sdk/bus/conversation-store.ts +29 -1
  86. package/src/sdk/bus/existing-thread-readiness.ts +155 -0
  87. package/src/sdk/bus/index.ts +70 -0
  88. package/src/sdk/bus/notification-service.ts +28 -1
  89. package/src/sdk/bus/rich-render.ts +3 -2
  90. package/src/sdk/bus/slack-daemon.ts +99 -4
  91. package/src/sdk/bus/slack-live-provider.ts +32 -2
  92. package/src/sdk/bus/slack-provider.ts +11 -0
  93. package/src/sdk/bus/slack-thread-binding.ts +641 -0
  94. package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
  95. package/src/sdk/bus/telegram-daemon-contract.ts +24 -7
  96. package/src/sdk/bus/telegram-daemon.ts +1961 -497
  97. package/src/sdk/bus/telegram-reference.ts +42 -6
  98. package/src/sdk/bus/topic-registry.ts +674 -181
  99. package/src/sdk/host/host.ts +180 -1
  100. package/src/sdk/lifecycle-session.ts +8 -2
  101. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  102. package/src/sdk/session-activation.ts +127 -0
  103. package/src/sdk/session.ts +139 -28
  104. package/src/sdk/startup-capability.ts +10 -1
  105. package/src/session/agent-session.ts +220 -9
  106. package/src/session/artifacts.ts +56 -13
  107. package/src/session/internal/managed-session-storage.ts +206 -7
  108. package/src/session/session-manager.ts +148 -38
  109. package/src/setup/provider-presets.json +1 -1
  110. package/src/task/index.ts +92 -44
  111. package/src/tools/index.ts +8 -0
  112. package/src/utils/npm-registry.ts +660 -0
@@ -1,21 +1,28 @@
1
1
  /**
2
2
  * Per-session forum-topic registry for the threaded session surface.
3
3
  *
4
- * Each GJC session owns one active Telegram forum topic in the paired private
5
- * DM. The topic is created via `createForumTopic`, reused while the session
6
- * remains active, and removed from the registry when the daemon deletes it on
7
- * shutdown. The registry also tracks whether the one-time identity header has
8
- * already been pinned, so it is sent exactly once per active topic, even across
9
- * reconnects.
4
+ * Each GJC session owns one active Telegram forum topic. Remote archive closes
5
+ * daemon-created topics without deleting their durable records; rotated
6
+ * successors move inactive records into retained history before creating a new
7
+ * active authority. The registry also tracks whether the one-time identity
8
+ * header has already been pinned.
10
9
  *
11
10
  * State is a plain serialisable map persisted beside the daemon state files;
12
11
  * topic creation is injected so this module is pure and unit-testable without a
13
12
  * live Bot API.
14
13
  */
15
14
  /** Persisted record for one session's topic. */
15
+ export type TopicLifecycleState = "active" | "disconnect_grace" | "archive_pending" | "archive_exhausted" | "inactive" | "legacy_quarantined"
16
+ /** Read-only input compatibility; normalized to archive_pending during load. */
17
+ | "delete_pending";
18
+ /** Persisted record for one immutable session identity's topic. */
16
19
  export interface TopicRecord {
17
20
  /** Telegram forum topic id (message_thread_id). */
18
21
  topicId: string;
22
+ /** Whether Telegram created this topic for the daemon or it was explicitly adopted from a user. */
23
+ topicOrigin: "daemon_created" | "user_created";
24
+ /** Immutable UUID record identity; never derive authority from a title or PID. */
25
+ sessionUuid?: string;
19
26
  /** Whether the one-time identity header has been sent/pinned. */
20
27
  identitySent: boolean;
21
28
  /** Creation timestamp (ms epoch). */
@@ -40,10 +47,8 @@ export interface TopicRecord {
40
47
  authorityEpoch?: number;
41
48
  /** Immutable authority epoch held when this remote topic create began. */
42
49
  creationLeaseEpoch?: number;
43
- /** An uncertain delete fences future creation and inbound routing. */
44
- authorityState?: "active" | "delete_pending";
45
- /** Provenance for topics created by the user and adopted by GJC. Missing means daemon-created. */
46
- topicOrigin?: "user_created";
50
+ /** Durable non-destructive lifecycle state. */
51
+ authorityState?: TopicLifecycleState;
47
52
  /** Telegram chat and endpoint authority last proven to use this topic. */
48
53
  chatId?: string;
49
54
  /** Canonical endpoint tuple (URL + token) that currently holds the lease. */
@@ -54,17 +59,68 @@ export interface TopicRecord {
54
59
  endpointGeneration?: number;
55
60
  /** Monotonic authenticated endpoint handoffs; legacy bindings begin at zero. */
56
61
  endpointIncarnation?: number;
62
+ /** Shared-authority lease owner (installation UUID), heartbeat, and expiry. */
63
+ leaseOwner?: string;
64
+ leaseHeartbeatAt?: number;
65
+ leaseExpiresAt?: number;
66
+ /** Durable archive initiator; a live foreign owner cannot be displaced. */
67
+ archiveHostId?: string;
68
+ /** Authority epoch captured by the archive initiator when it published the fence. */
69
+ archiveLeaseEpoch?: number;
70
+ disconnectGraceExpiresAt?: number;
57
71
  /** True when persisted binding fields were present but malformed; recovery must fail closed. */
58
72
  bindingMalformed?: true;
59
73
  }
74
+ /** Durable claim published before invoking createForumTopic. */
75
+ export interface TopicCreateClaim {
76
+ sessionId: string;
77
+ hostId?: string;
78
+ leaseOwner?: string;
79
+ authorityEpoch: number;
80
+ createdAt: number;
81
+ binding?: TopicEndpointBinding;
82
+ }
83
+ export interface ArchiveJob {
84
+ sessionId: string;
85
+ topicId: string;
86
+ /** Number of archive calls that have returned an ambiguous outcome. */
87
+ attempt: number;
88
+ /** First ambiguous result; bounds retry lifetime. */
89
+ firstAttemptAt?: number;
90
+ /** Compatibility read for pre-journal snapshots; normalized to `attempt`. */
91
+ retryCount?: number;
92
+ backoffMs: number;
93
+ nextAttemptAt: number;
94
+ safeDiagnostic?: string;
95
+ }
60
96
  /** Serialisable shape persisted to disk. */
61
97
  export interface TopicRegistryState {
98
+ /** Writer format. Missing is the quarantined legacy format; future versions fail closed. */
99
+ version?: 2;
100
+ /** Monotonically increasing snapshot generation used by shared CAS stores. */
101
+ registryGeneration?: number;
62
102
  /** sessionId -> record. */
63
103
  topics: Record<string, TopicRecord>;
64
- /** Durable deletion epochs retained after a definite delete. */
104
+ /** Durable lifecycle epochs retained after an archive starts. */
65
105
  fences?: Record<string, number>;
66
106
  /** Closed transport endpoint leases; unchanged endpoint discovery remains fenced across restart. */
67
107
  closedEndpoints?: Record<string, TopicEndpointBinding>;
108
+ /** Persistent host identity used to distinguish concurrent installations. */
109
+ installationHostId?: string;
110
+ /** Bounded durable archive work; no topic record is physically deleted. */
111
+ archiveJobs?: Record<string, ArchiveJob>;
112
+ /** Durable create claims. A claim fences concurrent creators before remote I/O. */
113
+ createClaims?: Record<string, TopicCreateClaim>;
114
+ /** Retained inactive predecessors keyed by logical session id. */
115
+ retiredTopics?: Record<string, TopicRecord[]>;
116
+ }
117
+ /**
118
+ * Shared registry authority. Filesystem atomic rename is sufficient for a single
119
+ * installation, but it cannot serialize two hosts sharing a state volume.
120
+ */
121
+ export interface TopicRegistryCasAuthority {
122
+ read(): Promise<TopicRegistryState | undefined>;
123
+ compareAndSet(expectedGeneration: number, next: TopicRegistryState): Promise<boolean>;
68
124
  }
69
125
  /** Authenticated runtime binding for a durable topic lease. */
70
126
  export interface TopicEndpointBinding {
@@ -83,7 +139,7 @@ export type TopicEndpointAuthority = {
83
139
  state: "ambiguous";
84
140
  };
85
141
  /** Conditional rollback token for a delete fence publication. */
86
- export interface TopicDeleteAuthoritySnapshot {
142
+ export interface TopicArchiveAuthoritySnapshot {
87
143
  sessionId: string;
88
144
  topicId?: string;
89
145
  authorityEpoch?: number;
@@ -92,18 +148,12 @@ export interface TopicDeleteAuthoritySnapshot {
92
148
  /** Exact fenced record, retained to restore an in-memory tombstone after a failed clear publication. */
93
149
  record?: TopicRecord;
94
150
  }
151
+ export declare function emptyTopicRegistryState(): TopicRegistryState;
95
152
  /**
96
- * Proof that a definite delete was settled in memory and still owes its durable
97
- * commit. Handed back only by a settlement that actually happened, so a refused
98
- * settlement structurally cannot be followed by a rollback.
153
+ * Reject snapshots written by a newer daemon. Missing versions are preserved as
154
+ * evidence but quarantined: legacy records must never route or mutate remotely.
99
155
  */
100
- export interface TopicSettledDelete {
101
- sessionId: string;
102
- topicId: string;
103
- /** Authority epoch proven current when the record was removed. */
104
- settledEpoch: number;
105
- }
106
- export declare function emptyTopicRegistryState(): TopicRegistryState;
156
+ export declare function parseTopicRegistryState(value: unknown): TopicRegistryState | undefined;
107
157
  /**
108
158
  * In-memory registry over a serialisable state. Topic creation is injected via
109
159
  * `getOrCreateTopic`'s `create` callback (the daemon supplies a real
@@ -124,6 +174,14 @@ export declare class TopicRegistry {
124
174
  private readonly creatingBindings;
125
175
  /** Monotonic authority epochs, including deletion fences for absent records. */
126
176
  private readonly epochs;
177
+ /** Archive work is retained and retryable; records are never physically removed. */
178
+ private readonly archiveJobs;
179
+ /** Durable pre-create claims; remote creation is forbidden until published. */
180
+ private readonly createClaims;
181
+ /** Inactive predecessor evidence retained across successor generations. */
182
+ private readonly retiredTopics;
183
+ /** Generation of the last loaded/published snapshot. */
184
+ private registryGeneration;
127
185
  constructor(state?: TopicRegistryState);
128
186
  /** Replace all runtime state after a successfully persisted staged publication. */
129
187
  replace(state: TopicRegistryState): void;
@@ -134,10 +192,18 @@ export declare class TopicRegistry {
134
192
  sessionForTopic(topicId: string): string | undefined;
135
193
  /** All session ids with a persisted topic record. */
136
194
  sessionIds(): string[];
137
- /** Persisted remote deletes that must be reconciled before recovery can proceed. */
138
- deletePendingSessionIds(): string[];
139
195
  /** The existing topic record for a session, if any. */
140
196
  get(sessionId: string): TopicRecord | undefined;
197
+ /** Durable claims restored from disk require explicit authoritative reconciliation. */
198
+ pendingCreateClaims(): TopicCreateClaim[];
199
+ /**
200
+ * Resolve a durable pre-create claim only with authoritative topic evidence.
201
+ * An absent or malformed topic deliberately leaves the claim in place, fencing
202
+ * subsequent creators after a crash.
203
+ */
204
+ reconcileCreateClaim(sessionId: string, topic?: TopicRecord): boolean;
205
+ /** Clear a claim only when the local create attempt proved no remote topic was accepted. */
206
+ abandonCreateClaim(sessionId: string, authorityEpoch: number): boolean;
141
207
  /** Current immutable authority epoch for a creation lease. */
142
208
  authorityEpoch(sessionId: string): number;
143
209
  /** Whether this session has an active, unambiguous topic authority. */
@@ -160,6 +226,12 @@ export declare class TopicRegistry {
160
226
  uniqueSessionForEndpoint(binding: TopicEndpointBinding): string | undefined;
161
227
  /** Whether this exact session owns the complete durable endpoint binding. */
162
228
  matchesEndpoint(sessionId: string, binding: TopicEndpointBinding): boolean;
229
+ /**
230
+ * Retire a remotely settled inactive topic only when an authenticated
231
+ * successor proves a different endpoint authority. The inactive predecessor
232
+ * is serialized into retained history before the active slot is released.
233
+ */
234
+ retireInactiveEndpointForSuccessor(sessionId: string, binding: TopicEndpointBinding): boolean;
163
235
  /**
164
236
  * Rebind an existing topic to an authenticated successor endpoint. The exact
165
237
  * logical session id is proved by replay before this method is called. A
@@ -177,6 +249,17 @@ export declare class TopicRegistry {
177
249
  getOrCreateTopic(sessionId: string, create: () => Promise<unknown>, now?: () => number, name?: string, binding?: TopicEndpointBinding, commit?: () => Promise<void>, transientClaimant?: object, topicOrigin?: TopicRecord["topicOrigin"]): Promise<TopicRecord>;
178
250
  /** Mark the identity header as sent for a session. Idempotent. */
179
251
  markIdentitySent(sessionId: string): void;
252
+ /** Generation used as the compare value for a shared CAS publication. */
253
+ registryVersion(): number;
254
+ /** Advance only after a successful shared compare-and-set publication. */
255
+ markRegistryPublished(generation: number): void;
256
+ /**
257
+ * Acquire or renew a host lease. Another unexpired host is never displaced;
258
+ * a disconnected owner may resume the exact topic during its grace window.
259
+ */
260
+ acquireLease(sessionId: string, hostId: string, now: number, ttlMs: number, graceMs: number): boolean;
261
+ /** Record a disconnect without losing the topic identity needed for a grace resume. */
262
+ releaseLeaseToGrace(sessionId: string, hostId: string, now: number, graceMs: number): boolean;
180
263
  /** Whether the identity header still needs sending for this session. */
181
264
  needsIdentity(sessionId: string): boolean;
182
265
  /** Remember stable repo/branch identity independently of the displayed name. */
@@ -206,73 +289,35 @@ export declare class TopicRegistry {
206
289
  markUserNamePending(sessionId: string, name: string): boolean;
207
290
  /** Commit a successfully-applied daemon topic title. */
208
291
  markNameApplied(sessionId: string, name: string): void;
209
- /** Capture only authority fields that a failed delete publication may restore. */
210
- captureDeleteAuthority(sessionId: string): TopicDeleteAuthoritySnapshot;
211
- /** Restore a failed delete fence only while its exact authority mutation remains current. */
212
- restoreDeleteAuthority(snapshot: TopicDeleteAuthoritySnapshot): boolean;
213
- /**
214
- * Restore the exact delete fence after a failed compensation publication.
215
- *
216
- * Settlement rebuilds derived routes, which can make a surviving colliding
217
- * record routable. Rebuild again on successful restoration so the reinstated
218
- * fence re-quarantines the topic id instead of leaving inbound routing open to
219
- * the collision partner despite the restored fence.
220
- */
221
- restoreDeleteFence(snapshot: TopicDeleteAuthoritySnapshot): boolean;
222
- /** Fence new work before the remote delete starts, including an absent in-flight create. */
223
- beginDelete(sessionId: string): TopicRecord | undefined;
292
+ /** Capture only authority fields that a failed archive-fence publication may restore. */
293
+ captureArchiveAuthority(sessionId: string): TopicArchiveAuthoritySnapshot;
294
+ /** Restore a failed archive fence only while its exact authority mutation remains current. */
295
+ restoreArchiveAuthority(snapshot: TopicArchiveAuthoritySnapshot): boolean;
296
+ /** Restore the exact archive fence after a failed compensation publication. */
297
+ restoreArchiveFence(snapshot: TopicArchiveAuthoritySnapshot): boolean;
298
+ /** Fence new work before the remote archive starts, including an absent in-flight create. */
299
+ beginArchive(sessionId: string, hostId?: string, now?: number): TopicRecord | undefined;
300
+ /** Verify the durable archive initiator immediately before remote dispatch. */
301
+ archiveAuthorityAllows(sessionId: string, hostId: string, pairedChatId: string, now: number): boolean;
302
+ /** @deprecated Production dispatch must provide the current paired chat id. */
303
+ archiveAuthorityAllows(sessionId: string, hostId: string, now: number): boolean;
224
304
  /** Retain an accepted create as deletion-fenced before remote compensation can begin. */
225
- fenceAcceptedCreate(sessionId: string, topicId: string, now?: () => number, name?: string, binding?: TopicEndpointBinding): TopicRecord;
305
+ fenceAcceptedCreate(sessionId: string, topicId: string, now?: () => number, name?: string, binding?: TopicEndpointBinding, topicOrigin?: TopicRecord["topicOrigin"], archiveChatId?: string): TopicRecord;
226
306
  /** Fence an accepted create only when its exact creator lease still owns the record. */
227
- fenceAcceptedCreateForLease(sessionId: string, topicId: string, creationLeaseEpoch: number, now?: () => number, name?: string, binding?: TopicEndpointBinding): TopicRecord | undefined;
307
+ fenceAcceptedCreateForLease(sessionId: string, topicId: string, creationLeaseEpoch: number, hostId: string, now?: () => number, name?: string, binding?: TopicEndpointBinding, topicOrigin?: TopicRecord["topicOrigin"], archiveChatId?: string): TopicRecord | undefined;
228
308
  /** Wait for a revoked create to settle before admitting a later lifecycle epoch. */
229
309
  awaitInflight(sessionId: string): Promise<void>;
230
310
  /**
231
- * Phase 1 of settling a definite remote delete: remove the record while
232
- * deliberately RETAINING its topic-id quarantine.
233
- *
234
- * `dispatchedAuthorityEpoch` is the authority epoch the caller held when it
235
- * dispatched the remote delete. Settlement requires that epoch to still equal
236
- * both the record's own authority epoch and the registry's current epoch for
237
- * the session, so a held earlier delete can never settle a newer fence: if a
238
- * scan or close-started delete re-fenced the same session/topic after this
239
- * delete was dispatched, the stale definite result is refused and the newer
240
- * `delete_pending` record plus its topic-id quarantine stay intact.
241
- *
242
- * An epoch outside the safe-integer range cannot identify a generation, and at
243
- * `Number.MAX_SAFE_INTEGER` epoch advancement has saturated so distinct
244
- * generations are no longer distinguishable. Both fail closed: settlement can
245
- * no longer be proven fresh, so the fence is kept.
246
- *
247
- * Derived routing tables are deliberately NOT rebuilt here. Until the cleared
248
- * snapshot is durably persisted the clear is not authoritative, so publishing
249
- * routes now would make a colliding survivor routable (and the settled id
250
- * adoptable) during the held write, and a failed persist would re-quarantine
251
- * too late. Publish with {@link commitSettledDelete} once the persist
252
- * succeeds, or undo with {@link rollbackSettledDelete} when it fails.
253
- */
254
- settleDelete(sessionId: string, topicId: string, dispatchedAuthorityEpoch: number): TopicSettledDelete | undefined;
255
- /**
256
- * Phase 2: publish derived routing tables once the cleared state is durable.
257
- *
258
- * Only here does the settled topic id lose its quarantine, so a surviving
259
- * colliding record becomes routable and a settled id becomes adoptable without
260
- * waiting for a daemon restart.
261
- */
262
- commitSettledDelete(settled: TopicSettledDelete): boolean;
263
- /**
264
- * Compare-and-swap undo for a settlement whose durable clear failed.
265
- *
266
- * Restoration applies only while the registry still holds exactly the state
267
- * that this settlement produced: the settlement is still awaiting commit, the
268
- * session still has no record, and the session epoch is still the settled
269
- * epoch. Any mismatch means a newer generation intervened, so the restore is
270
- * refused and the newer state is left untouched. A refused settlement produced
271
- * no token, so it can never reach this path.
311
+ * Retain a topic record after a definite remote archive only while the exact
312
+ * dispatched authority epoch is still current.
272
313
  */
273
- rollbackSettledDelete(settled: TopicSettledDelete): boolean;
274
- /** Remove a topic record immediately for local/test cleanup compatibility. */
275
- delete(sessionId: string): boolean;
314
+ settleArchive(sessionId: string, topicId: string, dispatchedAuthorityEpoch: number): boolean;
315
+ /** Durable archive jobs that are eligible for a retry at `now`. */
316
+ archivePendingSessionIds(now?: number): string[];
317
+ /** Durable/manual recovery candidates that exceeded the automatic archive retry budget. */
318
+ archiveExhaustedSessionIds(): string[];
319
+ /** Persist an indefinitely discoverable retry after an ambiguous archive result. */
320
+ scheduleArchiveRetry(sessionId: string, now: number, diagnostic?: string): ArchiveJob | undefined;
276
321
  /** Serialise active records plus unpublished staged creates for atomic commit. */
277
322
  serialize(): TopicRegistryState;
278
323
  }
@@ -2,6 +2,35 @@ import { type EventFrame, SessionEventStream } from "./events";
2
2
  import { ReverseLeaseRuntime } from "./reverse-leases";
3
3
  import type { BrokerIndexWriter, HostEndpointAdapters, SdkFrame } from "./types";
4
4
  export type SdkRequestObserver = (kind: "control" | "query", connectionId: string, frame: SdkFrame) => void;
5
+ /**
6
+ * When a session publishes its replayable readiness signal.
7
+ *
8
+ * `immediate` is the stock contract: `start()` publishes `session_ready` at
9
+ * once, so a chat daemon that attaches (or replays late) surfaces the session
10
+ * and creates its stock root. `deferred` prepares the session instead: the
11
+ * session id, endpoint, and broker registration become discoverable authority,
12
+ * but no readiness exists for any consumer to act on until `activate()` runs.
13
+ * That ordering is what lets an operator adopt an existing chat root before the
14
+ * daemon would otherwise have published one of its own.
15
+ */
16
+ export type SessionReadinessMode = "immediate" | "deferred";
17
+ /**
18
+ * Replayable signal that a deferred session is fully initialized and holding
19
+ * endpoint authority while its readiness stays withheld.
20
+ *
21
+ * It is deliberately not `session_ready`: a broker lifecycle wait can
22
+ * authenticate it as the child's semantic completion receipt, while every
23
+ * readiness consumer — chat daemons above all — keeps ignoring it and publishes
24
+ * no root until the session is explicitly activated.
25
+ */
26
+ export declare const SESSION_PREPARED_EVENT = "session_prepared";
27
+ /** Every terminal answer an activation attempt can produce. */
28
+ export type SessionActivationOutcome = "activated" | "already" | "not_prepared" | "generation_changed" | "not_authorized" | "authority_unavailable";
29
+ /** Proves that a prepared session may publish readiness at this exact generation. */
30
+ export type SessionActivationGate = (input: {
31
+ sessionId: string;
32
+ generation: number;
33
+ }) => boolean | Promise<boolean>;
5
34
  export interface SessionSdkHostOptions extends HostEndpointAdapters {
6
35
  control?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
7
36
  query?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
@@ -16,6 +45,14 @@ export interface SessionSdkHostOptions extends HostEndpointAdapters {
16
45
  onReverseCancel?: (requestId: string, reason: "provider_disconnected" | "lease_released") => void;
17
46
  /** Best-effort capabilities mirrored from the native transport for out-of-band consumers. */
18
47
  connectionCapabilities?: (connectionId: string) => ReadonlySet<string> | undefined;
48
+ /** Readiness publication mode; defaults to the stock immediate contract. */
49
+ readiness?: SessionReadinessMode;
50
+ /**
51
+ * Authorization for a deferred activation. It is consulted on every attempt
52
+ * that would publish readiness and never on an idempotent replay, and a gate
53
+ * that fails is never read as authorization.
54
+ */
55
+ activationGate?: SessionActivationGate;
19
56
  }
20
57
  /** SDK hosting is independent of notification configuration. Only root sessions host an endpoint. */
21
58
  export declare function shouldHostSdk(_settings: unknown, isTopLevel: boolean, env?: NodeJS.ProcessEnv): boolean;
@@ -27,6 +64,10 @@ export declare class SessionSdkHost {
27
64
  constructor(options: SessionSdkHostOptions);
28
65
  get started(): boolean;
29
66
  get generation(): number;
67
+ /** True while the session holds endpoint authority but has published no readiness. */
68
+ get prepared(): boolean;
69
+ /** True once readiness for the current generation has been published. */
70
+ get ready(): boolean;
30
71
  /** Current installed definitions for a live provider capability. */
31
72
  getProviderDefinitions(capability: string): unknown | undefined;
32
73
  /** Release reverse leases after the transport reports a WebSocket disconnect. */
@@ -34,6 +75,17 @@ export declare class SessionSdkHost {
34
75
  /** Adds an event to the resumable event ring. Transport delivery is owned by bus wiring. */
35
76
  emitEvent(frame: SdkFrame): EventFrame;
36
77
  start(): Promise<"started" | "already">;
78
+ /**
79
+ * Publish the readiness signal a prepared session withheld.
80
+ *
81
+ * The attempt is refused unless the session is still started at exactly
82
+ * `expectedGeneration` (when supplied) and the activation gate authorizes it
83
+ * at that same generation. Authority is re-proved after the gate resolves,
84
+ * because a stop or an endpoint roll can land while it is in flight, and an
85
+ * exact retry after a successful activation is answered `already` instead of
86
+ * publishing a second readiness signal.
87
+ */
88
+ activate(expectedGeneration?: number): Promise<SessionActivationOutcome>;
37
89
  stop(): Promise<"stopped" | "already">;
38
90
  registerWithBroker(writer: BrokerIndexWriter): Promise<void>;
39
91
  }
@@ -18,6 +18,12 @@ export type CreateLifecycleAgentSessionOptions = CreateAgentSessionOptions & {
18
18
  * the manager's short default ceiling.
19
19
  */
20
20
  mcpStartupTimeoutMs?: number;
21
+ /**
22
+ * The broker-issued readiness intent from this session's launch request.
23
+ * `deferred` prepares the session: it holds endpoint authority and publishes
24
+ * a prepared signal instead of readiness until it is explicitly activated.
25
+ */
26
+ readiness?: "immediate" | "deferred";
21
27
  };
22
28
  /** Internal lifecycle-only session construction with an owner-bound SDK startup result. */
23
29
  export declare function createLifecycleAgentSession(options?: CreateLifecycleAgentSessionOptions): Promise<CreateLifecycleAgentSessionResult>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The activation exchange a prepared session answers, and nothing else.
3
+ *
4
+ * A prepared session holds discoverable endpoint authority while its readiness
5
+ * stays withheld; activation is the one request that asks it to publish that
6
+ * readiness. Every caller shares this exchange whatever proved the endpoint
7
+ * authority beforehand: the `gjc notify activate-thread` CLI resolves it from
8
+ * the local session index, and the Coordinator resolves it from the broker's
9
+ * incarnation-bound binding.
10
+ *
11
+ * It lives outside `sdk/bus` on purpose. The existing-thread module reaches the
12
+ * chat-daemon and settings graph to prove a Slack binding, and the Coordinator
13
+ * MCP entrypoint may not reach that graph at all, so the exchange the machine
14
+ * entrypoint needs carries no notification, daemon, or settings dependency.
15
+ */
16
+ export type SessionActivationErrorCode = "session_not_live" | "not_prepared" | "not_bound" | "activation_unavailable" | "activation_outcome_unknown";
17
+ /** A fail-closed rejection while activating a prepared session. */
18
+ export declare class SessionActivationError extends Error {
19
+ readonly code: SessionActivationErrorCode;
20
+ constructor(code: SessionActivationErrorCode, message: string);
21
+ }
22
+ /** Safe confirmation of an activation. It carries identifiers only. */
23
+ export interface ActivatedPreparedSession {
24
+ sessionId: string;
25
+ endpointGeneration: number;
26
+ status: "activated" | "already";
27
+ }
28
+ /** The narrow transport surface activation needs from an SDK connection. */
29
+ export interface PreparedSessionActivationClient {
30
+ request(frame: Record<string, unknown>): Promise<Record<string, unknown>>;
31
+ close(): Promise<void>;
32
+ }
33
+ /**
34
+ * Ask one already-connected session to publish the readiness it withheld.
35
+ *
36
+ * The frame names the exact session and endpoint generation, and the answer is
37
+ * accepted only when it proves both, so a replacement session can never be
38
+ * activated in place of the requested one. Activation is idempotent: an exact
39
+ * retry answers `already` without a second readiness signal.
40
+ */
41
+ export declare function requestPreparedSessionActivation(client: PreparedSessionActivationClient, sessionId: string, endpointGeneration: number): Promise<ActivatedPreparedSession>;
@@ -120,6 +120,16 @@ export interface CreateAgentSessionOptions {
120
120
  /** Load MCP tools for a top-level session only from this caller-owned absolute config file path.
121
121
  * Mutually exclusive with mcpManager. */
122
122
  mcpConfigPath?: string;
123
+ /**
124
+ * Defer connecting an exact MCP config until the interactive UI is ready.
125
+ * @internal CLI-only startup optimization; SDK callers retain synchronous loading by default.
126
+ */
127
+ deferMcpConfigStartup?: boolean;
128
+ /**
129
+ * Defer memory backend startup until the caller has applied startup model profiles.
130
+ * @internal CLI-only ordering guard; SDK callers retain immediate startup by default.
131
+ */
132
+ deferMemoryBackendStartup?: boolean;
123
133
  /** Enable LSP integration (tool, formatting, diagnostics, warmup). Default: true */
124
134
  enableLsp?: boolean;
125
135
  /** Skip Python kernel availability check and prelude warmup */
@@ -202,6 +212,10 @@ export interface CreateAgentSessionResult {
202
212
  setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
203
213
  /** MCP manager for server lifecycle management (undefined if MCP disabled or an exact tools-only config is session-owned) */
204
214
  mcpManager?: MCPManager;
215
+ /** Starts a deferred exact-config MCP connection. Present only when deferMcpConfigStartup was requested. */
216
+ startDeferredMcpConfig?: () => Promise<DeferredMcpConfigStartupResult>;
217
+ /** Starts a deferred memory backend. Present only when deferMemoryBackendStartup was requested. */
218
+ startDeferredMemoryBackend?: () => void;
205
219
  /** Warning if session was restored with a different model than saved */
206
220
  modelFallbackMessage?: string;
207
221
  /** LSP servers configured for lazy startup in interactive mode */
@@ -214,6 +228,10 @@ export interface CreateAgentSessionResult {
214
228
  */
215
229
  gjcRuntimeSnapshot?: GjcRuntimeSnapshotProvider;
216
230
  }
231
+ export interface DeferredMcpConfigStartupResult {
232
+ loadedToolCount: number;
233
+ hasErrors: boolean;
234
+ }
217
235
  export type { PromptTemplate } from "../config/prompt-templates";
218
236
  export { Settings, type SkillsSettings } from "../config/settings";
219
237
  export type { CustomCommand, CustomCommandFactory } from "../extensibility/custom-commands/types";
@@ -48,9 +48,16 @@ export declare function normalizeSdkStartupFailure(phase: SdkStartupPhase, reaso
48
48
  export declare class SdkStartupCapability {
49
49
  #private;
50
50
  readonly rollback?: SdkStartupRollbackTracker | undefined;
51
+ readonly readiness: "immediate" | "deferred";
51
52
  get cancelled(): boolean;
52
53
  cancel(): void;
53
- constructor(rollback?: SdkStartupRollbackTracker | undefined);
54
+ /**
55
+ * The broker-issued, session-scoped readiness intent this child was launched
56
+ * with. It is carried on the capability rather than read from the ambient
57
+ * environment so bus wiring can never mistake an inherited process-global
58
+ * flag for a broker-managed preparation.
59
+ */
60
+ constructor(rollback?: SdkStartupRollbackTracker | undefined, readiness?: "immediate" | "deferred");
54
61
  normalizeFailure(phase: SdkStartupPhase, reason: SdkStartupReason, error?: unknown): SdkStartupFailure;
55
62
  get result(): SdkStartupResult | undefined;
56
63
  get promise(): Promise<SdkStartupResult>;
@@ -262,6 +262,8 @@ export interface AgentSessionConfig {
262
262
  discoveryMode?: "off" | "mcp-only" | "all";
263
263
  /** MCP tool names to activate for the current session when discovery mode is enabled. */
264
264
  initialSelectedMCPToolNames?: string[];
265
+ /** Keep persisted MCP names until a deferred exact catalog becomes available. */
266
+ preserveUnavailableInitialMCPToolSelection?: boolean;
265
267
  /** Built-in discoverable tool names restored for the current all-discovery session. */
266
268
  initialSelectedDiscoveredBuiltinToolNames?: string[];
267
269
  /** Discoverable built-ins active for configured or explicit reasons independently of persisted discovery selection. */
@@ -307,6 +309,8 @@ export interface AgentSessionConfig {
307
309
  * process-global manager **MUST NOT** dispose it on their own teardown.
308
310
  */
309
311
  ownedMcpManager?: MCPManager;
312
+ /** Optional startup dependency that must settle before the first provider turn. */
313
+ startupTurnBarrier?: Promise<void>;
310
314
  /** Optional fork-context seed used to initialize a child session before its first prompt. */
311
315
  forkContextSeed?: ForkContextSeed;
312
316
  /** Optional provider state override. Fork-context children should omit this by default. */
@@ -558,6 +562,7 @@ export declare class AgentSession {
558
562
  nonEditDeterminations: number;
559
563
  };
560
564
  readonly rawSseDebugBuffer: RawSseDebugBuffer;
565
+ extendStartupTurnBarrier(barrier: Promise<void>): void;
561
566
  constructor(config: AgentSessionConfig);
562
567
  /**
563
568
  * Resolves when constructor-time workflow-gate tool restoration (ask
@@ -634,7 +639,9 @@ export declare class AgentSession {
634
639
  * separately inside `sendCustomMessage`. */
635
640
  enqueueCustomMessageDisplay(text: string, mode: "steer" | "followUp"): string;
636
641
  getAgentId(): string | undefined;
642
+ get isDisposed(): boolean;
637
643
  registerToolSessionCleanup(cleanup: () => Promise<void> | void): () => void;
644
+ registerToolSessionTransitionCleanup(cleanup: () => Promise<void> | void): () => void;
638
645
  getAsyncJobSnapshot(options?: {
639
646
  recentLimit?: number;
640
647
  }): AsyncJobSnapshot | null;
@@ -54,8 +54,8 @@ export declare class ArtifactManager {
54
54
  */
55
55
  removeNamedBestEffort(filename: string): Promise<boolean>;
56
56
  /**
57
- * Atomically allocate next artifact ID.
58
- * IDs are sequential within the session.
57
+ * Atomically claim the next artifact ID after this manager has been initialized.
58
+ * Prefer `allocatePath` or `save`; this synchronous seam exists for pruning callbacks.
59
59
  */
60
60
  allocateId(): number;
61
61
  /**
@@ -615,6 +615,7 @@ interface SessionManagerStateSnapshot {
615
615
  needsFullRewriteOnNextPersist: boolean;
616
616
  fileEntries: FileEntry[];
617
617
  materializedFileEntries: FileEntry[];
618
+ adoptedArtifactManager: ArtifactManager | null;
618
619
  }
619
620
  /** Benchmark-derived cap for strong materialized session snapshots. */
620
621
  export declare const MATERIALIZED_CACHE_MAX_BYTES: number;
@@ -623,6 +624,7 @@ export declare const SessionManagerTestHooks: {
623
624
  materializedCacheMaxBytesOverride?: number;
624
625
  beforeResidentTransitionIndexBuild?: () => void;
625
626
  afterForkSnapshot?: () => void | Promise<void>;
627
+ beforeEphemeralArtifactManagerInstall?: (dir: string) => void | Promise<void>;
626
628
  };
627
629
  export declare class SessionManager {
628
630
  #private;
@@ -643,7 +645,9 @@ export declare class SessionManager {
643
645
  captureState(): SessionManagerStateSnapshot;
644
646
  restoreState(snapshot: SessionManagerStateSnapshot): void;
645
647
  /** Switch to a different session file (used for resume and branching). */
646
- setSessionFile(sessionFile: string): Promise<void>;
648
+ setSessionFile(sessionFile: string, options?: {
649
+ deferEphemeralArtifactRetirement?: boolean;
650
+ }): Promise<void>;
647
651
  /** Start a new session. Closes any existing writer first. */
648
652
  newSession(options?: NewSessionOptions): Promise<string | undefined>;
649
653
  /**
@@ -751,13 +755,23 @@ export declare class SessionManager {
751
755
  * the parent session's artifact directory and ID counter.
752
756
  */
753
757
  adoptArtifactManager(manager: ArtifactManager): void;
758
+ /** Release only the matching externally adopted manager. */
759
+ releaseArtifactManager(manager: ArtifactManager): void;
760
+ /** Temporarily release adopted authority while an outer transition validates its successor. */
761
+ stageAdoptedArtifactManagerForTransition(): void;
762
+ /** Prove manager authority by exact object identity, never by pathname shape. */
763
+ isArtifactManagerAuthorized(manager: ArtifactManager): boolean;
754
764
  /**
755
765
  * Returns the ArtifactManager this session writes through. Lazily creates
756
766
  * one bound to the current session file unless an external manager was
757
- * adopted via `adoptArtifactManager`. Returns null only for non-persistent
758
- * sessions with no adopted manager.
767
+ * adopted via `adoptArtifactManager`. Falls back to the lazily created
768
+ * ephemeral filesystem store once a non-persistent session has saved an
769
+ * artifact, so `artifact://` stays resolvable. Returns null only when no
770
+ * store has been established yet.
759
771
  */
760
772
  getArtifactManager(): ArtifactManager | null;
773
+ /** Linearizably establish this session's persistent or ephemeral artifact manager. */
774
+ ensureArtifactManager(): Promise<ArtifactManager | null>;
761
775
  /**
762
776
  * Allocate a new artifact path and ID for the current session.
763
777
  * Returns an empty object when the session is not persisted.
@@ -768,14 +782,18 @@ export declare class SessionManager {
768
782
  }>;
769
783
  /**
770
784
  * Save artifact content under the current session and return artifact ID.
771
- * Returns an artifact ID for all sessions (file-backed for persistent, in-memory fallback otherwise).
785
+ * Persistent sessions write into the session artifact directory; non-persistent
786
+ * sessions write into a lazily created temporary directory so the content is
787
+ * read back from the filesystem instead of being retained in memory.
772
788
  */
773
789
  saveArtifact(content: string, toolType: string): Promise<string | undefined>;
774
790
  /**
775
791
  * Resolve an artifact ID to an on-disk path for the current session.
776
- * Returns null when missing or when the session is not persisted.
792
+ * Returns null when the artifact is missing.
777
793
  */
778
794
  getArtifactPath(id: string): Promise<string | null>;
795
+ /** Retire predecessor ephemeral artifacts after an outer logical transition commits. */
796
+ retireEphemeralArtifactsAfterTransition(): void;
779
797
  /**
780
798
  * Persist (or clear) the current editor draft so the next resume of this
781
799
  * session can restore it. Empty text deletes any stale draft. No-op when the
@@ -219,6 +219,14 @@ export interface ToolSession {
219
219
  getAuthorizedArtifactsDirs?: () => readonly string[];
220
220
  /** Get the ArtifactManager backing this session (shared across parent + subagents). */
221
221
  getArtifactManager?: () => ArtifactManager | null;
222
+ /** Prove that an ArtifactManager belongs to this concrete session or its explicitly adopted parent tree. */
223
+ isArtifactManagerAuthorized?: (manager: ArtifactManager) => boolean;
224
+ /** Adopt a task-created fallback manager into the concrete session owner. */
225
+ adoptArtifactManager?: (manager: ArtifactManager) => void;
226
+ /** Linearizably establish the concrete session owner's canonical artifact manager. */
227
+ ensureArtifactManager?: () => Promise<ArtifactManager | null>;
228
+ /** Release a task-created fallback manager when its logical session ends. */
229
+ releaseArtifactManager?: (manager: ArtifactManager) => void;
222
230
  /** Register teardown work owned by the current logical session. */
223
231
  registerSessionCleanup?: (cleanup: () => Promise<void> | void) => () => void;
224
232
  /** Allocate a new artifact path and ID for session-scoped truncated output. */