@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.
- package/CHANGELOG.md +38 -0
- package/dist/types/cli/notify-cli.d.ts +42 -1
- package/dist/types/cli/update-cli.d.ts +9 -3
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/main.d.ts +6 -2
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +23 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
- package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
- package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
- package/dist/types/sdk/bus/notification-service.d.ts +10 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +24 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +104 -16
- package/dist/types/sdk/bus/telegram-reference.d.ts +10 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
- package/dist/types/sdk/host/host.d.ts +52 -0
- package/dist/types/sdk/lifecycle-session.d.ts +6 -0
- package/dist/types/sdk/session-activation.d.ts +41 -0
- package/dist/types/sdk/session.d.ts +18 -0
- package/dist/types/sdk/startup-capability.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +7 -0
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +23 -5
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/utils/npm-registry.d.ts +71 -0
- package/examples/extensions/README.md +38 -4
- package/examples/extensions/ooo-bridge.ts +15 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/src/async/job-manager.ts +9 -2
- package/src/cli/notify-cli.ts +226 -2
- package/src/cli/update-cli.ts +90 -23
- package/src/commands/notify.ts +27 -3
- package/src/commands/sdk.ts +1 -0
- package/src/config/file-lock-gc.ts +18 -0
- package/src/config/file-lock.ts +61 -16
- package/src/config/model-profiles.ts +31 -15
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +2 -0
- package/src/coordinator-mcp/server.ts +358 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
- package/src/extensibility/extensions/runner.ts +15 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/launch-tmux.ts +53 -1
- package/src/internal-urls/docs-index.generated.ts +11 -10
- package/src/main.ts +203 -67
- package/src/modes/DESIGN.md +8 -5
- package/src/modes/components/footer.ts +10 -18
- package/src/modes/components/session-selector.ts +26 -3
- package/src/modes/components/status-line/gh.ts +23 -2
- package/src/modes/controllers/event-controller.ts +3 -0
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +11 -0
- package/src/modes/controllers/selector-controller.ts +8 -2
- package/src/modes/interactive-mode.ts +7 -1
- package/src/modes/utils/host-status.ts +38 -0
- package/src/runtime-mcp/types.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +60 -3
- package/src/sdk/bus/chat-daemon-cli.ts +42 -0
- package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
- package/src/sdk/bus/chat-daemon-control.ts +16 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
- package/src/sdk/bus/control-plane-frames.ts +51 -0
- package/src/sdk/bus/conversation-store.ts +29 -1
- package/src/sdk/bus/existing-thread-readiness.ts +155 -0
- package/src/sdk/bus/index.ts +70 -0
- package/src/sdk/bus/notification-service.ts +28 -1
- package/src/sdk/bus/rich-render.ts +3 -2
- package/src/sdk/bus/slack-daemon.ts +99 -4
- package/src/sdk/bus/slack-live-provider.ts +32 -2
- package/src/sdk/bus/slack-provider.ts +11 -0
- package/src/sdk/bus/slack-thread-binding.ts +641 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +24 -7
- package/src/sdk/bus/telegram-daemon.ts +1961 -497
- package/src/sdk/bus/telegram-reference.ts +42 -6
- package/src/sdk/bus/topic-registry.ts +674 -181
- package/src/sdk/host/host.ts +180 -1
- package/src/sdk/lifecycle-session.ts +8 -2
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session-activation.ts +127 -0
- package/src/sdk/session.ts +139 -28
- package/src/sdk/startup-capability.ts +10 -1
- package/src/session/agent-session.ts +220 -9
- package/src/session/artifacts.ts +56 -13
- package/src/session/internal/managed-session-storage.ts +206 -7
- package/src/session/session-manager.ts +148 -38
- package/src/setup/provider-presets.json +1 -1
- package/src/task/index.ts +92 -44
- package/src/tools/index.ts +8 -0
- 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
|
}
|