@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
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Request/response control channel for chat-daemon commands that must be
3
+ * executed *by* the running owner instead of terminating it.
4
+ *
5
+ * `control.json` is the lifecycle channel: every request the owner recognizes
6
+ * there ends its serving loop (stop/reload). Adopting an existing Slack root
7
+ * must not stop a healthy daemon, so operator commands travel on this separate
8
+ * per-request channel and are answered in place.
9
+ *
10
+ * Invariants:
11
+ * - Every request is addressed to an exact owner (`ownerId`, `pid`,
12
+ * `incarnation`, daemon `generation`). A daemon that is not that exact owner
13
+ * answers `owner_changed` and performs no work, so a replaced or restarted
14
+ * owner can never satisfy a request captured against its predecessor. The
15
+ * same tuple is re-proven inside the commit fence and echoed in the answer,
16
+ * so the submitter can verify who actually acted.
17
+ * - `<id>.response.json` is a single-winner arbitration object created with
18
+ * `O_CREAT|O_EXCL`. The serving daemon must win it *before* it commits, and a
19
+ * submitter that has given up must win it before reporting failure. Whoever
20
+ * loses learns so definitively, which removes the commit-vs-timeout race
21
+ * without any sleep: a definitive `cancelled` answer proves no dispatch can
22
+ * follow within this submission's race, and a lost cancellation is reported
23
+ * as `unknown` rather than as a failure the caller could act on. The
24
+ * arbitration is scoped to that race under the trust boundary below; it is
25
+ * not durable settlement across a crashed and resurrected daemon.
26
+ * - Documents live in an owner-only (`0700`) directory below the agent
27
+ * directory, are bounded, and are rejected unless they are ordinary regular
28
+ * files. This matches the product's existing same-UID local trust boundary;
29
+ * it is not a defence against a hostile process running as the same user.
30
+ * - Only identifiers travel through the channel. Tokens, message bodies, and
31
+ * control secrets are never written here.
32
+ *
33
+ * The channel proves *correlation*, never authorship: every field a response
34
+ * echoes is copied verbatim out of the plaintext request published beside it,
35
+ * so a definitive answer is only actionable once the caller corroborates the
36
+ * mutation against an authority outside this directory.
37
+ */
38
+ import { type ChatDaemonKind } from "./chat-daemon-control";
39
+ export declare const CHAT_DAEMON_COMMAND_VERSION = 1;
40
+ export declare const DEFAULT_CHAT_DAEMON_COMMAND_TIMEOUT_MS = 15000;
41
+ export type ChatDaemonCommandName = "bind-thread";
42
+ export type ChatDaemonCommandStatus = "ok" | "rejected" | "owner_changed" | "expired" | "outcome_unknown";
43
+ /** Exact owner authority a request is addressed to. */
44
+ export interface ChatDaemonCommandOwner {
45
+ ownerId: string;
46
+ pid: number;
47
+ incarnation: string;
48
+ generation: number;
49
+ }
50
+ export interface ChatDaemonCommandRequest extends ChatDaemonCommandOwner {
51
+ version: typeof CHAT_DAEMON_COMMAND_VERSION;
52
+ requestId: string;
53
+ kind: ChatDaemonKind;
54
+ command: ChatDaemonCommandName;
55
+ sessionId: string;
56
+ rootTs: string;
57
+ createdAt: number;
58
+ expiresAt: number;
59
+ }
60
+ /**
61
+ * The answer to exactly one request.
62
+ *
63
+ * The owner tuple, session, and root are the *addressed* request's, echoed
64
+ * verbatim. A response therefore carries the complete request envelope, and a
65
+ * submitter can prove that the document in front of it is correlated with its
66
+ * own request rather than with a replayed or concurrent one.
67
+ *
68
+ * That is correlation, not authentication. Every echoed field is public: it is
69
+ * copied out of the plaintext request published in the same command directory,
70
+ * so a stale or planted document can satisfy the envelope without the addressed
71
+ * daemon ever running. A caller that needs a definitive outcome must corroborate
72
+ * it against an authority outside this directory (the durable mutation itself).
73
+ */
74
+ export interface ChatDaemonCommandResponse extends ChatDaemonCommandOwner {
75
+ version: typeof CHAT_DAEMON_COMMAND_VERSION;
76
+ requestId: string;
77
+ kind: ChatDaemonKind;
78
+ command: ChatDaemonCommandName;
79
+ sessionId: string;
80
+ rootTs: string;
81
+ status: ChatDaemonCommandStatus;
82
+ /** Machine-readable rejection category; never a message body or credential. */
83
+ code?: string;
84
+ endpointGeneration?: number;
85
+ teamId?: string;
86
+ channelId?: string;
87
+ completedAt: number;
88
+ }
89
+ /**
90
+ * Result of one handler dispatch, carrying explicit commit certainty.
91
+ *
92
+ * A failure is never just a code. `rejected` asserts that no mapping changed, so
93
+ * the caller may be told the binding definitively failed. `unknown` asserts the
94
+ * opposite: commit authority was exercised and the mapping may already be
95
+ * applied, so no definitive rejection may be reported for it. Handlers state
96
+ * this directly instead of leaving the channel to infer it from error text.
97
+ */
98
+ export type ChatDaemonCommandOutcome = {
99
+ ok: true;
100
+ sessionId: string;
101
+ endpointGeneration: number;
102
+ teamId: string;
103
+ channelId: string;
104
+ rootTs: string;
105
+ } | {
106
+ ok: false;
107
+ certainty: "rejected";
108
+ code: string;
109
+ } | {
110
+ ok: false;
111
+ certainty: "unknown";
112
+ code: string;
113
+ };
114
+ export interface ChatDaemonCommandBindInput {
115
+ sessionId: string;
116
+ rootTs: string;
117
+ /**
118
+ * Terminal authority for this exact request. It must be awaited inside the
119
+ * store fence, immediately before the commit, and the commit must be
120
+ * abandoned when it answers `false`. It re-proves the exact daemon owner
121
+ * tuple, that the request is still published and unexpired, and takes the
122
+ * single-winner response claim, after which no cancellation can succeed.
123
+ */
124
+ commitAuthority?: () => Promise<boolean>;
125
+ }
126
+ /** Daemon-side executor. Implemented by the runtime that owns the live transports. */
127
+ export interface ChatDaemonCommandHandler {
128
+ bindExistingRoot(input: ChatDaemonCommandBindInput): Promise<ChatDaemonCommandOutcome>;
129
+ }
130
+ export declare function isChatDaemonCommandRequest(value: unknown): value is ChatDaemonCommandRequest;
131
+ export declare function isChatDaemonCommandResponse(value: unknown): value is ChatDaemonCommandResponse;
132
+ /**
133
+ * The owner-only directory the channel exchanges its documents in.
134
+ *
135
+ * `directory` is a plain pathname: this codebase already trusts same-UID local
136
+ * processes, and every document below it is bounded and validated, so the
137
+ * channel does not attempt hostile-filesystem hardening it cannot honestly
138
+ * provide from TypeScript.
139
+ */
140
+ export interface ChatDaemonCommandScope {
141
+ readonly directory: string;
142
+ }
143
+ export interface OpenChatDaemonCommandScopeInput {
144
+ agentDir: string;
145
+ kind: ChatDaemonKind;
146
+ /** Creates the daemon and command directories owner-only when absent. */
147
+ create?: boolean;
148
+ }
149
+ /** Capture the command directory, or report the channel unusable. */
150
+ export declare function openChatDaemonCommandScope(input: OpenChatDaemonCommandScopeInput): Promise<ChatDaemonCommandScope | undefined>;
151
+ /** Build a request addressed to one exact owner. */
152
+ export declare function buildChatDaemonCommandRequest(input: {
153
+ kind: ChatDaemonKind;
154
+ command: ChatDaemonCommandName;
155
+ owner: ChatDaemonCommandOwner;
156
+ sessionId: string;
157
+ rootTs: string;
158
+ now?: number;
159
+ ttlMs?: number;
160
+ requestId?: string;
161
+ }): ChatDaemonCommandRequest;
162
+ export interface SubmitChatDaemonCommandInput {
163
+ agentDir: string;
164
+ kind: ChatDaemonKind;
165
+ owner: ChatDaemonCommandOwner;
166
+ command: ChatDaemonCommandName;
167
+ sessionId: string;
168
+ rootTs: string;
169
+ timeoutMs?: number;
170
+ pollIntervalMs?: number;
171
+ /** Bounded wait after a lost cancellation, before the outcome is reported unknown. */
172
+ settleGraceMs?: number;
173
+ ttlMs?: number;
174
+ now?: () => number;
175
+ sleep?: (ms: number) => Promise<void>;
176
+ requestId?: string;
177
+ }
178
+ /**
179
+ * Terminal result of one submission.
180
+ *
181
+ * `cancelled` is the only definitive failure: the submitter won the response
182
+ * claim, so the addressed owner cannot dispatch this request from here on within
183
+ * this submission's race. `unknown` means the daemon won that claim first and its
184
+ * answer was not observed; the caller must not report a definitive failure,
185
+ * because a commit may already be applied.
186
+ *
187
+ * `untrusted` means the response object under this identifier is a complete,
188
+ * well-formed document that does not carry this request's envelope. It is
189
+ * channel corruption — stale or planted — so it may not be read as an answer of
190
+ * any status, and because the identifier is now occupied the real outcome is
191
+ * unknowable from here; the caller must treat it exactly like `unknown`.
192
+ */
193
+ export type ChatDaemonCommandSubmission = {
194
+ outcome: "answered";
195
+ response: ChatDaemonCommandResponse;
196
+ } | {
197
+ outcome: "cancelled";
198
+ } | {
199
+ outcome: "unknown";
200
+ } | {
201
+ outcome: "untrusted";
202
+ code: "response_envelope_mismatch";
203
+ } | {
204
+ outcome: "unavailable";
205
+ code: "command_channel_unavailable" | "request_id_unavailable";
206
+ };
207
+ /**
208
+ * Publish a command for the addressed owner and wait for its answer.
209
+ *
210
+ * The wait ends in exactly one of four states, and the commit-vs-timeout race is
211
+ * arbitrated by the exclusive creation of the response object rather than by any
212
+ * timing assumption.
213
+ */
214
+ export declare function submitChatDaemonCommand(input: SubmitChatDaemonCommandInput): Promise<ChatDaemonCommandSubmission>;
215
+ export interface ServeChatDaemonCommandsInput {
216
+ agentDir: string;
217
+ kind: ChatDaemonKind;
218
+ /** The serving daemon's own proven authority. */
219
+ ownerId: string;
220
+ pid: number;
221
+ incarnation: string;
222
+ generation: number;
223
+ handler: ChatDaemonCommandHandler;
224
+ /**
225
+ * Re-proves that this process still holds the persisted owner record. It is
226
+ * checked before any work is dispatched *and again* inside the commit fence,
227
+ * so a daemon that lost ownership after dispatch mutates nothing.
228
+ */
229
+ verifyOwnership?: () => Promise<boolean>;
230
+ now?: () => number;
231
+ }
232
+ /**
233
+ * Answer every pending request addressed to this exact owner. Requests aimed at
234
+ * any other owner identity are answered `owner_changed` without doing work, and
235
+ * expired requests are answered `expired`; neither performs a mutation.
236
+ */
237
+ export declare function serveChatDaemonCommandsOnce(input: ServeChatDaemonCommandsInput): Promise<number>;
238
+ /**
239
+ * Answer every pending request against an already-captured command scope.
240
+ *
241
+ * Exposed so a caller that already holds the directory authority drives exactly
242
+ * the production path.
243
+ */
244
+ export declare function serveChatDaemonCommandsAgainstScope(scope: ChatDaemonCommandScope, input: ServeChatDaemonCommandsInput): Promise<number>;
@@ -32,7 +32,11 @@ export type ChatDaemonAction = "stop" | "reload";
32
32
  * Discord generation 24 / slack generation 23 apply provider-completeness and
