@gajae-code/coding-agent 0.6.5 → 0.7.1
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/async/job-manager.d.ts +3 -1
- package/dist/types/cli/daemon-cli.d.ts +25 -0
- package/dist/types/cli/notify-cli.d.ts +23 -0
- package/dist/types/cli/setup-cli.d.ts +20 -1
- package/dist/types/commands/daemon.d.ts +41 -0
- package/dist/types/commands/notify.d.ts +41 -0
- package/dist/types/config/model-profile-activation.d.ts +12 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +38 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/daemon/builtin.d.ts +20 -0
- package/dist/types/daemon/control-types.d.ts +57 -0
- package/dist/types/daemon/runtime.d.ts +25 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +2 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +14 -0
- package/dist/types/modes/components/oauth-selector.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/notifications/config-commands.d.ts +26 -0
- package/dist/types/notifications/config.d.ts +61 -0
- package/dist/types/notifications/helpers.d.ts +55 -0
- package/dist/types/notifications/html-format.d.ts +62 -0
- package/dist/types/notifications/index.d.ts +28 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
- package/dist/types/notifications/telegram-cli.d.ts +19 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
- package/dist/types/notifications/telegram-daemon.d.ts +276 -0
- package/dist/types/notifications/telegram-reference.d.ts +111 -0
- package/dist/types/notifications/threaded-inbound.d.ts +58 -0
- package/dist/types/notifications/threaded-render.d.ts +66 -0
- package/dist/types/notifications/topic-registry.d.ts +67 -0
- package/dist/types/rlm/index.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +39 -2
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/setup/credential-auto-import.d.ts +63 -0
- package/dist/types/setup/credential-import.d.ts +3 -0
- package/dist/types/setup/host-plugin-setup.d.ts +39 -0
- package/dist/types/tools/ask-answer-registry.d.ts +13 -0
- package/dist/types/tools/index.d.ts +18 -0
- package/dist/types/tools/subagent.d.ts +3 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +3 -0
- package/src/async/job-manager.ts +5 -1
- package/src/cli/daemon-cli.ts +122 -0
- package/src/cli/notify-cli.ts +274 -0
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +3 -3
- package/src/commands/daemon.ts +47 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- package/src/config/model-profile-activation.ts +74 -5
- package/src/config/model-profiles.ts +7 -4
- package/src/config/model-registry.ts +6 -3
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +29 -0
- package/src/coordinator/contract.ts +3 -0
- package/src/coordinator-mcp/server.ts +270 -1
- package/src/daemon/builtin.ts +46 -0
- package/src/daemon/control-types.ts +65 -0
- package/src/daemon/runtime.ts +51 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
- package/src/edit/modes/replace.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +12 -4
- package/src/gjc-runtime/launch-tmux.ts +10 -2
- package/src/gjc-runtime/state-runtime.ts +18 -4
- package/src/gjc-runtime/state-writer.ts +8 -8
- package/src/gjc-runtime/tmux-common.ts +8 -0
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/gjc-runtime/ultragoal-guard.ts +57 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +105 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
- package/src/gjc-runtime/workflow-manifest.ts +11 -1
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/hashline/hash.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +9 -7
- package/src/main.ts +30 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- package/src/modes/components/hook-editor.ts +7 -2
- package/src/modes/components/oauth-selector.ts +19 -0
- package/src/modes/controllers/event-controller.ts +20 -0
- package/src/modes/controllers/selector-controller.ts +80 -17
- package/src/modes/interactive-mode.ts +6 -2
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/event-contract.ts +1 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
- package/src/modes/shared/agent-wire/event-observation.ts +16 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/utils/ui-helpers.ts +23 -0
- package/src/notifications/config-commands.ts +50 -0
- package/src/notifications/config.ts +107 -0
- package/src/notifications/helpers.ts +135 -0
- package/src/notifications/html-format.ts +389 -0
- package/src/notifications/index.ts +700 -0
- package/src/notifications/rate-limit-pool.ts +179 -0
- package/src/notifications/telegram-cli.ts +194 -0
- package/src/notifications/telegram-daemon-cli.ts +74 -0
- package/src/notifications/telegram-daemon-control.ts +370 -0
- package/src/notifications/telegram-daemon.ts +1370 -0
- package/src/notifications/telegram-reference.ts +335 -0
- package/src/notifications/threaded-inbound.ts +80 -0
- package/src/notifications/threaded-render.ts +155 -0
- package/src/notifications/topic-registry.ts +133 -0
- package/src/rlm/index.ts +19 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +113 -3
- package/src/session/auth-storage.ts +3 -0
- package/src/session/session-dump-format.ts +43 -2
- package/src/session/session-manager.ts +39 -5
- package/src/setup/credential-auto-import.ts +258 -0
- package/src/setup/credential-import.ts +17 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
- package/src/setup/host-plugin-setup.ts +142 -0
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/executor.ts +5 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +77 -6
- package/src/tools/image-gen.ts +5 -8
- package/src/tools/index.ts +19 -0
- package/src/tools/inspect-image.ts +16 -11
- package/src/tools/subagent-render.ts +7 -0
- package/src/tools/subagent.ts +38 -7
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram daemon controller + owner-scoped control-request helpers.
|
|
3
|
+
*
|
|
4
|
+
* Reload is a hybrid: an owner-scoped control-request file records auditable
|
|
5
|
+
* intent, SIGTERM is the wakeup that aborts the in-flight long poll, and a
|
|
6
|
+
* fresh daemon is spawned only after the old pid is dead / has exited. This
|
|
7
|
+
* keeps the single-poller invariant (no Telegram getUpdates 409 overlap) and
|
|
8
|
+
* never steals a still-live owner.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import type { Settings } from "../config/settings";
|
|
14
|
+
import type {
|
|
15
|
+
BuiltInDaemonController,
|
|
16
|
+
DaemonHealth,
|
|
17
|
+
DaemonOperationOptions,
|
|
18
|
+
DaemonOperationResult,
|
|
19
|
+
DaemonRuntimeInfo,
|
|
20
|
+
DaemonStatus,
|
|
21
|
+
} from "../daemon/control-types";
|
|
22
|
+
import { resolveGjcRuntimeSpawnInfo } from "../daemon/runtime";
|
|
23
|
+
import { getNotificationConfig, isGloballyConfigured, tokenFingerprint } from "./config";
|
|
24
|
+
import {
|
|
25
|
+
daemonPaths,
|
|
26
|
+
isFreshLiveOwner,
|
|
27
|
+
readDaemonRoots,
|
|
28
|
+
readDaemonState,
|
|
29
|
+
spawnTelegramDaemonOwner,
|
|
30
|
+
type TelegramDaemonDeps,
|
|
31
|
+
type TelegramDaemonFs,
|
|
32
|
+
} from "./telegram-daemon";
|
|
33
|
+
|
|
34
|
+
const nodeFs: TelegramDaemonFs = fs.promises as unknown as TelegramDaemonFs;
|
|
35
|
+
const DEFAULT_GRACEFUL_TIMEOUT_MS = 8_000;
|
|
36
|
+
const DEFAULT_KILL_TIMEOUT_MS = 3_000;
|
|
37
|
+
const DEFAULT_WAIT_STEP_MS = 25;
|
|
38
|
+
|
|
39
|
+
export interface TelegramDaemonControlRequest {
|
|
40
|
+
version: 1;
|
|
41
|
+
requestId: string;
|
|
42
|
+
action: "reload" | "stop";
|
|
43
|
+
ownerId: string;
|
|
44
|
+
pid: number;
|
|
45
|
+
createdAt: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function telegramControlRequestPath(agentDir: string): string {
|
|
49
|
+
return path.join(daemonPaths(agentDir).dir, "telegram-daemon.control.json");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function readTelegramControlRequest(
|
|
53
|
+
settings: Settings,
|
|
54
|
+
fsImpl: TelegramDaemonFs = nodeFs,
|
|
55
|
+
): Promise<TelegramDaemonControlRequest | undefined> {
|
|
56
|
+
const file = telegramControlRequestPath(settings.getAgentDir());
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(await fsImpl.readFile(file, "utf8")) as TelegramDaemonControlRequest;
|
|
59
|
+
return parsed?.version === 1 ? parsed : undefined;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function writeTelegramControlRequest(
|
|
67
|
+
settings: Settings,
|
|
68
|
+
request: TelegramDaemonControlRequest,
|
|
69
|
+
fsImpl: TelegramDaemonFs = nodeFs,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
const dir = daemonPaths(settings.getAgentDir()).dir;
|
|
72
|
+
await fsImpl.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
73
|
+
const file = telegramControlRequestPath(settings.getAgentDir());
|
|
74
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
75
|
+
await fsImpl.writeFile(tmp, `${JSON.stringify(request, null, 2)}\n`, { mode: 0o600 });
|
|
76
|
+
await fsImpl.chmod(tmp, 0o600).catch(() => undefined);
|
|
77
|
+
await fsImpl.rename(tmp, file);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function clearTelegramControlRequest(
|
|
81
|
+
settings: Settings,
|
|
82
|
+
requestId?: string,
|
|
83
|
+
fsImpl: TelegramDaemonFs = nodeFs,
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
const file = telegramControlRequestPath(settings.getAgentDir());
|
|
86
|
+
if (requestId) {
|
|
87
|
+
const current = await readTelegramControlRequest(settings, fsImpl);
|
|
88
|
+
if (current && current.requestId !== requestId) return;
|
|
89
|
+
}
|
|
90
|
+
await fsImpl.unlink(file).catch(() => undefined);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface TelegramDaemonControlDeps {
|
|
94
|
+
fs?: TelegramDaemonFs;
|
|
95
|
+
now?: () => number;
|
|
96
|
+
pidAlive?: (pid: number) => boolean;
|
|
97
|
+
sendSignal?: (pid: number, signal: NodeJS.Signals) => void;
|
|
98
|
+
spawn?: TelegramDaemonDeps["spawn"];
|
|
99
|
+
execPath?: string;
|
|
100
|
+
randomId?: () => string;
|
|
101
|
+
sleep?: (ms: number) => Promise<void>;
|
|
102
|
+
waitStepMs?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function defaultPidAlive(pid: number): boolean {
|
|
106
|
+
try {
|
|
107
|
+
process.kill(pid, 0);
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function defaultSendSignal(pid: number, signal: NodeJS.Signals): void {
|
|
115
|
+
try {
|
|
116
|
+
process.kill(pid, signal);
|
|
117
|
+
} catch {
|
|
118
|
+
// Best-effort: the process may already be gone.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class TelegramDaemonController implements BuiltInDaemonController {
|
|
123
|
+
readonly kind = "telegram" as const;
|
|
124
|
+
private readonly fsImpl: TelegramDaemonFs;
|
|
125
|
+
private readonly now: () => number;
|
|
126
|
+
private readonly pidAlive: (pid: number) => boolean;
|
|
127
|
+
private readonly sendSignal: (pid: number, signal: NodeJS.Signals) => void;
|
|
128
|
+
private readonly waitStepMs: number;
|
|
129
|
+
|
|
130
|
+
constructor(
|
|
131
|
+
private readonly settings: Settings,
|
|
132
|
+
private readonly deps: TelegramDaemonControlDeps = {},
|
|
133
|
+
) {
|
|
134
|
+
this.fsImpl = deps.fs ?? nodeFs;
|
|
135
|
+
this.now = deps.now ?? Date.now;
|
|
136
|
+
this.pidAlive = deps.pidAlive ?? defaultPidAlive;
|
|
137
|
+
this.sendSignal = deps.sendSignal ?? defaultSendSignal;
|
|
138
|
+
this.waitStepMs = deps.waitStepMs ?? DEFAULT_WAIT_STEP_MS;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private runtimeInfo(): DaemonRuntimeInfo {
|
|
142
|
+
const rt = resolveGjcRuntimeSpawnInfo(this.deps.execPath ?? process.execPath);
|
|
143
|
+
return {
|
|
144
|
+
mode: rt.mode,
|
|
145
|
+
execPath: rt.execPath,
|
|
146
|
+
reloadPicksUpSourceEdits: rt.reloadPicksUpSourceEdits,
|
|
147
|
+
warning: rt.warning,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async status(): Promise<DaemonStatus> {
|
|
152
|
+
const runtime = this.runtimeInfo();
|
|
153
|
+
const cfg = getNotificationConfig(this.settings);
|
|
154
|
+
const configured = isGloballyConfigured(cfg) && Boolean(cfg.botToken) && Boolean(cfg.chatId);
|
|
155
|
+
if (!configured) {
|
|
156
|
+
return { kind: this.kind, configured: false, health: "not_configured", runtime };
|
|
157
|
+
}
|
|
158
|
+
const state = await readDaemonState(this.settings, this.fsImpl);
|
|
159
|
+
const roots = await readDaemonRoots(this.settings, this.fsImpl);
|
|
160
|
+
const live =
|
|
161
|
+
state !== undefined &&
|
|
162
|
+
isFreshLiveOwner({
|
|
163
|
+
state,
|
|
164
|
+
now: this.now(),
|
|
165
|
+
tokenFingerprint: tokenFingerprint(cfg.botToken as string),
|
|
166
|
+
chatId: cfg.chatId as string,
|
|
167
|
+
pidAlive: this.pidAlive,
|
|
168
|
+
});
|
|
169
|
+
let health: DaemonHealth = "stopped";
|
|
170
|
+
if (live) health = "running";
|
|
171
|
+
else if (state && state.stoppedAt === undefined) health = "stale";
|
|
172
|
+
return {
|
|
173
|
+
kind: this.kind,
|
|
174
|
+
configured: true,
|
|
175
|
+
health,
|
|
176
|
+
pid: state?.pid,
|
|
177
|
+
ownerId: state?.ownerId,
|
|
178
|
+
startedAt: state?.startedAt,
|
|
179
|
+
heartbeatAt: state?.heartbeatAt,
|
|
180
|
+
roots,
|
|
181
|
+
rootCount: roots.length,
|
|
182
|
+
runtime,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private spawnDeps(): TelegramDaemonDeps {
|
|
187
|
+
return {
|
|
188
|
+
fs: this.deps.fs,
|
|
189
|
+
now: this.deps.now,
|
|
190
|
+
pidAlive: this.deps.pidAlive,
|
|
191
|
+
spawn: this.deps.spawn,
|
|
192
|
+
execPath: this.deps.execPath,
|
|
193
|
+
randomId: this.deps.randomId,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private sleep(ms: number): Promise<void> {
|
|
198
|
+
if (this.deps.sleep) return this.deps.sleep(ms);
|
|
199
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Wait until the captured pid is dead. Ownership-file movement is NOT treated
|
|
204
|
+
* as quiescence here: only actual process death proves the old poller stopped,
|
|
205
|
+
* which is what the no-409 invariant requires before spawning a fresh poller.
|
|
206
|
+
*/
|
|
207
|
+
private async waitForPidDeath(pid: number, timeoutMs: number): Promise<boolean> {
|
|
208
|
+
if (!this.pidAlive(pid)) return true;
|
|
209
|
+
const deadline = this.now() + timeoutMs;
|
|
210
|
+
while (this.now() < deadline) {
|
|
211
|
+
await this.sleep(this.waitStepMs);
|
|
212
|
+
if (!this.pidAlive(pid)) return true;
|
|
213
|
+
}
|
|
214
|
+
return !this.pidAlive(pid);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private result(
|
|
218
|
+
action: "stop" | "reload",
|
|
219
|
+
ok: boolean,
|
|
220
|
+
message: string,
|
|
221
|
+
before: DaemonStatus | undefined,
|
|
222
|
+
after: DaemonStatus | undefined,
|
|
223
|
+
warnings: string[],
|
|
224
|
+
): DaemonOperationResult {
|
|
225
|
+
return { kind: this.kind, action, ok, before, after, message, warnings };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async reload(opts: DaemonOperationOptions = {}): Promise<DaemonOperationResult> {
|
|
229
|
+
return this.stopOrReload("reload", opts);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async stop(opts: DaemonOperationOptions = {}): Promise<DaemonOperationResult> {
|
|
233
|
+
return this.stopOrReload("stop", opts);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private async stopOrReload(action: "stop" | "reload", opts: DaemonOperationOptions): Promise<DaemonOperationResult> {
|
|
237
|
+
const before = await this.status();
|
|
238
|
+
const warnings: string[] = [];
|
|
239
|
+
if (before.runtime.warning) warnings.push(before.runtime.warning);
|
|
240
|
+
if (!before.configured) {
|
|
241
|
+
return this.result(action, false, "telegram notifications are not configured", before, before, warnings);
|
|
242
|
+
}
|
|
243
|
+
const cfg = getNotificationConfig(this.settings);
|
|
244
|
+
const fp = tokenFingerprint(cfg.botToken as string);
|
|
245
|
+
const chatId = cfg.chatId as string;
|
|
246
|
+
const roots = before.roots ?? (await readDaemonRoots(this.settings, this.fsImpl));
|
|
247
|
+
const gracefulTimeoutMs = opts.gracefulTimeoutMs ?? DEFAULT_GRACEFUL_TIMEOUT_MS;
|
|
248
|
+
const killTimeoutMs = opts.killTimeoutMs ?? DEFAULT_KILL_TIMEOUT_MS;
|
|
249
|
+
|
|
250
|
+
// No running owner.
|
|
251
|
+
if (before.health !== "running") {
|
|
252
|
+
if (action === "stop") {
|
|
253
|
+
return this.result(action, true, "no running telegram daemon", before, before, warnings);
|
|
254
|
+
}
|
|
255
|
+
const spawnIfStopped = opts.spawnIfStopped ?? true;
|
|
256
|
+
if (!spawnIfStopped) {
|
|
257
|
+
return this.result(action, true, "no running telegram daemon to reload", before, before, warnings);
|
|
258
|
+
}
|
|
259
|
+
const spawned = await spawnTelegramDaemonOwner(
|
|
260
|
+
{ settings: this.settings, roots, tokenFingerprint: fp, chatId },
|
|
261
|
+
this.spawnDeps(),
|
|
262
|
+
);
|
|
263
|
+
const after = await this.status();
|
|
264
|
+
return this.result(
|
|
265
|
+
action,
|
|
266
|
+
spawned.result === "owner_spawned",
|
|
267
|
+
`spawned fresh telegram daemon (${spawned.result})`,
|
|
268
|
+
before,
|
|
269
|
+
after,
|
|
270
|
+
warnings,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Running owner: capture identity, request cooperative stop, signal, wait.
|
|
275
|
+
const oldOwnerId = before.ownerId as string;
|
|
276
|
+
const oldPid = before.pid as number;
|
|
277
|
+
const requestId = this.deps.randomId?.() ?? `${this.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
278
|
+
await writeTelegramControlRequest(
|
|
279
|
+
this.settings,
|
|
280
|
+
{ version: 1, requestId, action, ownerId: oldOwnerId, pid: oldPid, createdAt: this.now() },
|
|
281
|
+
this.fsImpl,
|
|
282
|
+
);
|
|
283
|
+
if (this.pidAlive(oldPid)) this.sendSignal(oldPid, "SIGTERM");
|
|
284
|
+
|
|
285
|
+
let dead = await this.waitForPidDeath(oldPid, gracefulTimeoutMs);
|
|
286
|
+
if (!dead) {
|
|
287
|
+
// Old pid still alive after the cooperative SIGTERM. Inspect current ownership.
|
|
288
|
+
const current = await readDaemonState(this.settings, this.fsImpl);
|
|
289
|
+
const changedToLiveOwner =
|
|
290
|
+
current !== undefined &&
|
|
291
|
+
current.ownerId !== oldOwnerId &&
|
|
292
|
+
isFreshLiveOwner({
|
|
293
|
+
state: current,
|
|
294
|
+
now: this.now(),
|
|
295
|
+
tokenFingerprint: fp,
|
|
296
|
+
chatId,
|
|
297
|
+
pidAlive: this.pidAlive,
|
|
298
|
+
});
|
|
299
|
+
if (changedToLiveOwner) {
|
|
300
|
+
// A different, fresh-live owner already supersedes the old one. Do not
|
|
301
|
+
// kill it or spawn another; attach to the running daemon.
|
|
302
|
+
await this.clearOwnRequest(requestId, oldOwnerId);
|
|
303
|
+
const after = await this.status();
|
|
304
|
+
warnings.push("ownership changed to a live owner; attached without spawning");
|
|
305
|
+
return this.result(
|
|
306
|
+
action,
|
|
307
|
+
true,
|
|
308
|
+
action === "reload"
|
|
309
|
+
? "a live owner already exists; attached instead of reloading"
|
|
310
|
+
: "another live owner already exists",
|
|
311
|
+
before,
|
|
312
|
+
after,
|
|
313
|
+
warnings,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
// No live replacement. Escalate to SIGKILL only with --force and only when
|
|
317
|
+
// the captured owner/pid still matches, so we never kill a different owner.
|
|
318
|
+
const stillSameOwner = current !== undefined && current.ownerId === oldOwnerId && current.pid === oldPid;
|
|
319
|
+
if (opts.force && stillSameOwner && this.pidAlive(oldPid)) {
|
|
320
|
+
this.sendSignal(oldPid, "SIGKILL");
|
|
321
|
+
dead = await this.waitForPidDeath(oldPid, killTimeoutMs);
|
|
322
|
+
}
|
|
323
|
+
if (!dead) {
|
|
324
|
+
await this.clearOwnRequest(requestId, oldOwnerId);
|
|
325
|
+
const after = await this.status();
|
|
326
|
+
const message = opts.force
|
|
327
|
+
? "old daemon did not exit after SIGKILL; refusing to spawn to avoid a Telegram 409 conflict"
|
|
328
|
+
: "old daemon did not exit within the graceful timeout; rerun with --force to hard-kill";
|
|
329
|
+
return this.result(action, false, message, before, after, warnings);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Old pid is dead: safe to clear our request and proceed.
|
|
334
|
+
await this.clearOwnRequest(requestId, oldOwnerId);
|
|
335
|
+
|
|
336
|
+
if (action === "stop") {
|
|
337
|
+
const after = await this.status();
|
|
338
|
+
return this.result(action, true, "stopped telegram daemon", before, after, warnings);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const spawned = await spawnTelegramDaemonOwner(
|
|
342
|
+
{ settings: this.settings, roots, tokenFingerprint: fp, chatId },
|
|
343
|
+
this.spawnDeps(),
|
|
344
|
+
);
|
|
345
|
+
const after = await this.status();
|
|
346
|
+
if (spawned.result === "attached") {
|
|
347
|
+
// A live owner already exists; attaching to it is a valid running end-state.
|
|
348
|
+
warnings.push("a live owner already exists; attached instead of spawning");
|
|
349
|
+
} else if (after.ownerId && after.ownerId === oldOwnerId) {
|
|
350
|
+
warnings.push("owner id unchanged after reload");
|
|
351
|
+
}
|
|
352
|
+
return this.result(
|
|
353
|
+
action,
|
|
354
|
+
spawned.result !== "disabled",
|
|
355
|
+
`reloaded telegram daemon (${spawned.result})`,
|
|
356
|
+
before,
|
|
357
|
+
after,
|
|
358
|
+
warnings,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Clear our own control request unless a newer owner-scoped request replaced it. */
|
|
363
|
+
private async clearOwnRequest(requestId: string, oldOwnerId: string): Promise<void> {
|
|
364
|
+
const current = await readTelegramControlRequest(this.settings, this.fsImpl);
|
|
365
|
+
if (!current) return;
|
|
366
|
+
if (current.requestId === requestId || current.ownerId === oldOwnerId) {
|
|
367
|
+
await clearTelegramControlRequest(this.settings, current.requestId, this.fsImpl);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|