@gajae-code/coding-agent 0.11.3 → 0.11.5
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 +47 -0
- package/dist/types/cli/notify-cli.d.ts +5 -1
- package/dist/types/cli/worktree-cli.d.ts +8 -25
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/worktree.d.ts +7 -32
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
- package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/owner.d.ts +3 -1
- package/dist/types/internal-urls/local-protocol.d.ts +18 -0
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +8 -3
- package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-transcript-format.d.ts +77 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
- package/dist/types/modes/controllers/input-controller.d.ts +8 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
- package/dist/types/plan-mode/approved-plan.d.ts +1 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
- package/dist/types/sdk/broker/broker.d.ts +1 -0
- package/dist/types/sdk/broker/discovery.d.ts +10 -0
- package/dist/types/sdk/broker/ensure.d.ts +24 -1
- package/dist/types/sdk/broker/internal.d.ts +3 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -0
- package/dist/types/sdk/broker/state-version.d.ts +2 -1
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
- package/dist/types/sdk/bus/config-commands.d.ts +8 -0
- package/dist/types/sdk/bus/config.d.ts +22 -0
- package/dist/types/sdk/bus/index.d.ts +162 -2
- package/dist/types/sdk/bus/notification-service.d.ts +71 -8
- package/dist/types/sdk/bus/session-control.d.ts +11 -8
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
- package/dist/types/sdk/session.d.ts +16 -1
- package/dist/types/secrets/index.d.ts +4 -2
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +31 -2
- package/dist/types/session/artifacts.d.ts +30 -8
- package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
- package/dist/types/session/session-manager.d.ts +88 -18
- package/dist/types/session/session-storage.d.ts +27 -0
- package/dist/types/session/streaming-output.d.ts +4 -0
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/tool-discovery/tool-index.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +87 -12
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
- package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/path-utils.d.ts +9 -14
- package/dist/types/tools/render-utils.d.ts +1 -8
- package/dist/types/tools/resource-gc.d.ts +14 -0
- package/dist/types/utils/image-loading.d.ts +23 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/pasted-image-loading.d.ts +39 -0
- package/dist/types/utils/pasted-image-path.d.ts +21 -19
- package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
- package/dist/types/web/scrapers/types.d.ts +1 -5
- package/package.json +25 -9
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-threat-model-check.ts +60 -0
- package/src/cli/notify-cli.ts +40 -18
- package/src/cli/worktree-cli.ts +9 -287
- package/src/cli.ts +10 -9
- package/src/commands/gc.ts +11 -1
- package/src/commands/sdk.ts +10 -4
- package/src/commands/worktree.ts +10 -53
- package/src/config/model-profiles.ts +15 -15
- package/src/config/settings-schema.ts +17 -0
- package/src/daemon/runtime.ts +2 -2
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
- package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
- package/src/defaults/gjc/skills/team/SKILL.md +7 -9
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/index.ts +2 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/plugins/marketplace/manager.ts +94 -10
- package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
- package/src/gjc-runtime/deep-interview-state.ts +127 -3
- package/src/gjc-runtime/gc-render.ts +19 -1
- package/src/gjc-runtime/gc-runtime.ts +68 -3
- package/src/gjc-runtime/launch-tmux.ts +61 -6
- package/src/gjc-runtime/managed-owner-admission.ts +352 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
- package/src/gjc-runtime/state-argv.ts +292 -0
- package/src/gjc-runtime/state-runtime.ts +80 -222
- package/src/gjc-runtime/team-runtime.ts +1 -1
- package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
- package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
- package/src/harness-control-plane/owner.ts +59 -3
- package/src/hindsight/client.ts +65 -2
- package/src/internal-urls/agent-protocol.ts +76 -5
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/internal-urls/local-protocol.ts +416 -18
- package/src/lsp/index.ts +4 -88
- package/src/main.ts +37 -13
- package/src/modes/acp/acp-agent.ts +8 -0
- package/src/modes/components/ansi-display-validator.ts +54 -0
- package/src/modes/components/custom-editor.ts +103 -32
- package/src/modes/components/custom-provider-wizard.ts +26 -1
- package/src/modes/components/notifications-settings-editor.ts +26 -2
- package/src/modes/components/session-observer-overlay.ts +40 -37
- package/src/modes/components/settings-selector.ts +3 -1
- package/src/modes/components/tool-transcript-format.ts +393 -0
- package/src/modes/components/transcript-viewer-overlay.ts +63 -1
- package/src/modes/controllers/goal-mode-controller.ts +20 -1
- package/src/modes/controllers/input-controller.ts +98 -37
- package/src/modes/controllers/plan-mode-controller.ts +2 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +71 -9
- package/src/modes/interactive-mode.ts +14 -2
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
- package/src/plan-mode/approved-plan.ts +3 -1
- package/src/prompts/agents/architect.md +1 -0
- package/src/prompts/agents/critic.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/prompts/tools/lsp.md +3 -3
- package/src/runtime-mcp/manager.ts +16 -1
- package/src/runtime-mcp/oauth-discovery.ts +116 -26
- package/src/sdk/broker/broker.ts +139 -32
- package/src/sdk/broker/discovery.ts +125 -0
- package/src/sdk/broker/ensure.ts +103 -19
- package/src/sdk/broker/internal.ts +10 -0
- package/src/sdk/broker/session-index.ts +309 -38
- package/src/sdk/broker/state-version.ts +5 -2
- package/src/sdk/bus/chat-daemon-cli.ts +72 -10
- package/src/sdk/bus/chat-daemon-control.ts +526 -130
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/config-commands.ts +22 -0
- package/src/sdk/bus/config.ts +37 -0
- package/src/sdk/bus/index.ts +384 -92
- package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
- package/src/sdk/bus/notification-service.ts +284 -62
- package/src/sdk/bus/recent-activity.ts +14 -5
- package/src/sdk/bus/session-control.ts +150 -44
- package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
- package/src/sdk/bus/telegram-daemon-control.ts +294 -76
- package/src/sdk/bus/telegram-daemon.ts +1842 -281
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +155 -27
- package/src/secrets/index.ts +53 -10
- package/src/secrets/obfuscator.ts +21 -18
- package/src/secrets/regex.ts +2 -0
- package/src/session/agent-session.ts +495 -253
- package/src/session/artifacts.ts +188 -23
- package/src/session/context-estimation.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +215 -39
- package/src/session/internal/managed-session-storage.ts +1152 -46
- package/src/session/session-manager.ts +1055 -148
- package/src/session/session-storage.ts +109 -11
- package/src/session/streaming-output.ts +6 -0
- package/src/task/executor.ts +71 -26
- package/src/task/index.ts +63 -20
- package/src/tool-discovery/tool-index.ts +18 -0
- package/src/tools/ask.ts +329 -57
- package/src/tools/bash-allowed-prefixes.ts +79 -29
- package/src/tools/bash-skill-urls.ts +14 -3
- package/src/tools/bash.ts +2 -2
- package/src/tools/browser/launch.ts +3 -1
- package/src/tools/browser/readable.ts +1 -1
- package/src/tools/browser/screenshot-format.ts +34 -0
- package/src/tools/browser/tab-protocol.ts +3 -0
- package/src/tools/browser/tab-supervisor.ts +125 -122
- package/src/tools/browser/tab-worker-entry.ts +33 -14
- package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
- package/src/tools/browser/tab-worker-smoke.ts +87 -0
- package/src/tools/browser/tab-worker.ts +7 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/path-utils.ts +42 -35
- package/src/tools/plan-mode-guard.ts +1 -0
- package/src/tools/read.ts +7 -1
- package/src/tools/render-utils.ts +1 -27
- package/src/tools/resource-gc.ts +123 -34
- package/src/tools/sqlite-reader.ts +3 -2
- package/src/tools/subagent.ts +2 -0
- package/src/utils/image-loading.ts +85 -34
- package/src/utils/image-resize.ts +16 -9
- package/src/utils/pasted-image-loading.ts +514 -0
- package/src/utils/pasted-image-path.ts +223 -98
- package/src/web/scrapers/html-to-markdown.ts +68 -0
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/types.ts +1 -68
- package/src/web/search/providers/openai-compatible.ts +76 -23
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
|
+
import * as os from "node:os";
|
|
12
13
|
import * as path from "node:path";
|
|
14
|
+
import { Process } from "@gajae-code/natives";
|
|
13
15
|
import type { Settings } from "../../config/settings";
|
|
14
16
|
import type {
|
|
15
17
|
BuiltInDaemonController,
|
|
@@ -22,18 +24,31 @@ import type {
|
|
|
22
24
|
} from "../../daemon/control-types";
|
|
23
25
|
import { OWNERSHIP_MISMATCH_MESSAGE, ownershipMismatchRecovery } from "../../daemon/operator-contract";
|
|
24
26
|
import { resolveGjcRuntimeSpawnInfo } from "../../daemon/runtime";
|
|
27
|
+
import { isProcessIncarnation } from "../broker/process-incarnation";
|
|
25
28
|
import { getNotificationConfig, isTelegramConfigured, tokenFingerprint } from "./config";
|
|
29
|
+
import { exactUnlinkNotificationFile, readNotificationEndpointFile } from "./notification-service";
|
|
26
30
|
import {
|
|
31
|
+
confirmTelegramDaemonSpawn,
|
|
32
|
+
type DaemonState,
|
|
27
33
|
daemonPaths,
|
|
34
|
+
hasSafeDaemonStateShape,
|
|
35
|
+
isCurrentCompatibleOwner,
|
|
28
36
|
isFreshLiveOwner,
|
|
37
|
+
isSignalableMatchingOwner,
|
|
29
38
|
readDaemonRoots,
|
|
30
39
|
readDaemonState,
|
|
31
40
|
spawnTelegramDaemonOwner,
|
|
32
41
|
type TelegramDaemonDeps,
|
|
33
42
|
type TelegramDaemonFs,
|
|
43
|
+
type TelegramSpawnOwnerResult,
|
|
34
44
|
} from "./telegram-daemon";
|
|
35
45
|
|
|
36
|
-
const nodeFs: TelegramDaemonFs =
|
|
46
|
+
const nodeFs: TelegramDaemonFs = {
|
|
47
|
+
...(fs.promises as unknown as TelegramDaemonFs),
|
|
48
|
+
readEndpointFile: readNotificationEndpointFile,
|
|
49
|
+
exactUnlink: async (file, identity) =>
|
|
50
|
+
exactUnlinkNotificationFile(file, identity, `.gjc-delete-daemon-transition-${crypto.randomUUID()}.json`),
|
|
51
|
+
};
|
|
37
52
|
const DEFAULT_GRACEFUL_TIMEOUT_MS = 8_000;
|
|
38
53
|
const DEFAULT_KILL_TIMEOUT_MS = 3_000;
|
|
39
54
|
const DEFAULT_WAIT_STEP_MS = 25;
|
|
@@ -92,11 +107,41 @@ export async function clearTelegramControlRequest(
|
|
|
92
107
|
await fsImpl.unlink(file).catch(() => undefined);
|
|
93
108
|
}
|
|
94
109
|
|
|
110
|
+
export interface DaemonProcessReference {
|
|
111
|
+
incarnation: string;
|
|
112
|
+
signalRoot(signal: NodeJS.Signals): void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function defaultProcessReference(pid: number, platform = os.platform()): DaemonProcessReference | undefined {
|
|
116
|
+
// Process.fromPid on Darwin still resolves identity and then signals a numeric
|
|
117
|
+
// PID. A process can exit and its PID be reused in that interval, so daemon
|
|
118
|
+
// control must not use it as privileged signaling authority.
|
|
119
|
+
if (platform === "darwin") return undefined;
|
|
120
|
+
try {
|
|
121
|
+
const processRef = Process.fromPid(pid);
|
|
122
|
+
if (!processRef || !isProcessIncarnation(processRef.incarnation)) return undefined;
|
|
123
|
+
return {
|
|
124
|
+
incarnation: processRef.incarnation,
|
|
125
|
+
signalRoot: signal => {
|
|
126
|
+
const nativeSignal = os.constants.signals[signal];
|
|
127
|
+
if (nativeSignal === undefined) throw new Error(`Unsupported signal: ${signal}`);
|
|
128
|
+
const rootProcess = processRef as typeof processRef & { signalRoot(signal: number): boolean };
|
|
129
|
+
if (!rootProcess.signalRoot(nativeSignal)) throw new Error("Pinned process is already gone");
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
95
137
|
export interface TelegramDaemonControlDeps {
|
|
96
138
|
fs?: TelegramDaemonFs;
|
|
97
139
|
now?: () => number;
|
|
98
140
|
pidAlive?: (pid: number) => boolean;
|
|
99
|
-
|
|
141
|
+
processReference?: (pid: number) => DaemonProcessReference | undefined;
|
|
142
|
+
pidIncarnation?: (pid: number) => string | undefined;
|
|
143
|
+
/** Test seam for platform-specific default stable-process authority. */
|
|
144
|
+
platform?: NodeJS.Platform;
|
|
100
145
|
spawn?: TelegramDaemonDeps["spawn"];
|
|
101
146
|
execPath?: string;
|
|
102
147
|
/**
|
|
@@ -110,8 +155,14 @@ export interface TelegramDaemonControlDeps {
|
|
|
110
155
|
randomId?: () => string;
|
|
111
156
|
sleep?: (ms: number) => Promise<void>;
|
|
112
157
|
waitStepMs?: number;
|
|
158
|
+
/** Bounded startup-readiness timeout; injectable for deterministic controller tests. */
|
|
159
|
+
readinessTimeoutMs?: number;
|
|
113
160
|
}
|
|
114
161
|
|
|
162
|
+
export type TelegramGenerationReloadResult =
|
|
163
|
+
| { outcome: "ready"; operation: DaemonOperationResult }
|
|
164
|
+
| { outcome: "failed"; operation: DaemonOperationResult };
|
|
165
|
+
|
|
115
166
|
function defaultPidAlive(pid: number): boolean {
|
|
116
167
|
try {
|
|
117
168
|
process.kill(pid, 0);
|
|
@@ -121,20 +172,12 @@ function defaultPidAlive(pid: number): boolean {
|
|
|
121
172
|
}
|
|
122
173
|
}
|
|
123
174
|
|
|
124
|
-
function defaultSendSignal(pid: number, signal: NodeJS.Signals): void {
|
|
125
|
-
try {
|
|
126
|
-
process.kill(pid, signal);
|
|
127
|
-
} catch {
|
|
128
|
-
// Best-effort: the process may already be gone.
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
175
|
export class TelegramDaemonController implements BuiltInDaemonController {
|
|
133
176
|
readonly kind = "telegram" as const;
|
|
134
177
|
private readonly fsImpl: TelegramDaemonFs;
|
|
135
|
-
private readonly now: () => number;
|
|
136
178
|
private readonly pidAlive: (pid: number) => boolean;
|
|
137
|
-
private readonly
|
|
179
|
+
private readonly now: () => number;
|
|
180
|
+
private readonly processReference: (pid: number) => DaemonProcessReference | undefined;
|
|
138
181
|
private readonly waitStepMs: number;
|
|
139
182
|
|
|
140
183
|
constructor(
|
|
@@ -144,7 +187,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
144
187
|
this.fsImpl = deps.fs ?? nodeFs;
|
|
145
188
|
this.now = deps.now ?? Date.now;
|
|
146
189
|
this.pidAlive = deps.pidAlive ?? defaultPidAlive;
|
|
147
|
-
this.
|
|
190
|
+
this.processReference = deps.processReference ?? (pid => defaultProcessReference(pid, deps.platform));
|
|
148
191
|
this.waitStepMs = deps.waitStepMs ?? DEFAULT_WAIT_STEP_MS;
|
|
149
192
|
}
|
|
150
193
|
|
|
@@ -167,18 +210,19 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
167
210
|
}
|
|
168
211
|
const state = await readDaemonState(this.settings, this.fsImpl);
|
|
169
212
|
const roots = await readDaemonRoots(this.settings, this.fsImpl);
|
|
170
|
-
const
|
|
171
|
-
state !== undefined
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
213
|
+
const health: DaemonHealth =
|
|
214
|
+
!state || state.stoppedAt !== undefined || !this.pidAlive(state.pid)
|
|
215
|
+
? "stopped"
|
|
216
|
+
: isCurrentCompatibleOwner({
|
|
217
|
+
state,
|
|
218
|
+
now: this.now(),
|
|
219
|
+
tokenFingerprint: tokenFingerprint(cfg.botToken as string),
|
|
220
|
+
chatId: cfg.chatId as string,
|
|
221
|
+
pidAlive: this.pidAlive,
|
|
222
|
+
pidIncarnation: this.deps.pidIncarnation,
|
|
223
|
+
})
|
|
224
|
+
? "running"
|
|
225
|
+
: "stale";
|
|
182
226
|
return {
|
|
183
227
|
kind: this.kind,
|
|
184
228
|
configured: true,
|
|
@@ -198,6 +242,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
198
242
|
fs: this.deps.fs,
|
|
199
243
|
now: this.deps.now,
|
|
200
244
|
pidAlive: this.deps.pidAlive,
|
|
245
|
+
pidIncarnation: this.deps.pidIncarnation,
|
|
201
246
|
pid: this.deps.ownerPid ?? process.ppid,
|
|
202
247
|
spawn: this.deps.spawn,
|
|
203
248
|
execPath: this.deps.execPath,
|
|
@@ -205,6 +250,32 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
205
250
|
};
|
|
206
251
|
}
|
|
207
252
|
|
|
253
|
+
private async spawnAndWait(
|
|
254
|
+
roots: string[],
|
|
255
|
+
token: string,
|
|
256
|
+
chatId: string,
|
|
257
|
+
): Promise<{ spawned: TelegramSpawnOwnerResult; ready: boolean }> {
|
|
258
|
+
const spawned = await spawnTelegramDaemonOwner(
|
|
259
|
+
{ settings: this.settings, roots, tokenFingerprint: token, chatId },
|
|
260
|
+
this.spawnDeps(),
|
|
261
|
+
);
|
|
262
|
+
const ready = await confirmTelegramDaemonSpawn({
|
|
263
|
+
settings: this.settings,
|
|
264
|
+
spawned,
|
|
265
|
+
tokenFingerprint: token,
|
|
266
|
+
chatId,
|
|
267
|
+
pid: this.deps.ownerPid ?? process.ppid,
|
|
268
|
+
fs: this.fsImpl,
|
|
269
|
+
now: this.now,
|
|
270
|
+
pidAlive: this.pidAlive,
|
|
271
|
+
pidIncarnation: this.deps.pidIncarnation,
|
|
272
|
+
sleep: this.deps.sleep,
|
|
273
|
+
waitStepMs: this.waitStepMs,
|
|
274
|
+
timeoutMs: this.deps.readinessTimeoutMs,
|
|
275
|
+
});
|
|
276
|
+
return { spawned, ready };
|
|
277
|
+
}
|
|
278
|
+
|
|
208
279
|
private sleep(ms: number): Promise<void> {
|
|
209
280
|
if (this.deps.sleep) return this.deps.sleep(ms);
|
|
210
281
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
@@ -217,14 +288,67 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
217
288
|
*/
|
|
218
289
|
private async waitForPidDeath(pid: number, timeoutMs: number): Promise<boolean> {
|
|
219
290
|
if (!this.pidAlive(pid)) return true;
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
291
|
+
const timeout = Math.max(timeoutMs, 0);
|
|
292
|
+
const waitStepMs = Math.max(this.waitStepMs, 1);
|
|
293
|
+
const deadline = this.now() + timeout;
|
|
294
|
+
const maxPolls = Math.ceil(timeout / waitStepMs);
|
|
295
|
+
for (let poll = 0; poll < maxPolls && this.now() < deadline; poll++) {
|
|
296
|
+
await this.sleep(waitStepMs);
|
|
223
297
|
if (!this.pidAlive(pid)) return true;
|
|
224
298
|
}
|
|
225
299
|
return !this.pidAlive(pid);
|
|
226
300
|
}
|
|
227
301
|
|
|
302
|
+
private async signalCapturedOwner(
|
|
303
|
+
captured: DaemonState,
|
|
304
|
+
tokenFingerprint: string,
|
|
305
|
+
chatId: string,
|
|
306
|
+
signal: NodeJS.Signals,
|
|
307
|
+
): Promise<"signaled" | "already_gone" | "ownership_changed"> {
|
|
308
|
+
const current = await readDaemonState(this.settings, this.fsImpl);
|
|
309
|
+
// The signal is a privileged action: immediately before sending it, prove the
|
|
310
|
+
// complete state record is still the exact captured owner. A malformed record,
|
|
311
|
+
// configuration mutation, or successor using a reused PID is never signalable.
|
|
312
|
+
if (
|
|
313
|
+
!hasSafeDaemonStateShape(current) ||
|
|
314
|
+
current.ownerId !== captured.ownerId ||
|
|
315
|
+
current.acquisitionId !== captured.acquisitionId ||
|
|
316
|
+
current.pid !== captured.pid ||
|
|
317
|
+
current.generation !== captured.generation ||
|
|
318
|
+
current.incarnation !== captured.incarnation ||
|
|
319
|
+
current.tokenFingerprint !== captured.tokenFingerprint ||
|
|
320
|
+
current.chatId !== captured.chatId ||
|
|
321
|
+
current.tokenFingerprint !== tokenFingerprint ||
|
|
322
|
+
current.chatId !== chatId
|
|
323
|
+
)
|
|
324
|
+
return "ownership_changed";
|
|
325
|
+
// A matching captured owner that exited between the request and recheck has
|
|
326
|
+
// completed the handoff. A live owner with changed or unavailable provenance
|
|
327
|
+
// remains ambiguous and must not be treated as stopped.
|
|
328
|
+
if (!this.pidAlive(captured.pid)) return "already_gone";
|
|
329
|
+
if (
|
|
330
|
+
!isSignalableMatchingOwner({
|
|
331
|
+
state: current,
|
|
332
|
+
tokenFingerprint,
|
|
333
|
+
chatId,
|
|
334
|
+
pidAlive: this.pidAlive,
|
|
335
|
+
pidIncarnation: this.deps.pidIncarnation,
|
|
336
|
+
})
|
|
337
|
+
)
|
|
338
|
+
return "ownership_changed";
|
|
339
|
+
const processRef = this.processReference(captured.pid);
|
|
340
|
+
// A stable native process reference (pidfd/handle/start-time reference) closes
|
|
341
|
+
// the exit-and-reuse window between ordinary provenance checks and signaling.
|
|
342
|
+
// Its identity must still be the exact persisted incarnation before use.
|
|
343
|
+
if (!processRef || processRef.incarnation !== captured.incarnation) return "ownership_changed";
|
|
344
|
+
try {
|
|
345
|
+
processRef.signalRoot(signal);
|
|
346
|
+
} catch {
|
|
347
|
+
return "already_gone";
|
|
348
|
+
}
|
|
349
|
+
return "signaled";
|
|
350
|
+
}
|
|
351
|
+
|
|
228
352
|
private result(
|
|
229
353
|
action: "stop" | "reload",
|
|
230
354
|
ok: boolean,
|
|
@@ -241,6 +365,13 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
241
365
|
return this.stopOrReload("reload", opts);
|
|
242
366
|
}
|
|
243
367
|
|
|
368
|
+
async reloadForGenerationUpgrade(opts: DaemonOperationOptions = {}): Promise<TelegramGenerationReloadResult> {
|
|
369
|
+
const operation = await this.stopOrReload("reload", opts);
|
|
370
|
+
if (!operation.ok) return { outcome: "failed", operation };
|
|
371
|
+
const after = await this.status();
|
|
372
|
+
return after.health === "running" ? { outcome: "ready", operation } : { outcome: "failed", operation };
|
|
373
|
+
}
|
|
374
|
+
|
|
244
375
|
async stop(opts: DaemonOperationOptions = {}): Promise<DaemonOperationResult> {
|
|
245
376
|
return this.stopOrReload("stop", opts);
|
|
246
377
|
}
|
|
@@ -259,19 +390,55 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
259
390
|
const gracefulTimeoutMs = opts.gracefulTimeoutMs ?? DEFAULT_GRACEFUL_TIMEOUT_MS;
|
|
260
391
|
const killTimeoutMs = opts.killTimeoutMs ?? DEFAULT_KILL_TIMEOUT_MS;
|
|
261
392
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
393
|
+
const state = await readDaemonState(this.settings, this.fsImpl);
|
|
394
|
+
const replaceableLiveOwner =
|
|
395
|
+
(action === "reload" &&
|
|
396
|
+
state !== undefined &&
|
|
397
|
+
isFreshLiveOwner({
|
|
398
|
+
state,
|
|
399
|
+
now: this.now(),
|
|
400
|
+
tokenFingerprint: fp,
|
|
401
|
+
chatId,
|
|
402
|
+
pidAlive: this.pidAlive,
|
|
403
|
+
pidIncarnation: this.deps.pidIncarnation,
|
|
404
|
+
}) &&
|
|
405
|
+
isSignalableMatchingOwner({
|
|
406
|
+
state,
|
|
407
|
+
tokenFingerprint: fp,
|
|
408
|
+
chatId,
|
|
409
|
+
pidAlive: this.pidAlive,
|
|
410
|
+
pidIncarnation: this.deps.pidIncarnation,
|
|
411
|
+
})) ||
|
|
412
|
+
// A physically-live matching owner whose heartbeat is stale (hung) may be
|
|
413
|
+
// past-TTL yet still holding the poller. Autostart/generation-upgrade reloads
|
|
414
|
+
// stay conservative and refuse it, but an explicit `reload --force` must be
|
|
415
|
+
// able to signal and replace it rather than deadlock behind the live PID.
|
|
416
|
+
(opts.force === true &&
|
|
417
|
+
isSignalableMatchingOwner({
|
|
418
|
+
state,
|
|
419
|
+
tokenFingerprint: fp,
|
|
420
|
+
chatId,
|
|
421
|
+
pidAlive: this.pidAlive,
|
|
422
|
+
pidIncarnation: this.deps.pidIncarnation,
|
|
423
|
+
}));
|
|
424
|
+
const stoppableLiveOwner =
|
|
425
|
+
action === "stop" &&
|
|
426
|
+
isSignalableMatchingOwner({
|
|
427
|
+
state,
|
|
428
|
+
tokenFingerprint: fp,
|
|
429
|
+
chatId,
|
|
430
|
+
pidAlive: this.pidAlive,
|
|
431
|
+
pidIncarnation: this.deps.pidIncarnation,
|
|
432
|
+
});
|
|
433
|
+
// A stale pre-generation owner may only be moved by reload; manual stop
|
|
434
|
+
// also targets a physically live matching legacy owner, but never spawns.
|
|
435
|
+
if (before.health !== "running" && !replaceableLiveOwner && !stoppableLiveOwner) {
|
|
436
|
+
if (action === "stop")
|
|
265
437
|
return this.result(action, true, "no running telegram daemon", before, before, warnings);
|
|
266
|
-
|
|
267
|
-
const spawnIfStopped = opts.spawnIfStopped ?? true;
|
|
268
|
-
if (!spawnIfStopped) {
|
|
438
|
+
if (!(opts.spawnIfStopped ?? true)) {
|
|
269
439
|
return this.result(action, true, "no running telegram daemon to reload", before, before, warnings);
|
|
270
440
|
}
|
|
271
|
-
const spawned = await
|
|
272
|
-
{ settings: this.settings, roots, tokenFingerprint: fp, chatId },
|
|
273
|
-
this.spawnDeps(),
|
|
274
|
-
);
|
|
441
|
+
const { spawned, ready } = await this.spawnAndWait(roots, fp, chatId);
|
|
275
442
|
warnings.push(...spawned.warnings);
|
|
276
443
|
const after = await this.status();
|
|
277
444
|
if (spawned.result === "blocked") {
|
|
@@ -287,8 +454,10 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
287
454
|
}
|
|
288
455
|
return this.result(
|
|
289
456
|
action,
|
|
290
|
-
spawned.result === "owner_spawned",
|
|
291
|
-
|
|
457
|
+
spawned.result === "owner_spawned" && ready && after.health === "running",
|
|
458
|
+
ready
|
|
459
|
+
? `spawned fresh telegram daemon (${spawned.result})`
|
|
460
|
+
: "telegram daemon did not become ready after spawning",
|
|
292
461
|
before,
|
|
293
462
|
after,
|
|
294
463
|
warnings,
|
|
@@ -296,15 +465,48 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
296
465
|
}
|
|
297
466
|
|
|
298
467
|
// Running owner: capture identity, request cooperative stop, signal, wait.
|
|
299
|
-
|
|
300
|
-
|
|
468
|
+
if (
|
|
469
|
+
!hasSafeDaemonStateShape(state) ||
|
|
470
|
+
!isSignalableMatchingOwner({
|
|
471
|
+
state,
|
|
472
|
+
tokenFingerprint: fp,
|
|
473
|
+
chatId,
|
|
474
|
+
pidAlive: this.pidAlive,
|
|
475
|
+
pidIncarnation: this.deps.pidIncarnation,
|
|
476
|
+
})
|
|
477
|
+
) {
|
|
478
|
+
return this.result(
|
|
479
|
+
action,
|
|
480
|
+
false,
|
|
481
|
+
"telegram daemon ownership changed; refusing to signal",
|
|
482
|
+
before,
|
|
483
|
+
before,
|
|
484
|
+
warnings,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
const capturedOwner = state;
|
|
488
|
+
const oldOwnerId = capturedOwner.ownerId;
|
|
489
|
+
const oldPid = capturedOwner.pid;
|
|
301
490
|
const requestId = this.deps.randomId?.() ?? `${this.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
302
491
|
await writeTelegramControlRequest(
|
|
303
492
|
this.settings,
|
|
304
493
|
{ version: 1, requestId, action, ownerId: oldOwnerId, pid: oldPid, createdAt: this.now() },
|
|
305
494
|
this.fsImpl,
|
|
306
495
|
);
|
|
307
|
-
if (
|
|
496
|
+
if ((await this.signalCapturedOwner(capturedOwner, fp, chatId, "SIGTERM")) === "ownership_changed") {
|
|
497
|
+
await this.clearOwnRequest(requestId);
|
|
498
|
+
return this.result(
|
|
499
|
+
action,
|
|
500
|
+
false,
|
|
501
|
+
"telegram daemon ownership changed; refusing to signal",
|
|
502
|
+
before,
|
|
503
|
+
await this.status(),
|
|
504
|
+
warnings,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
// "signaled" or "already_gone" (the owner already exited cooperatively): confirm
|
|
508
|
+
// death through the normal path — waitForPidDeath returns immediately for a
|
|
509
|
+
// dead pid, so stop succeeds and reload proceeds to spawn the replacement.
|
|
308
510
|
|
|
309
511
|
let dead = await this.waitForPidDeath(oldPid, gracefulTimeoutMs);
|
|
310
512
|
if (!dead) {
|
|
@@ -319,33 +521,36 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
319
521
|
tokenFingerprint: fp,
|
|
320
522
|
chatId,
|
|
321
523
|
pidAlive: this.pidAlive,
|
|
524
|
+
pidIncarnation: this.deps.pidIncarnation,
|
|
322
525
|
});
|
|
323
526
|
if (changedToLiveOwner) {
|
|
324
|
-
|
|
325
|
-
// kill it or spawn another; attach to the running daemon.
|
|
326
|
-
await this.clearOwnRequest(requestId, oldOwnerId);
|
|
527
|
+
await this.clearOwnRequest(requestId);
|
|
327
528
|
const after = await this.status();
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
);
|
|
529
|
+
if (!dead) {
|
|
530
|
+
return this.result(
|
|
531
|
+
action,
|
|
532
|
+
false,
|
|
533
|
+
"ownership changed before the captured daemon exited; refusing to signal or spawn",
|
|
534
|
+
before,
|
|
535
|
+
after,
|
|
536
|
+
warnings,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
339
539
|
}
|
|
340
540
|
// No live replacement. Escalate to SIGKILL only with --force and only when
|
|
341
541
|
// the captured owner/pid still matches, so we never kill a different owner.
|
|
342
542
|
const stillSameOwner = current !== undefined && current.ownerId === oldOwnerId && current.pid === oldPid;
|
|
343
|
-
if (opts.force && stillSameOwner
|
|
344
|
-
this.
|
|
345
|
-
|
|
543
|
+
if (opts.force && stillSameOwner) {
|
|
544
|
+
const killResult = await this.signalCapturedOwner(capturedOwner, fp, chatId, "SIGKILL");
|
|
545
|
+
// Kill only a still-live matching owner; an owner that exited between the
|
|
546
|
+
// graceful timeout and this recheck ("already_gone") is confirmed dead by
|
|
547
|
+
// waitForPidDeath, while a real ownership change stays fenced.
|
|
548
|
+
if (killResult === "signaled" || killResult === "already_gone") {
|
|
549
|
+
dead = await this.waitForPidDeath(oldPid, killTimeoutMs);
|
|
550
|
+
}
|
|
346
551
|
}
|
|
347
552
|
if (!dead) {
|
|
348
|
-
await this.clearOwnRequest(requestId
|
|
553
|
+
await this.clearOwnRequest(requestId);
|
|
349
554
|
const after = await this.status();
|
|
350
555
|
const message = opts.force
|
|
351
556
|
? "old daemon did not exit after SIGKILL; refusing to spawn to avoid a Telegram 409 conflict"
|
|
@@ -355,22 +560,38 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
355
560
|
}
|
|
356
561
|
|
|
357
562
|
// Old pid is dead: safe to clear our request and proceed.
|
|
358
|
-
await this.clearOwnRequest(requestId
|
|
563
|
+
await this.clearOwnRequest(requestId);
|
|
359
564
|
|
|
360
565
|
if (action === "stop") {
|
|
361
566
|
const after = await this.status();
|
|
362
567
|
return this.result(action, true, "stopped telegram daemon", before, after, warnings);
|
|
363
568
|
}
|
|
364
569
|
|
|
365
|
-
const spawned = await
|
|
366
|
-
{ settings: this.settings, roots, tokenFingerprint: fp, chatId },
|
|
367
|
-
this.spawnDeps(),
|
|
368
|
-
);
|
|
570
|
+
const { spawned, ready } = await this.spawnAndWait(roots, fp, chatId);
|
|
369
571
|
warnings.push(...spawned.warnings);
|
|
370
572
|
const after = await this.status();
|
|
371
573
|
if (spawned.result === "attached") {
|
|
372
|
-
|
|
373
|
-
|
|
574
|
+
const attachedState = await readDaemonState(this.settings, this.fsImpl);
|
|
575
|
+
if (
|
|
576
|
+
!isCurrentCompatibleOwner({
|
|
577
|
+
state: attachedState,
|
|
578
|
+
now: this.now(),
|
|
579
|
+
tokenFingerprint: fp,
|
|
580
|
+
chatId,
|
|
581
|
+
pidAlive: this.pidAlive,
|
|
582
|
+
pidIncarnation: this.deps.pidIncarnation,
|
|
583
|
+
})
|
|
584
|
+
) {
|
|
585
|
+
return this.result(
|
|
586
|
+
action,
|
|
587
|
+
false,
|
|
588
|
+
"ownership moved to a non-ready or incompatible daemon; reload required",
|
|
589
|
+
before,
|
|
590
|
+
after,
|
|
591
|
+
warnings,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
warnings.push("a ready current-compatible owner already exists; attached instead of spawning");
|
|
374
595
|
} else if (after.ownerId && after.ownerId === oldOwnerId) {
|
|
375
596
|
warnings.push("owner id unchanged after reload");
|
|
376
597
|
}
|
|
@@ -387,20 +608,17 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
387
608
|
}
|
|
388
609
|
return this.result(
|
|
389
610
|
action,
|
|
390
|
-
spawned.result
|
|
391
|
-
`reloaded telegram daemon (${spawned.result})
|
|
611
|
+
(spawned.result === "owner_spawned" || spawned.result === "attached") && ready && after.health === "running",
|
|
612
|
+
ready ? `reloaded telegram daemon (${spawned.result})` : "telegram daemon did not become ready after spawning",
|
|
392
613
|
before,
|
|
393
614
|
after,
|
|
394
615
|
warnings,
|
|
395
616
|
);
|
|
396
617
|
}
|
|
397
618
|
|
|
398
|
-
/** Clear our
|
|
399
|
-
private async clearOwnRequest(requestId: string
|
|
619
|
+
/** Clear only our exact control request; a successor request must survive. */
|
|
620
|
+
private async clearOwnRequest(requestId: string): Promise<void> {
|
|
400
621
|
const current = await readTelegramControlRequest(this.settings, this.fsImpl);
|
|
401
|
-
if (
|
|
402
|
-
if (current.requestId === requestId || current.ownerId === oldOwnerId) {
|
|
403
|
-
await clearTelegramControlRequest(this.settings, current.requestId, this.fsImpl);
|
|
404
|
-
}
|
|
622
|
+
if (current?.requestId === requestId) await clearTelegramControlRequest(this.settings, requestId, this.fsImpl);
|
|
405
623
|
}
|
|
406
624
|
}
|