33
33
  * effective-enable admission to chat daemon lifecycle controls. Discord
34
34
  * generation 25 / slack generation 24 apply identity-bound exact replacement
35
- * cleanup shared by managed-session and daemon filesystem authority.
35
+ * cleanup shared by managed-session and daemon filesystem authority. Discord
36
+ * generation 26 / slack generation 25 add the in-place operator command channel:
37
+ * an owner serves per-request commands inside its own serving loop and answers
38
+ * them against an exact owner tuple, so an owner at an earlier generation may
39
+ * not serve or answer a request captured against this contract.
36
40
  */
37
41
  export declare const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>>;
38
42
  export declare function chatDaemonGeneration(kind: ChatDaemonKind): number;
@@ -93,6 +97,12 @@ export declare function chatDaemonPaths(agentDir: string, kind: ChatDaemonKind):
93
97
  state: string;
94
98
  control: string;
95
99
  };
100
+ /**
101
+ * Configuration fingerprint that identifies which settings a daemon owner was
102
+ * started for. `undefined` means the current settings cannot configure that
103
+ * transport at all, so no owner can be authorized against them.
104
+ */
105
+ export declare function chatDaemonIdentity(settings: Settings, kind: ChatDaemonKind): string | undefined;
96
106
  export declare function readChatDaemonState(agentDir: string, kind: ChatDaemonKind): Promise<ChatDaemonState | undefined>;
