@gajae-code/coding-agent 0.11.6 → 0.11.8
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 +92 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +34 -4
- package/dist/types/config/model-profile-activation.d.ts +4 -3
- package/dist/types/config/model-profile-contract.d.ts +38 -0
- package/dist/types/config/settings-schema.d.ts +70 -7
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +6 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +43 -5
- package/dist/types/main.d.ts +32 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +4 -2
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +7 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +26 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +18 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +189 -4
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +1 -1
- package/dist/types/sdk/host/query/handlers.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/startup-capability.d.ts +3 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +30 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +32 -9
- package/dist/types/session/internal/managed-session-storage.d.ts +27 -10
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-manager.d.ts +25 -4
- package/dist/types/session/session-storage.d.ts +18 -0
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +94 -0
- package/dist/types/tools/ask.d.ts +454 -174
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +63 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/args.ts +2 -92
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +52 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +44 -11
- package/src/config/keybindings.ts +158 -25
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-profiles.ts +18 -0
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +73 -7
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/py/executor.ts +30 -11
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +75 -3
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
- package/src/gjc-runtime/tmux-sessions.ts +45 -6
- package/src/gjc-runtime/ultragoal-guard.ts +93 -15
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +456 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +12 -10
- package/src/internal-urls/local-protocol.ts +64 -21
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +111 -37
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +73 -11
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/tool-status-header.ts +25 -4
- package/src/modes/components/welcome.ts +22 -18
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +97 -54
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/input-controller.ts +38 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +149 -63
- package/src/modes/interactive-mode.ts +57 -34
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/prompts/agents/architect.md +0 -1
- package/src/prompts/agents/critic.md +0 -1
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/manager.ts +51 -53
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/transports/http.ts +63 -22
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +19 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +9 -1
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/index.ts +244 -70
- package/src/sdk/bus/lifecycle-commands.ts +63 -10
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +68 -20
- package/src/sdk/bus/prompt-reconciliation.ts +239 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/telegram-daemon-contract.ts +18 -3
- package/src/sdk/bus/telegram-daemon-control.ts +147 -35
- package/src/sdk/bus/telegram-daemon.ts +2463 -450
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +1 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +2 -6
- package/src/sdk/host/query/handlers.ts +70 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +58 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +31 -5
- package/src/sdk/startup-capability.ts +9 -1
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +292 -154
- package/src/session/blob-store.ts +25 -7
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +1163 -137
- package/src/session/internal/managed-session-storage.ts +397 -234
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +620 -403
- package/src/session/session-storage.ts +128 -11
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/task/gjc-command.ts +2 -2
- package/src/task/index.ts +121 -15
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +34 -3
- package/src/tools/ask.ts +199 -89
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +476 -23
- package/src/tools/index.ts +4 -0
- package/src/tools/read.ts +2 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +140 -27
- package/src/web/scrapers/docs-rs.ts +30 -5
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -11,7 +11,7 @@ import * as fs from "node:fs/promises";
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import { verifyOwnerOnlyPathSecurity } from "@gajae-code/natives";
|
|
13
13
|
import { getAgentDir, getSessionsDir } from "@gajae-code/utils";
|
|
14
|
-
import { FileSessionStorage } from "../../session/session-storage";
|
|
14
|
+
import { FileSessionStorage, type SessionStorageSnapshot } from "../../session/session-storage";
|
|
15
15
|
import {
|
|
16
16
|
type LogicalSessionCandidate,
|
|
17
17
|
listManagedSessionCandidates,
|
|
@@ -58,25 +58,52 @@ export interface RecentActivityDeps {
|
|
|
58
58
|
readInitialLines?: (file: string, maxLines: number) => string[];
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
class ManagedCandidateChangedError extends Error {}
|
|
62
|
+
class ManagedCandidateUnavailableError extends Error {}
|
|
63
|
+
|
|
64
|
+
const CHANGED_CANDIDATE_WARNING = "Ignored managed session candidate that changed during inspection.";
|
|
65
|
+
|
|
66
|
+
function isAppendExtension(bytes: Uint8Array, candidate: LogicalSessionCandidate): boolean {
|
|
67
|
+
return (
|
|
68
|
+
bytes.byteLength >= candidate.identity.size &&
|
|
69
|
+
createHash("sha256").update(bytes.subarray(0, candidate.identity.size)).digest("hex") ===
|
|
70
|
+
candidate.identity.sha256
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
function readCandidateInitialLines(
|
|
62
75
|
candidate: LogicalSessionCandidate,
|
|
63
76
|
readInitialLines: ((file: string, maxLines: number) => string[]) | undefined,
|
|
77
|
+
appendBase?: LogicalSessionCandidate,
|
|
64
78
|
): string[] {
|
|
65
79
|
if (readInitialLines) return readInitialLines(candidate.path, 8);
|
|
66
80
|
if (candidate.provenance !== "legacy") {
|
|
67
81
|
const security = verifyOwnerOnlyPathSecurity(candidate.path, "file");
|
|
68
|
-
if (!security.ok) throw new
|
|
82
|
+
if (!security.ok) throw new ManagedCandidateUnavailableError("Managed session metadata path is unsafe.");
|
|
69
83
|
}
|
|
70
|
-
|
|
84
|
+
let snapshot: SessionStorageSnapshot;
|
|
85
|
+
try {
|
|
86
|
+
snapshot = new FileSessionStorage().readSnapshotSync(candidate.path);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
89
|
+
if (code === "ENOENT" || code === "ELOOP" || code === "ENOTDIR")
|
|
90
|
+
throw new ManagedCandidateUnavailableError("Managed session metadata is unavailable.");
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
const sameFile = snapshot.stat.dev === candidate.identity.dev && snapshot.stat.ino === candidate.identity.ino;
|
|
71
94
|
const digest = createHash("sha256").update(snapshot.bytes).digest("hex");
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
snapshot.stat.
|
|
75
|
-
snapshot.stat.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
95
|
+
const exactIdentity =
|
|
96
|
+
sameFile &&
|
|
97
|
+
snapshot.stat.size === candidate.identity.size &&
|
|
98
|
+
snapshot.stat.mtimeNs === candidate.identity.mtimeNs &&
|
|
99
|
+
digest === candidate.identity.sha256;
|
|
100
|
+
if (appendBase && (!sameFile || !isAppendExtension(snapshot.bytes, appendBase)))
|
|
101
|
+
throw new ManagedCandidateUnavailableError("Managed session no longer extends the verified transcript.");
|
|
102
|
+
if (!exactIdentity) {
|
|
103
|
+
if (sameFile && isAppendExtension(snapshot.bytes, candidate))
|
|
104
|
+
throw new ManagedCandidateChangedError("Managed session was appended after ownership was verified.");
|
|
105
|
+
throw new ManagedCandidateUnavailableError("Managed session changed without an append-only extension.");
|
|
106
|
+
}
|
|
80
107
|
return Buffer.from(snapshot.bytes).toString("utf8").split("\n").slice(0, 8);
|
|
81
108
|
}
|
|
82
109
|
|
|
@@ -227,13 +254,13 @@ export async function listRecentSessions(deps: RecentActivityDeps): Promise<List
|
|
|
227
254
|
const resolved = await resolveRecentScopes(deps);
|
|
228
255
|
if (resolved.kind === "error") return resolved;
|
|
229
256
|
const warnings = [...resolved.warnings];
|
|
230
|
-
const candidates: LogicalSessionCandidate
|
|
257
|
+
const candidates: Array<{ scope: ManagedSessionScope; candidate: LogicalSessionCandidate }> = [];
|
|
231
258
|
for (const scope of resolved.scopes) {
|
|
232
259
|
const listed = await listManagedSessionCandidates({ scope });
|
|
233
260
|
if (listed.kind !== "complete") {
|
|
234
261
|
return { kind: "error", code: "managed_scan_failed", message: listed.message };
|
|
235
262
|
}
|
|
236
|
-
candidates.push(...listed.owned);
|
|
263
|
+
candidates.push(...listed.owned.map(candidate => ({ scope, candidate })));
|
|
237
264
|
warnings.push(
|
|
238
265
|
...listed.invalid
|
|
239
266
|
.filter(
|
|
@@ -245,28 +272,65 @@ export async function listRecentSessions(deps: RecentActivityDeps): Promise<List
|
|
|
245
272
|
}
|
|
246
273
|
|
|
247
274
|
const entries: RecentSessionEntry[] = [];
|
|
248
|
-
for (const candidate of candidates) {
|
|
249
|
-
let
|
|
275
|
+
for (const { scope, candidate } of candidates) {
|
|
276
|
+
let selected = candidate;
|
|
277
|
+
let initialLines: string[] | undefined;
|
|
250
278
|
try {
|
|
251
|
-
initialLines = readCandidateInitialLines(
|
|
279
|
+
initialLines = readCandidateInitialLines(selected, readInitialLines);
|
|
252
280
|
} catch (error) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
281
|
+
if (error instanceof ManagedCandidateChangedError) {
|
|
282
|
+
const refreshed = await listManagedSessionCandidates({ scope });
|
|
283
|
+
if (refreshed.kind !== "complete")
|
|
284
|
+
return { kind: "error", code: "managed_scan_failed", message: refreshed.message };
|
|
285
|
+
const sameFile = refreshed.owned.find(
|
|
286
|
+
current =>
|
|
287
|
+
path.resolve(current.path) === path.resolve(candidate.path) &&
|
|
288
|
+
current.identity.dev === candidate.identity.dev &&
|
|
289
|
+
current.identity.ino === candidate.identity.ino,
|
|
290
|
+
);
|
|
291
|
+
if (sameFile) {
|
|
292
|
+
try {
|
|
293
|
+
initialLines = readCandidateInitialLines(sameFile, readInitialLines, candidate);
|
|
294
|
+
selected = sameFile;
|
|
295
|
+
} catch (retryError) {
|
|
296
|
+
if (
|
|
297
|
+
!(retryError instanceof ManagedCandidateChangedError) &&
|
|
298
|
+
!(retryError instanceof ManagedCandidateUnavailableError)
|
|
299
|
+
)
|
|
300
|
+
return {
|
|
301
|
+
kind: "error",
|
|
302
|
+
code: "managed_scan_failed",
|
|
303
|
+
message: `Could not read managed session metadata: ${
|
|
304
|
+
retryError instanceof Error ? retryError.message : String(retryError)
|
|
305
|
+
}`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} else if (!(error instanceof ManagedCandidateUnavailableError)) {
|
|
310
|
+
return {
|
|
311
|
+
kind: "error",
|
|
312
|
+
code: "managed_scan_failed",
|
|
313
|
+
message: `Could not read managed session metadata: ${
|
|
314
|
+
error instanceof Error ? error.message : String(error)
|
|
315
|
+
}`,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (!initialLines) {
|
|
319
|
+
warnings.push(CHANGED_CANDIDATE_WARNING);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
258
322
|
}
|
|
259
323
|
const meta = headerMeta(initialLines[0]);
|
|
260
324
|
const internal = isInternalSession(initialLines);
|
|
261
325
|
if (internal && !includeInternal) continue;
|
|
262
326
|
entries.push({
|
|
263
|
-
sessionId:
|
|
264
|
-
path:
|
|
327
|
+
sessionId: selected.sessionId,
|
|
328
|
+
path: selected.cwd,
|
|
265
329
|
branch: meta.branch,
|
|
266
330
|
title: meta.title,
|
|
267
|
-
sessionStateFile:
|
|
268
|
-
mtimeMs:
|
|
269
|
-
currentTerminal: breadcrumbs.has(path.resolve(
|
|
331
|
+
sessionStateFile: selected.path,
|
|
332
|
+
mtimeMs: selected.identity.mtimeMs,
|
|
333
|
+
currentTerminal: breadcrumbs.has(path.resolve(selected.path)) || undefined,
|
|
270
334
|
internal: internal || undefined,
|
|
271
335
|
});
|
|
272
336
|
}
|
|
@@ -21,7 +21,22 @@ export const NOTIFICATION_PROTOCOL_VERSION = 3;
|
|
|
21
21
|
* confirmed-dead owner's lock in generation 10, legacy stopped-tombstone
|
|
22
22
|
* reclamation in generation 11, force-escalated SIGKILL of an unresponsive
|
|
23
23
|
* older-generation owner during automatic generation-upgrade reload in
|
|
24
|
-
* generation 12,
|
|
25
|
-
* incarnation recheck, replacing the darwin no-op) in generation 13
|
|
24
|
+
* generation 12, restored macOS daemon signaling (kill(2) with a start-time
|
|
25
|
+
* incarnation recheck, replacing the darwin no-op) in generation 13, retained
|
|
26
|
+
* legacy stopped-lock reclamation in generation 14, Windows expected-identity
|
|
27
|
+
* ACL verification and repair in generation 15, identity-fenced stale endpoint
|
|
28
|
+
* startup recovery in generation 16, Telegram topic recovery authority fencing
|
|
29
|
+
* in generation 17, fail-closed blank-token validation plus lifecycle-startup
|
|
30
|
+
* stop fencing in generation 18, recommended ask metadata rendering in
|
|
31
|
+
* generation 19, authoritative terminal session-close delivery and cleanup
|
|
32
|
+
* fencing, attested generation-bearing pre-incarnation owner handoff in
|
|
33
|
+
* generation 20, guarded modern generation-absent predecessor signaling in
|
|
34
|
+
* generation 21, dead Windows v0.10 owner replacement in generation 22, and
|
|
35
|
+
* retained native cleanup authority revalidation in generation 23, and typed
|
|
36
|
+
* retained exact-unlink cleanup authority acceptance (concrete detached
|
|
37
|
+
* quarantine plus proven canonical absence) in generation 24.
|
|
38
|
+
* Generation 25 adds startup dead-root prune + leak-artifact self-heal
|
|
39
|
+
* on TelegramNotificationDaemon.run (#2958). Generation 26 adds bounded reload
|
|
40
|
+
* cooldown and lazy Telegram topic lifecycle safeguards (#2956, #2960, #2984).
|
|
26
41
|
*/
|
|
27
|
-
export const DAEMON_GENERATION =
|
|
42
|
+
export const DAEMON_GENERATION = 26;
|
|
@@ -25,9 +25,11 @@ import type {
|
|
|
25
25
|
import { OWNERSHIP_MISMATCH_MESSAGE, ownershipMismatchRecovery } from "../../daemon/operator-contract";
|
|
26
26
|
import { resolveGjcRuntimeSpawnInfo } from "../../daemon/runtime";
|
|
27
27
|
import { isProcessIncarnation } from "../broker/process-incarnation";
|
|
28
|
+
|
|
28
29
|
import { getNotificationConfig, isTelegramConfigured, tokenFingerprint } from "./config";
|
|
29
30
|
import { exactUnlinkNotificationFile, readNotificationEndpointFile } from "./notification-service";
|
|
30
31
|
import {
|
|
32
|
+
type AttestedLegacyDaemonOwner,
|
|
31
33
|
confirmTelegramDaemonSpawn,
|
|
32
34
|
type DaemonState,
|
|
33
35
|
daemonPaths,
|
|
@@ -35,6 +37,7 @@ import {
|
|
|
35
37
|
isCurrentCompatibleOwner,
|
|
36
38
|
isFreshLiveOwner,
|
|
37
39
|
isSignalableMatchingOwner,
|
|
40
|
+
readAttestedLegacyDaemonOwner,
|
|
38
41
|
readDaemonRoots,
|
|
39
42
|
readDaemonState,
|
|
40
43
|
spawnTelegramDaemonOwner,
|
|
@@ -109,16 +112,19 @@ export async function clearTelegramControlRequest(
|
|
|
109
112
|
|
|
110
113
|
export interface DaemonProcessReference {
|
|
111
114
|
incarnation: string;
|
|
115
|
+
/** Whether the platform delivers the requested signal cooperatively or as a hard termination. */
|
|
116
|
+
termination: "cooperative" | "hard";
|
|
112
117
|
signalRoot(signal: NodeJS.Signals): void;
|
|
113
118
|
}
|
|
114
119
|
|
|
115
|
-
function defaultProcessReference(pid: number, platform = os.platform()): DaemonProcessReference | undefined {
|
|
120
|
+
export function defaultProcessReference(pid: number, platform = os.platform()): DaemonProcessReference | undefined {
|
|
116
121
|
try {
|
|
117
122
|
const processRef = Process.fromPid(pid);
|
|
118
123
|
if (!processRef || !isProcessIncarnation(processRef.incarnation)) return undefined;
|
|
119
124
|
const incarnation = processRef.incarnation;
|
|
120
125
|
return {
|
|
121
126
|
incarnation,
|
|
127
|
+
termination: platform === "win32" ? "hard" : "cooperative",
|
|
122
128
|
signalRoot: signal => {
|
|
123
129
|
const nativeSignal = os.constants.signals[signal];
|
|
124
130
|
if (nativeSignal === undefined) throw new Error(`Unsupported signal: ${signal}`);
|
|
@@ -178,8 +184,8 @@ function defaultPidAlive(pid: number): boolean {
|
|
|
178
184
|
try {
|
|
179
185
|
process.kill(pid, 0);
|
|
180
186
|
return true;
|
|
181
|
-
} catch {
|
|
182
|
-
return
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return (error as NodeJS.ErrnoException).code !== "ESRCH";
|
|
183
189
|
}
|
|
184
190
|
}
|
|
185
191
|
|
|
@@ -190,7 +196,6 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
190
196
|
private readonly now: () => number;
|
|
191
197
|
private readonly processReference: (pid: number) => DaemonProcessReference | undefined;
|
|
192
198
|
private readonly waitStepMs: number;
|
|
193
|
-
|
|
194
199
|
constructor(
|
|
195
200
|
private readonly settings: Settings,
|
|
196
201
|
private readonly deps: TelegramDaemonControlDeps = {},
|
|
@@ -315,31 +320,28 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
315
320
|
tokenFingerprint: string,
|
|
316
321
|
chatId: string,
|
|
317
322
|
signal: NodeJS.Signals,
|
|
318
|
-
): Promise<"signaled" | "already_gone" | "ownership_changed"> {
|
|
323
|
+
): Promise<"signaled" | "already_gone" | "ownership_changed" | "hard_termination"> {
|
|
319
324
|
const current = await readDaemonState(this.settings, this.fsImpl);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
current.
|
|
326
|
-
current.
|
|
327
|
-
current.
|
|
328
|
-
current.
|
|
329
|
-
current.
|
|
330
|
-
current.tokenFingerprint
|
|
331
|
-
current.chatId
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
// A matching captured owner that exited between the request and recheck has
|
|
337
|
-
// completed the handoff. A live owner with changed or unavailable provenance
|
|
338
|
-
// remains ambiguous and must not be treated as stopped.
|
|
325
|
+
|
|
326
|
+
const modernCurrentMatches =
|
|
327
|
+
hasSafeDaemonStateShape(current) &&
|
|
328
|
+
current.ownerId === captured.ownerId &&
|
|
329
|
+
current.acquisitionId === captured.acquisitionId &&
|
|
330
|
+
current.pid === captured.pid &&
|
|
331
|
+
current.generation === captured.generation &&
|
|
332
|
+
current.incarnation === captured.incarnation &&
|
|
333
|
+
current.tokenFingerprint === captured.tokenFingerprint &&
|
|
334
|
+
current.chatId === captured.chatId &&
|
|
335
|
+
current.tokenFingerprint === tokenFingerprint &&
|
|
336
|
+
current.chatId === chatId;
|
|
337
|
+
// The signal is privileged: the persisted record must still be the exact
|
|
338
|
+
// modern owner. Legacy generation-3 records remain migration evidence only;
|
|
339
|
+
// this controller never upgrades or signals them.
|
|
340
|
+
if (!modernCurrentMatches) return "ownership_changed";
|
|
339
341
|
if (!this.pidAlive(captured.pid)) return "already_gone";
|
|
340
342
|
if (
|
|
341
343
|
!isSignalableMatchingOwner({
|
|
342
|
-
state: current,
|
|
344
|
+
state: modernCurrentMatches ? current : captured,
|
|
343
345
|
tokenFingerprint,
|
|
344
346
|
chatId,
|
|
345
347
|
pidAlive: this.pidAlive,
|
|
@@ -352,6 +354,69 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
352
354
|
// the exit-and-reuse window between ordinary provenance checks and signaling.
|
|
353
355
|
// Its identity must still be the exact persisted incarnation before use.
|
|
354
356
|
if (!processRef || processRef.incarnation !== captured.incarnation) return "ownership_changed";
|
|
357
|
+
if (signal === "SIGTERM" && processRef.termination === "hard") return "hard_termination";
|
|
358
|
+
try {
|
|
359
|
+
processRef.signalRoot(signal);
|
|
360
|
+
} catch {
|
|
361
|
+
return "already_gone";
|
|
362
|
+
}
|
|
363
|
+
return "signaled";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#currentConfigurationMatches(tokenFingerprintValue: string, chatId: string): boolean {
|
|
367
|
+
const current = getNotificationConfig(this.settings);
|
|
368
|
+
return (
|
|
369
|
+
isTelegramConfigured(current) &&
|
|
370
|
+
tokenFingerprint(current.botToken) === tokenFingerprintValue &&
|
|
371
|
+
current.chatId === chatId
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async #signalAttestedLegacyOwner(
|
|
376
|
+
captured: AttestedLegacyDaemonOwner,
|
|
377
|
+
tokenFingerprint: string,
|
|
378
|
+
chatId: string,
|
|
379
|
+
signal: NodeJS.Signals,
|
|
380
|
+
): Promise<"signaled" | "already_gone" | "ownership_changed" | "hard_termination"> {
|
|
381
|
+
if (!this.#currentConfigurationMatches(tokenFingerprint, chatId)) return "ownership_changed";
|
|
382
|
+
const current = await readAttestedLegacyDaemonOwner({
|
|
383
|
+
settings: this.settings,
|
|
384
|
+
fs: this.fsImpl,
|
|
385
|
+
now: this.now,
|
|
386
|
+
pidIncarnation: this.deps.pidIncarnation,
|
|
387
|
+
tokenFingerprint,
|
|
388
|
+
chatId,
|
|
389
|
+
});
|
|
390
|
+
if (
|
|
391
|
+
!current ||
|
|
392
|
+
current.state.pid !== captured.state.pid ||
|
|
393
|
+
current.state.ownerId !== captured.state.ownerId ||
|
|
394
|
+
current.state.startedAt !== captured.state.startedAt ||
|
|
395
|
+
current.incarnation !== captured.incarnation
|
|
396
|
+
)
|
|
397
|
+
return "ownership_changed";
|
|
398
|
+
if (!this.pidAlive(captured.state.pid)) return "already_gone";
|
|
399
|
+
const processRef = this.processReference(captured.state.pid);
|
|
400
|
+
if (!processRef || processRef.incarnation !== captured.incarnation) return "ownership_changed";
|
|
401
|
+
const rechecked = await readAttestedLegacyDaemonOwner({
|
|
402
|
+
settings: this.settings,
|
|
403
|
+
fs: this.fsImpl,
|
|
404
|
+
now: this.now,
|
|
405
|
+
pidIncarnation: this.deps.pidIncarnation,
|
|
406
|
+
tokenFingerprint,
|
|
407
|
+
chatId,
|
|
408
|
+
});
|
|
409
|
+
if (
|
|
410
|
+
!rechecked ||
|
|
411
|
+
rechecked.state.pid !== current.state.pid ||
|
|
412
|
+
rechecked.state.ownerId !== current.state.ownerId ||
|
|
413
|
+
rechecked.state.startedAt !== current.state.startedAt ||
|
|
414
|
+
rechecked.incarnation !== current.incarnation ||
|
|
415
|
+
!this.#currentConfigurationMatches(tokenFingerprint, chatId) ||
|
|
416
|
+
!this.pidAlive(captured.state.pid)
|
|
417
|
+
)
|
|
418
|
+
return "ownership_changed";
|
|
419
|
+
if (signal === "SIGTERM" && processRef.termination === "hard") return "hard_termination";
|
|
355
420
|
try {
|
|
356
421
|
processRef.signalRoot(signal);
|
|
357
422
|
} catch {
|
|
@@ -376,7 +441,10 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
376
441
|
return this.stopOrReload("reload", opts);
|
|
377
442
|
}
|
|
378
443
|
|
|
379
|
-
async reloadForGenerationUpgrade(
|
|
444
|
+
async reloadForGenerationUpgrade(
|
|
445
|
+
opts: DaemonOperationOptions = {},
|
|
446
|
+
attestedLegacyUpgrade = false,
|
|
447
|
+
): Promise<TelegramGenerationReloadResult> {
|
|
380
448
|
// A generation upgrade MUST replace an incompatible older-generation owner to
|
|
381
449
|
// avoid a permanent single-poller deadlock. Unlike a manual `gjc daemon
|
|
382
450
|
// reload`, this automatic path force-escalates to SIGKILL when the old owner
|
|
@@ -384,7 +452,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
384
452
|
// self-recovers instead of failing closed and asking the operator to rerun
|
|
385
453
|
// with --force. The SIGKILL remains fenced to the still-live, still-matching
|
|
386
454
|
// captured owner (same ownerId + pid), so a fresh replacement is never killed.
|
|
387
|
-
const operation = await this.stopOrReload("reload", { ...opts, force: true });
|
|
455
|
+
const operation = await this.stopOrReload("reload", { ...opts, force: true }, attestedLegacyUpgrade);
|
|
388
456
|
if (!operation.ok) return { outcome: "failed", operation };
|
|
389
457
|
const after = await this.status();
|
|
390
458
|
return after.health === "running" ? { outcome: "ready", operation } : { outcome: "failed", operation };
|
|
@@ -394,7 +462,11 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
394
462
|
return this.stopOrReload("stop", opts);
|
|
395
463
|
}
|
|
396
464
|
|
|
397
|
-
private async stopOrReload(
|
|
465
|
+
private async stopOrReload(
|
|
466
|
+
action: "stop" | "reload",
|
|
467
|
+
opts: DaemonOperationOptions,
|
|
468
|
+
attestedLegacyUpgrade = false,
|
|
469
|
+
): Promise<DaemonOperationResult> {
|
|
398
470
|
const before = await this.status();
|
|
399
471
|
const warnings: string[] = [];
|
|
400
472
|
if (before.runtime.warning) warnings.push(before.runtime.warning);
|
|
@@ -409,6 +481,16 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
409
481
|
const killTimeoutMs = opts.killTimeoutMs ?? DEFAULT_KILL_TIMEOUT_MS;
|
|
410
482
|
|
|
411
483
|
const state = await readDaemonState(this.settings, this.fsImpl);
|
|
484
|
+
const attestedLegacyOwner = attestedLegacyUpgrade
|
|
485
|
+
? await readAttestedLegacyDaemonOwner({
|
|
486
|
+
settings: this.settings,
|
|
487
|
+
fs: this.fsImpl,
|
|
488
|
+
now: this.now,
|
|
489
|
+
pidIncarnation: this.deps.pidIncarnation,
|
|
490
|
+
tokenFingerprint: fp,
|
|
491
|
+
chatId,
|
|
492
|
+
})
|
|
493
|
+
: undefined;
|
|
412
494
|
const replaceableLiveOwner =
|
|
413
495
|
(action === "reload" &&
|
|
414
496
|
state !== undefined &&
|
|
@@ -427,6 +509,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
427
509
|
pidAlive: this.pidAlive,
|
|
428
510
|
pidIncarnation: this.deps.pidIncarnation,
|
|
429
511
|
})) ||
|
|
512
|
+
(action === "reload" && attestedLegacyOwner !== undefined && this.pidAlive(attestedLegacyOwner.state.pid)) ||
|
|
430
513
|
// A physically-live matching owner whose heartbeat is stale (hung) may be
|
|
431
514
|
// past-TTL yet still holding the poller. Autostart/generation-upgrade reloads
|
|
432
515
|
// stay conservative and refuse it, but an explicit `reload --force` must be
|
|
@@ -483,16 +566,18 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
483
566
|
}
|
|
484
567
|
|
|
485
568
|
// Running owner: capture identity, request cooperative stop, signal, wait.
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
569
|
+
const modernCapturedOwner =
|
|
570
|
+
hasSafeDaemonStateShape(state) &&
|
|
571
|
+
isSignalableMatchingOwner({
|
|
489
572
|
state,
|
|
490
573
|
tokenFingerprint: fp,
|
|
491
574
|
chatId,
|
|
492
575
|
pidAlive: this.pidAlive,
|
|
493
576
|
pidIncarnation: this.deps.pidIncarnation,
|
|
494
577
|
})
|
|
495
|
-
|
|
578
|
+
? state
|
|
579
|
+
: undefined;
|
|
580
|
+
if (!modernCapturedOwner && !attestedLegacyOwner) {
|
|
496
581
|
return this.result(
|
|
497
582
|
action,
|
|
498
583
|
false,
|
|
@@ -502,7 +587,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
502
587
|
warnings,
|
|
503
588
|
);
|
|
504
589
|
}
|
|
505
|
-
const capturedOwner = state;
|
|
590
|
+
const capturedOwner = modernCapturedOwner ?? attestedLegacyOwner!.state;
|
|
506
591
|
const oldOwnerId = capturedOwner.ownerId;
|
|
507
592
|
const oldPid = capturedOwner.pid;
|
|
508
593
|
const requestId = this.deps.randomId?.() ?? `${this.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
@@ -511,7 +596,21 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
511
596
|
{ version: 1, requestId, action, ownerId: oldOwnerId, pid: oldPid, createdAt: this.now() },
|
|
512
597
|
this.fsImpl,
|
|
513
598
|
);
|
|
514
|
-
|
|
599
|
+
const signalResult = attestedLegacyOwner
|
|
600
|
+
? await this.#signalAttestedLegacyOwner(attestedLegacyOwner, fp, chatId, "SIGTERM")
|
|
601
|
+
: await this.signalCapturedOwner(modernCapturedOwner!, fp, chatId, "SIGTERM");
|
|
602
|
+
if (signalResult === "hard_termination") {
|
|
603
|
+
await this.clearOwnRequest(requestId);
|
|
604
|
+
return this.result(
|
|
605
|
+
action,
|
|
606
|
+
false,
|
|
607
|
+
"telegram daemon has hard process authority; refusing cooperative SIGTERM",
|
|
608
|
+
before,
|
|
609
|
+
await this.status(),
|
|
610
|
+
warnings,
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
if (signalResult === "ownership_changed") {
|
|
515
614
|
await this.clearOwnRequest(requestId);
|
|
516
615
|
return this.result(
|
|
517
616
|
action,
|
|
@@ -559,7 +658,9 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
559
658
|
// the captured owner/pid still matches, so we never kill a different owner.
|
|
560
659
|
const stillSameOwner = current !== undefined && current.ownerId === oldOwnerId && current.pid === oldPid;
|
|
561
660
|
if (opts.force && stillSameOwner) {
|
|
562
|
-
const killResult =
|
|
661
|
+
const killResult = attestedLegacyOwner
|
|
662
|
+
? await this.#signalAttestedLegacyOwner(attestedLegacyOwner, fp, chatId, "SIGKILL")
|
|
663
|
+
: await this.signalCapturedOwner(modernCapturedOwner!, fp, chatId, "SIGKILL");
|
|
563
664
|
// Kill only a still-live matching owner; an owner that exited between the
|
|
564
665
|
// graceful timeout and this recheck ("already_gone") is confirmed dead by
|
|
565
666
|
// waitForPidDeath, while a real ownership change stays fenced.
|
|
@@ -585,6 +686,17 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
585
686
|
return this.result(action, true, "stopped telegram daemon", before, after, warnings);
|
|
586
687
|
}
|
|
587
688
|
|
|
689
|
+
if (!this.#currentConfigurationMatches(fp, chatId)) {
|
|
690
|
+
const after = await this.status();
|
|
691
|
+
return this.result(
|
|
692
|
+
action,
|
|
693
|
+
false,
|
|
694
|
+
"telegram notification configuration changed during reload; refusing to spawn",
|
|
695
|
+
before,
|
|
696
|
+
after,
|
|
697
|
+
warnings,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
588
700
|
const { spawned, ready } = await this.spawnAndWait(roots, fp, chatId);
|
|
589
701
|
warnings.push(...spawned.warnings);
|
|
590
702
|
const after = await this.status();
|