97
107
  export declare function readChatDaemonControlRequest(agentDir: string, kind: ChatDaemonKind): Promise<ChatDaemonControlRequest | undefined>;
98
108
  export declare function writeChatDaemonControlRequest(agentDir: string, kind: ChatDaemonKind, request: ChatDaemonControlRequest): Promise<void>;
@@ -1,5 +1,6 @@
1
1
  import { SessionIndex } from "../broker/session-index";
2
2
  import { type SdkSessionEndpoint } from "../client/discovery";
3
+ import type { ChatDaemonCommandBindInput, ChatDaemonCommandOutcome } from "./chat-daemon-command-channel";
3
4
  import type { ChatDaemonKind } from "./chat-daemon-control";
4
5
  import type { DiscordProvider } from "./discord-provider";
5
6
  import { type SlackProviderClient } from "./slack-provider";
@@ -69,6 +70,12 @@ export declare class ChatDaemonRuntime {
69
70
  private reconcile;
70
71
  private attach;
71
72
  private resolveEndpoint;
73
+ /**
74
+ * Adopt an operator-supplied Slack root for one attached session. Returns a
75
+ * machine-readable rejection instead of throwing so the daemon control plane
76
+ * can answer without exposing internal failure detail.
77
+ */
78
+ bindExistingRoot(request: ChatDaemonCommandBindInput): Promise<ChatDaemonCommandOutcome>;
72
79
  private schedule;
73
80
  private enqueueFrame;
74
81
  private handleFrame;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Typed control-plane discriminants that must never reach chat presentation.
3
+ *
4
+ * A chat daemon observes one session socket for user-visible events, but that
5
+ * socket also carries the SDK's own request/response traffic: `SdkClient`
6
+ * settles a pending request from an inbound frame and still forwards the very
7
+ * same frame to every `onFrame` observer. A runtime that projects unknown frame
8
+ * types into a generic notification therefore renders protocol answers — most
9
+ * visibly `GJC event replay result` — as chat content, and a chat transport
10
+ * with no mapping for that session publishes it as a brand-new root.
11
+ *
12
+ * Membership is exact, typed, and closed. It is never inferred from a `_result`
13
+ * or `_response` suffix: ordinary public frames are free to be named anything,
14
+ * and a fuzzy rule would silently swallow real user content. Every entry below
15
+ * names the source authority that emits it and the code path that proves it can
16
+ * be delivered to a chat daemon's frame observer.
17
+ *
18
+ * In scope (proven reachable at the chat-daemon session-client seam):
19
+ * - `event_replay_result` — `SessionSdkHost` (`sdk/host/host.ts`) answers the
20
+ * `event_replay` request that `ChatDaemonRuntime.attach()` issues on every
21
+ * attachment, so every attached session receives exactly this frame.
22
+ * - `control_response` — the same host's answer to the `control_request` that
23
+ * `ChatDaemonRuntime` sends for an authorized `/sdk control …` command, and
24
+ * the endpoint-stale refusal the session runtime writes for a fenced
25
+ * connection.
26
+ * - `query_response` — the host's answer to the matching `query_request`, and
27
+ * the same endpoint-stale refusal path.
28
+ * - `hello` — `ServerMessage::Hello` from the native session server
29
+ * (`crates/gjc-sdk/src/protocol.rs`), sent to every connection at accept time
30
+ * and re-observed by frame handlers whenever the client reconnects and
31
+ * adopts a new connection id.
32
+ *
33
+ * Deliberately out of scope — typed server→client frames that exist but are
34
+ * unicast only to a connection that issued a request a chat daemon never
35
+ * sends, so no source path delivers them here: `session_activate_result`,
36
+ * `register_provider_result`, `lease_state`, `reverse_response`,
37
+ * `reverse_request`, `reverse_cancel`, `global_response`, `pong`, and
38
+ * `transport_error`. Add one only together with the source path that proves it
39
+ * reaches this seam.
40
+ */
41
+ export declare const CONTROL_PLANE_FRAME_TYPES: ReadonlySet<string>;
42
+ /** Whether an exact frame/event discriminant is protocol-only control plane. */
43
+ export declare function isControlPlaneFrameType(name: string | undefined): boolean;
@@ -70,4 +70,22 @@ export declare class ConversationStore<T extends ConversationRecord> {
70
70
  delete(key: string, expectedGeneration: number): Promise<boolean>;
71
71
  /** Apply a synchronous update under the mapping lock, retrying no stale state. */
72
72
  transact(key: string, update: (current: T | undefined) => T | undefined): Promise<T | undefined>;
73
+ /**
74
+ * Apply one key update while atomically observing every mapping in the same
75
+ * store snapshot. The observation and the write share the cross-process file
76
+ * lock, so a caller can enforce uniqueness constraints that span keys.
77
+ *
78
+ * The update may be asynchronous so a caller can re-prove local authority
79
+ * (process, endpoint, or session state) inside the lock, immediately before
80
+ * its commit; returning `current` unchanged aborts without any mutation. It
81
+ * must never perform a remote/network call while holding the lock: the lock
82
+ * is a short fence, not a place to wait on a provider.
83
+ *
84
+ * Authority proven by the update is the last proof before the atomic
85
+ * replacement. Authority that rolls after it is an ordinary lifecycle event:
86
+ * the mapping records the endpoint generation it was proven under, so a newer
87
+ * generation fences it through the same monotonic compare-and-swap every
88
+ * other writer uses.
89
+ */
90
+ transactWithSnapshot(key: string, update: (current: T | undefined, conversations: Readonly<Record<string, T>>) => T | undefined | Promise<T | undefined>): Promise<T | undefined>;
73
91
  }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The prepare → bind → activate lifecycle for adopting an existing chat root.
3
+ *
4
+ * Stock startup publishes a session's readiness immediately, so the running
5
+ * chat daemon surfaces the session and creates its own root before an operator
6
+ * could ever name an existing one. A session that opts in is *prepared*
7
+ * instead: its id, endpoint, and broker registration are discoverable authority
8
+ * while readiness stays withheld, which leaves exactly one root claim for
9
+ * `gjc notify bind-thread` to fill. Activation then publishes readiness once,
10
+ * and the daemon adopts the bound root instead of replacing it.
11
+ *
12
+ * Nothing here mutates a mapping. The gate only reads the daemon-owned store to
13
+ * prove a binding exists, and the activation client only asks the session's own
14
+ * host to publish the readiness it withheld.
15
+ */
16
+ import type { SessionIndex } from "../broker/session-index";
17
+ import type { SdkSessionEndpoint, SdkSessionEndpointScope } from "../client/discovery";
18
+ import type { SessionActivationGate } from "../host";
19
+ import { type ActivatedPreparedSession, type PreparedSessionActivationClient } from "../session-activation";
20
+ import type { ConversationStore } from "./conversation-store";
21
+ import type { SlackConversation } from "./slack-conversation";
22
+ /** Explicit, session-scoped opt-in for withholding readiness until a root is bound. */
23
+ export declare const EXISTING_THREAD_BIND_ENV = "GJC_NOTIFY_BIND_EXISTING_THREAD";
24
+ /**
25
+ * Existing-thread binding is opt-in, and only the exact value `1` opts in.
26
+ *
27
+ * Every other value — absent, empty, `0`, or anything truthy-looking — keeps
28
+ * the stock immediate-readiness contract, so no session silently loses its
29
+ * root publication because of an ambiguous environment.
30
+ */
31
+ export declare function isExistingThreadBindingRequested(env?: NodeJS.ProcessEnv): boolean;
32
+ export interface SlackBindingActivationInput {
33
+ store: ConversationStore<SlackConversation>;
34
+ teamId: string;
35
+ channelId: string;
36
+ }
37
+ /**
38
+ * Authorize activation only against an applied binding for this exact session.
39
+ *
40
+ * The proof is the daemon-owned mapping itself: an active record for this
41
+ * session, in the configured workspace and channel, carrying a root and the
42
+ * exact endpoint generation being activated. A missing, foreign, or
43
+ * stale-generation mapping is a refusal, and an unreadable store raises so the
44
+ * caller reports unavailable authority instead of an authorization.
45
+ */
46
+ export declare function createSlackBindingActivationGate(input: SlackBindingActivationInput): SessionActivationGate;
47
+ export type { ActivatedPreparedSession, PreparedSessionActivationClient, SessionActivationErrorCode, } from "../session-activation";
48
+ export { requestPreparedSessionActivation, SessionActivationError } from "../session-activation";
49
+ export interface PreparedSessionActivationInput {
50
+ sessionIndex: SessionIndex;
51
+ sessionId: string;
52
+ }
53
+ export interface PreparedSessionActivationDeps {
54
+ readEndpoint?: (repo: string, sessionId: string, scope?: SdkSessionEndpointScope) => Promise<SdkSessionEndpoint | null>;
55
+ connect?: (endpoint: {
56
+ url: string;
57
+ token: string;
58
+ }) => Promise<PreparedSessionActivationClient>;
59
+ }
60
+ /**
61
+ * Ask a prepared session to publish the readiness it withheld.
62
+ *
63
+ * The same discovery authority the binding requires is resolved first, so an
64
+ * unindexed, terminated, or endpoint-rolled session is refused before any
65
+ * connection is opened. The request names the exact session and endpoint
66
+ * generation, and the answer is accepted only when it proves both, so a
67
+ * replacement session can never be activated in place of the requested one.
68
+ * Activation is idempotent: an exact retry answers `already` without a second
69
+ * readiness signal.
70
+ */
71
+ export declare function activatePreparedSession(input: PreparedSessionActivationInput, deps?: PreparedSessionActivationDeps): Promise<ActivatedPreparedSession>;
@@ -51,6 +51,16 @@ export interface NotificationExactUnlinkResult {
51
51
  export declare function readNotificationEndpointFile(file: string): Promise<NotificationEndpointFile>;
52
52
  /** Bump when the native exact-deletion identity contract changes. */
53
53
  export declare const NATIVE_PATH_IDENTITY_CONTRACT_VERSION = 1;
54
+ /**
55
+ * Remove exactly the inspected regular file.
56
+ *
57
+ * Intermediate directory symlinks (e.g. multi-account layouts that share
58
+ * `notifications/` via a directory symlink) are resolved before the native
59
+ * no-follow unlink. Only the parent path is canonicalized; the final basename
60
+ * is rejoined so native exact_unlink still applies AT_SYMLINK_NOFOLLOW to the
61
+ * final path component at mutation time. A final-component file symlink stays
62
+ * `reparse_point` (including TOCTOU replacement after JS preflight).
63
+ */
54
64
  export declare function exactUnlinkNotificationFile(file: string, identity: NotificationEndpointFileIdentity, quarantineName: string): NotificationExactUnlinkResult;
55
65
  /** Minimal filesystem surface the service needs; injectable for tests. */
56
66
  export interface NotificationServiceFs {
@@ -1,6 +1,8 @@
1
1
  import { type SdkSessionEndpoint } from "../client/discovery";
2
2
  import { ConversationStore } from "./conversation-store";
3
3
  import { type SlackConversation } from "./slack-conversation";
4
+ export type { SlackThreadBindingErrorCode } from "./slack-thread-binding";
5
+ export { SlackThreadBindingError } from "./slack-thread-binding";
4
6
  export declare class SlackEndpointBindingError extends Error {
5
7
  constructor(message?: string);
6
8
  }
@@ -11,6 +13,11 @@ export interface SlackEndpoint extends SdkSessionEndpoint {
11
13
  export interface SlackSdkClient {
12
14
  send(frame: Record<string, unknown>): void;
13
15
  }
16
+ /** Proven right to bind one session at one endpoint generation. */
17
+ export interface SlackBindingAuthority {
18
+ sessionId: string;
19
+ endpointGeneration: number;
20
+ }
14
21
  export interface SlackNotificationDaemonOptions {
15
22
  agentDir: string;
16
23
  repo: string;
@@ -27,6 +34,12 @@ export interface SlackNotificationDaemonOptions {
27
34
  publicationOwnerId?: string;
28
35
  publicationLeaseMs?: number;
29
36
  resolveEndpoint?: (sessionId: string) => Promise<SlackEndpoint | null>;
37
+ /**
38
+ * Exact discovery/attachment authority for adopting an existing root. The
39
+ * runtime supplies index, endpoint, and attachment proof; without it only the
40
+ * resolvable endpoint generation can be proven.
41
+ */
42
+ resolveBindingAuthority?: (sessionId: string) => Promise<SlackBindingAuthority | undefined>;
30
43
  createClient: (endpoint: SlackEndpoint) => SlackSdkClient;
31
44
  onCommand?: (sessionId: string, content: string, endpoint: SlackEndpoint, idempotencyKey: string) => Promise<boolean>;
32
45
  }
@@ -46,6 +59,20 @@ export declare class SlackNotificationDaemon {
46
59
  /** Accepted inbound effects are durably claimed before their Socket Mode ACK. */
47
60
  handleEnvelope(envelope: SlackSocketEnvelope): Promise<boolean>;
48
61
  postRoot(sessionId: string, body: string, endpointGeneration?: number): Promise<SlackConversation>;
62
+ /**
63
+ * Adopt an operator-supplied root in the configured workspace and channel
64
+ * without publishing a replacement root.
65
+ *
66
+ * The root is verified against the provider before any lock is taken, and the
67
+ * claim re-proves session authority inside the store lock. When the caller is
68
+ * the daemon command channel it also supplies `commitAuthority`, which
69
+ * re-proves the exact daemon owner tuple and takes terminal request
70
+ * authority in the same fence, so an ownership change or a cancellation
71
+ * between dispatch and commit leaves the store untouched. The claim targets
72
+ * the same session key stock publication uses, so a concurrent first
73
+ * notification observes the adopted root instead of posting a second one.
74
+ */
75
+ bindExistingRoot(sessionId: string, rootTs: string, commitAuthority?: () => Promise<boolean>): Promise<SlackConversation>;
49
76
  /** Deliver a notification into the mapped root thread, creating that root once. */
50
77
  notify(sessionId: string, body: string, actionId?: string, endpointGeneration?: number): Promise<SlackConversation>;
51
78
  /** Posts a safe command outcome to the active mapped root thread. */
@@ -63,4 +63,15 @@ export declare class SlackLiveProvider implements SlackProviderClient, SlackDiag
63
63
  clientMsgId: string;
64
64
  threadTs?: string;
65
65
  }): Promise<SlackMessageSearchResult | null>;
66
+ /**
67
+ * Confirm that `ts` addresses a real message in `channel`. Slack answers
68
+ * `conversations.replies` with the addressed message first; a timestamp that
69
+ * is unknown, deleted, or lives in another channel produces a Web API error
70
+ * instead, which surfaces as a `SlackProviderError` the caller treats as an
71
+ * unverified root.
72
+ */
73
+ findMessageByTimestamp(input: {
74
+ channel: string;
75
+ ts: string;
76
+ }): Promise<SlackMessageSearchResult | null>;
66
77
  }
@@ -50,6 +50,15 @@ export interface SlackProviderClient {
50
50
  clientMsgId: string;
51
51
  threadTs?: string;
52
52
  }): Promise<SlackMessageSearchResult | null>;
53
+ /**
54
+ * Prove that an operator-supplied timestamp addresses a real message in the
55
+ * requested channel. Adoption of an existing root is refused when a client
56
+ * cannot answer this, so verification is never silently skipped.
57
+ */
58
+ findMessageByTimestamp?(input: {
59
+ channel: string;
60
+ ts: string;
61
+ }): Promise<SlackMessageSearchResult | null>;
53
62
  readonly transportHealthy?: boolean;
54
63
  }
55
64
  /**
@@ -74,4 +83,9 @@ export declare class SlackProvider {
74
83
  clientMsgId: string;
75
84
  threadTs?: string;
76
85
  }): Promise<SlackMessageSearchResult | null>;
86
+ /** Fail-closed root verification: a client without this capability can never confirm a root. */
87
+ findMessageByTimestamp(input: {
88
+ channel: string;
89
+ ts: string;
90
+ }): Promise<SlackMessageSearchResult | null>;
77
91
  }