@gajae-code/coding-agent 0.11.7 → 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 +55 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/config/keybindings.d.ts +33 -3
- 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 +15 -2
- 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/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 +1 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
- package/dist/types/main.d.ts +8 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +3 -2
- package/dist/types/modes/components/welcome.d.ts +2 -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 +5 -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 +3 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- 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 +5 -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 +25 -0
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -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/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +11 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +17 -2
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-storage.d.ts +18 -0
- 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/image-gen.d.ts +47 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -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 +2 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/cli/args.ts +2 -92
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli.ts +18 -0
- package/src/commands/sdk.ts +2 -2
- package/src/config/keybindings.ts +157 -24
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +17 -2
- 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/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/prelude.py +2 -2
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- 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 +5 -5
- package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/internal-urls/local-protocol.ts +33 -16
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +25 -10
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/components/model-selector.ts +42 -8
- 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 +20 -4
- package/src/modes/components/welcome.ts +22 -18
- 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 +7 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +46 -54
- package/src/modes/interactive-mode.ts +41 -27
- 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 +3 -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/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/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 +13 -6
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +18 -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 +240 -65
- package/src/sdk/bus/lifecycle-commands.ts +47 -3
- 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 +27 -2
- 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 +13 -3
- package/src/sdk/bus/telegram-daemon-control.ts +145 -33
- package/src/sdk/bus/telegram-daemon.ts +835 -110
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/cli/session-cli.ts +15 -2
- 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 +47 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +2 -0
- package/src/sdk/startup-capability.ts +9 -1
- package/src/session/agent-session.ts +52 -5
- package/src/session/blob-store.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +854 -84
- package/src/session/internal/managed-session-storage.ts +251 -199
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +310 -327
- package/src/session/session-storage.ts +128 -11
- 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 +198 -88
- 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/image-gen.ts +178 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/url-guard.ts +3 -1
- package/src/web/scrapers/docs-rs.ts +30 -5
|
@@ -49,6 +49,49 @@ const USAGE = [
|
|
|
49
49
|
"/session_recent [create|resume]",
|
|
50
50
|
].join("\n");
|
|
51
51
|
|
|
52
|
+
function tokenizeLifecycleCommand(value: string): string[] | undefined {
|
|
53
|
+
const tokens: string[] = [];
|
|
54
|
+
let token = "";
|
|
55
|
+
let quote: '"' | "'" | undefined;
|
|
56
|
+
let started = false;
|
|
57
|
+
for (let index = 0; index < value.length; index++) {
|
|
58
|
+
const character = value[index]!;
|
|
59
|
+
if (character === "\\") {
|
|
60
|
+
const next = value[index + 1];
|
|
61
|
+
if (next !== undefined && (next === "\\" || next === '"' || next === "'" || /\s/u.test(next))) {
|
|
62
|
+
token += next;
|
|
63
|
+
index++;
|
|
64
|
+
} else token += character;
|
|
65
|
+
started = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (quote) {
|
|
69
|
+
if (character === quote) quote = undefined;
|
|
70
|
+
else token += character;
|
|
71
|
+
started = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (character === '"' || character === "'") {
|
|
75
|
+
quote = character;
|
|
76
|
+
started = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (/\s/u.test(character)) {
|
|
80
|
+
if (started) {
|
|
81
|
+
tokens.push(token);
|
|
82
|
+
token = "";
|
|
83
|
+
started = false;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
token += character;
|
|
88
|
+
started = true;
|
|
89
|
+
}
|
|
90
|
+
if (quote) return undefined;
|
|
91
|
+
if (started) tokens.push(token);
|
|
92
|
+
return tokens;
|
|
93
|
+
}
|
|
94
|
+
|
|
52
95
|
/** True when the text begins with any /session_* token, regardless of addressability. */
|
|
53
96
|
export function isLifecycleCommandLikeText(text: string | undefined): boolean {
|
|
54
97
|
if (!text) return false;
|
|
@@ -98,8 +141,10 @@ export function parseLifecycleCommand(
|
|
|
98
141
|
text: string | undefined,
|
|
99
142
|
ctx: { chatType?: string; botUsername?: string } = {},
|
|
100
143
|
): ParsedLifecycleCommand {
|
|
144
|
+
const tokens = tokenizeLifecycleCommand((text ?? "").trim());
|
|
145
|
+
if (!tokens) return { kind: "usage", message: USAGE };
|
|
146
|
+
const [rawCommand, ...args] = tokens;
|
|
101
147
|
const raw = (text ?? "").trim();
|
|
102
|
-
const [rawCommand, ...args] = raw.split(/\s+/);
|
|
103
148
|
const command = normalizeLifecycleCommandToken(rawCommand ?? "", ctx);
|
|
104
149
|
if (command === undefined) return { kind: "none" };
|
|
105
150
|
|
|
@@ -140,9 +185,8 @@ export function parseLifecycleCommand(
|
|
|
140
185
|
|
|
141
186
|
// /session_create <kind> ... [--mpreset <profile>]
|
|
142
187
|
const { positional, modelPreset } = extractModelPreset(args);
|
|
143
|
-
if (modelPreset !== undefined &&
|
|
188
|
+
if (modelPreset !== undefined && modelPreset.length === 0)
|
|
144
189
|
return { kind: "reject", reason: "invalid_target", message: `Invalid model preset name.\n\n${USAGE}` };
|
|
145
|
-
}
|
|
146
190
|
const kind = positional[0];
|
|
147
191
|
if (kind === "path") {
|
|
148
192
|
if (positional.length !== 2) return { kind: "usage", message: USAGE };
|
|
@@ -1178,16 +1178,25 @@ export function outcomeToResponse(frame: SessionLifecycleRequest, outcome: Lifec
|
|
|
1178
1178
|
if (frame.type === "session_close") {
|
|
1179
1179
|
if (typeof e.sessionId !== "string" || typeof e.processGone !== "boolean")
|
|
1180
1180
|
throw new Error("invalid close success ledger entry");
|
|
1181
|
+
if (!e.processGone) {
|
|
1182
|
+
return {
|
|
1183
|
+
type: "session_lifecycle_error",
|
|
1184
|
+
requestId: frame.requestId,
|
|
1185
|
+
status: "error",
|
|
1186
|
+
reason: "terminal_uncertain",
|
|
1187
|
+
message: "close did not confirm process termination",
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1181
1190
|
return {
|
|
1182
1191
|
type: "session_close_response",
|
|
1183
1192
|
requestId: frame.requestId,
|
|
1184
1193
|
status: "ok",
|
|
1185
1194
|
sessionId: e.sessionId,
|
|
1186
|
-
processGone:
|
|
1195
|
+
processGone: true,
|
|
1187
1196
|
historyPreserved: true,
|
|
1188
1197
|
// The killed session's per-session endpoint record is reaped by the
|
|
1189
1198
|
// daemon's dead-PID scan (scanRoots), so it is effectively stale.
|
|
1190
|
-
endpointStale:
|
|
1199
|
+
endpointStale: true,
|
|
1191
1200
|
};
|
|
1192
1201
|
}
|
|
1193
1202
|
if (
|
|
@@ -309,6 +309,21 @@ export async function handleLifecycleRequest(
|
|
|
309
309
|
|
|
310
310
|
// 4. Durable idempotency.
|
|
311
311
|
const doc = await deps.store.read();
|
|
312
|
+
const existing = doc.entries[key];
|
|
313
|
+
if (
|
|
314
|
+
frame.type === "session_close" &&
|
|
315
|
+
existing?.verb === "session_close" &&
|
|
316
|
+
existing.requestHash === hash &&
|
|
317
|
+
existing.state === "success" &&
|
|
318
|
+
typeof existing.sessionId === "string" &&
|
|
319
|
+
existing.processGone === false
|
|
320
|
+
) {
|
|
321
|
+
existing.state = "terminal_uncertain";
|
|
322
|
+
existing.reason = "terminal_uncertain";
|
|
323
|
+
existing.updatedAt = deps.now();
|
|
324
|
+
delete existing.processGone;
|
|
325
|
+
await deps.store.write(doc);
|
|
326
|
+
}
|
|
312
327
|
const dup = classifyDuplicate(doc.entries[key], hash);
|
|
313
328
|
switch (dup.kind) {
|
|
314
329
|
case "conflict":
|
|
@@ -394,12 +409,28 @@ export async function handleLifecycleRequest(
|
|
|
394
409
|
|
|
395
410
|
if (frame.type === "session_close") {
|
|
396
411
|
const closed = await deps.closeSession(frame.target);
|
|
412
|
+
if (!closed.processGone) {
|
|
413
|
+
Object.assign(entry, {
|
|
414
|
+
state: "terminal_uncertain",
|
|
415
|
+
updatedAt: deps.now(),
|
|
416
|
+
reason: "terminal_uncertain",
|
|
417
|
+
sessionId: frame.target.sessionId,
|
|
418
|
+
tmuxSession: frame.target.tmuxSession,
|
|
419
|
+
});
|
|
420
|
+
await deps.store.write(doc);
|
|
421
|
+
await deps.audit({ ...baseAudit, event: "terminal_uncertain", reason: "terminal_uncertain" });
|
|
422
|
+
return {
|
|
423
|
+
status: "error",
|
|
424
|
+
reason: "terminal_uncertain",
|
|
425
|
+
message: "session_close could not confirm process disappearance; manual check",
|
|
426
|
+
};
|
|
427
|
+
}
|
|
397
428
|
Object.assign(entry, {
|
|
398
429
|
state: "success",
|
|
399
430
|
updatedAt: deps.now(),
|
|
400
431
|
sessionId: frame.target.sessionId,
|
|
401
432
|
tmuxSession: frame.target.tmuxSession,
|
|
402
|
-
processGone:
|
|
433
|
+
processGone: true,
|
|
403
434
|
});
|
|
404
435
|
await deps.store.write(doc);
|
|
405
436
|
await deps.audit({ ...baseAudit, event: "success" });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CasRestoreResult } from "../../config/atomic-yaml-patch";
|
|
2
2
|
import type { RawSettings, SettingsAtomicPatch, SettingsAtomicReceipt } from "../../config/settings";
|
|
3
|
+
import { isProcessIncarnation, processIncarnation } from "../broker/process-incarnation";
|
|
3
4
|
import {
|
|
4
5
|
getNotificationConfig,
|
|
5
6
|
hasNonBlankValue,
|
|
@@ -53,6 +54,7 @@ export interface ProposedTelegramIdentityPreflightInput {
|
|
|
53
54
|
deps?: {
|
|
54
55
|
fs?: TelegramDaemonFs;
|
|
55
56
|
pidAlive?: (pid: number) => boolean;
|
|
57
|
+
pidIncarnation?: (pid: number) => string | undefined;
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -109,6 +111,16 @@ export async function proposedTelegramIdentity(
|
|
|
109
111
|
|
|
110
112
|
const pidAlive = input.deps?.pidAlive ?? defaultPidAlive;
|
|
111
113
|
if (!pidAlive(metadata.pid)) return { status: "absent" };
|
|
114
|
+
const pidIncarnation = input.deps?.pidIncarnation ?? processIncarnation;
|
|
115
|
+
const persistedIncarnation = state.incarnation;
|
|
116
|
+
const currentIncarnation = pidIncarnation(metadata.pid);
|
|
117
|
+
if (
|
|
118
|
+
!isProcessIncarnation(persistedIncarnation) ||
|
|
119
|
+
!isProcessIncarnation(currentIncarnation) ||
|
|
120
|
+
persistedIncarnation !== currentIncarnation
|
|
121
|
+
) {
|
|
122
|
+
return { status: "absent" };
|
|
123
|
+
}
|
|
112
124
|
|
|
113
125
|
if (state.tokenFingerprint === tokenFingerprint(input.botToken) && state.chatId === input.chatId) {
|
|
114
126
|
return {
|
|
@@ -900,7 +900,16 @@ async function detachTransitionMarker(
|
|
|
900
900
|
): Promise<boolean> {
|
|
901
901
|
if (!snapshot.identity || !fs.exactUnlink) return false;
|
|
902
902
|
try {
|
|
903
|
-
|
|
903
|
+
const removed = await fs.exactUnlink(path, snapshot.identity);
|
|
904
|
+
if (removed.ok) return true;
|
|
905
|
+
// Accept only typed retained authority: a concrete detached quarantine plus
|
|
906
|
+
// a proven-absent canonical marker pathname. Anything else stays fail-closed.
|
|
907
|
+
return (
|
|
908
|
+
removed.code === "cleanup_pending" &&
|
|
909
|
+
typeof removed.detachedPath === "string" &&
|
|
910
|
+
removed.detachedPath.length > 0 &&
|
|
911
|
+
(await fs.readFile(path, "utf8").catch(() => undefined)) === undefined
|
|
912
|
+
);
|
|
904
913
|
} catch {
|
|
905
914
|
return false;
|
|
906
915
|
}
|
|
@@ -1080,11 +1089,27 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
|
|
|
1080
1089
|
try {
|
|
1081
1090
|
const result = await fs.exactUnlink(file, record.identity);
|
|
1082
1091
|
if (!result.ok) {
|
|
1083
|
-
recoveryFailures += 1;
|
|
1084
1092
|
if (result.detachedPath) detached.push(result.detachedPath);
|
|
1085
1093
|
if (result.retainedSuccessorPath) retainedSuccessors.push(result.retainedSuccessorPath);
|
|
1086
1094
|
if (result.retainedPlaceholderPath) retainedPlaceholders.push(result.retainedPlaceholderPath);
|
|
1087
1095
|
if (result.retainedUnknownPath) retainedUnknown.push(result.retainedUnknownPath);
|
|
1096
|
+
// Typed retained authority with a concrete quarantine and a proven-absent
|
|
1097
|
+
// canonical endpoint counts as removed; the detached path above is the
|
|
1098
|
+
// durable evidence. Anything else is a genuine recovery failure.
|
|
1099
|
+
const retainedRemoved =
|
|
1100
|
+
result.code === "cleanup_pending" &&
|
|
1101
|
+
typeof result.detachedPath === "string" &&
|
|
1102
|
+
result.detachedPath.length > 0 &&
|
|
1103
|
+
(await fs.readFile(file, "utf8").catch(() => undefined)) === undefined;
|
|
1104
|
+
if (retainedRemoved) {
|
|
1105
|
+
removed.push({
|
|
1106
|
+
sessionId: view.sessionId,
|
|
1107
|
+
pid: view.pid,
|
|
1108
|
+
reason: view.stale ? "stale-flag" : "dead-pid",
|
|
1109
|
+
});
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
recoveryFailures += 1;
|
|
1088
1113
|
if (
|
|
1089
1114
|
!result.detachedPath &&
|
|
1090
1115
|
!result.retainedSuccessorPath &&
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded authoritative reconciliation state for Q26 `turn.prompt_status`.
|
|
3
|
+
*
|
|
4
|
+
* Separate from the lifecycle delivery buffers (promptSubmissions /
|
|
5
|
+
* promptTerminalTombstones), which exist to deliver frames and intentionally
|
|
6
|
+
* forget outcomes. This record preserves accepted/in_flight/terminal_ok/failed
|
|
7
|
+
* plus bounded sanitized failure metadata so a caller can reconcile a prior
|
|
8
|
+
* `turn.prompt` after disconnect/reconnect.
|
|
9
|
+
*
|
|
10
|
+
* Semantics (public contract, mirrored in sdk/prompt-status.ts):
|
|
11
|
+
* - Active records are NEVER converted to terminal by age or capacity.
|
|
12
|
+
* - Terminal records are retained for TERMINAL_TTL_MS and bounded to
|
|
13
|
+
* TERMINAL_CAPACITY, evicted oldest-terminal-first by terminalAt; only then
|
|
14
|
+
* does a lookup honestly report `unknown`.
|
|
15
|
+
* - The durability floor is the live session process: restart means `unknown`.
|
|
16
|
+
* - The clientRef index is session-runtime scoped; a ref conflicts only while
|
|
17
|
+
* retained and must never be reused as a retry mechanism.
|
|
18
|
+
* - Terminal transitions settle once: the first terminal outcome wins.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const PROMPT_RECONCILIATION_ACTIVE_CAPACITY = 128;
|
|
22
|
+
export const PROMPT_RECONCILIATION_TERMINAL_CAPACITY = 256;
|
|
23
|
+
export const PROMPT_RECONCILIATION_TERMINAL_TTL_MS = 15 * 60_000;
|
|
24
|
+
export const PROMPT_FAILURE_CODE_MAX = 64;
|
|
25
|
+
|
|
26
|
+
export type PromptReconciliationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed";
|
|
27
|
+
|
|
28
|
+
export interface PromptCorrelation {
|
|
29
|
+
commandId: string;
|
|
30
|
+
turnId: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PromptReconciliationRecord extends PromptCorrelation {
|
|
34
|
+
clientRef?: string;
|
|
35
|
+
status: PromptReconciliationStatus;
|
|
36
|
+
error?: { code: string; message: string };
|
|
37
|
+
acceptedAt: number;
|
|
38
|
+
startedAt?: number;
|
|
39
|
+
terminalAt?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type TurnPromptReconciliation =
|
|
43
|
+
| {
|
|
44
|
+
status: "accepted";
|
|
45
|
+
commandId: string;
|
|
46
|
+
turnId: string;
|
|
47
|
+
clientRef?: string;
|
|
48
|
+
acceptedAt: number;
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
status: "in_flight";
|
|
52
|
+
commandId: string;
|
|
53
|
+
turnId: string;
|
|
54
|
+
clientRef?: string;
|
|
55
|
+
acceptedAt: number;
|
|
56
|
+
startedAt: number;
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
status: "terminal_ok";
|
|
60
|
+
commandId: string;
|
|
61
|
+
turnId: string;
|
|
62
|
+
clientRef?: string;
|
|
63
|
+
acceptedAt: number;
|
|
64
|
+
startedAt?: number;
|
|
65
|
+
terminalAt: number;
|
|
66
|
+
}
|
|
67
|
+
| {
|
|
68
|
+
status: "failed";
|
|
69
|
+
commandId: string;
|
|
70
|
+
turnId: string;
|
|
71
|
+
clientRef?: string;
|
|
72
|
+
acceptedAt: number;
|
|
73
|
+
startedAt?: number;
|
|
74
|
+
terminalAt: number;
|
|
75
|
+
error: { code: string; message: string };
|
|
76
|
+
}
|
|
77
|
+
| { status: "unknown" };
|
|
78
|
+
|
|
79
|
+
export interface PromptReconciliation {
|
|
80
|
+
/** Fail-closed admission BEFORE any execution; holds an identity-bound reservation. */
|
|
81
|
+
admit(clientRef?: string): void;
|
|
82
|
+
/** Discard one admission reservation without creating a record (rejection/cancellation). */
|
|
83
|
+
releaseAdmission(clientRef?: string): void;
|
|
84
|
+
/** Transition a reservation into the accepted record at preflight acceptance. */
|
|
85
|
+
noteAccepted(correlation: PromptCorrelation, clientRef?: string): void;
|
|
86
|
+
/** Lifecycle transition; terminal outcomes settle exactly once. */
|
|
87
|
+
noteTransition(
|
|
88
|
+
correlation: PromptCorrelation | undefined,
|
|
89
|
+
frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
|
|
90
|
+
): void;
|
|
91
|
+
lookup(selector: { commandId?: string; turnId?: string; clientRef?: string }): TurnPromptReconciliation;
|
|
92
|
+
cleanup(): void;
|
|
93
|
+
activeCount(): number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Safe-token code capped at 64; arbitrary failure text is never retained. */
|
|
97
|
+
export function sanitizePromptFailure(error: unknown): { code: string; message: string } {
|
|
98
|
+
const candidate = error as { code?: unknown } | undefined;
|
|
99
|
+
const rawCode = typeof candidate?.code === "string" ? candidate.code : "";
|
|
100
|
+
const code = rawCode.length <= PROMPT_FAILURE_CODE_MAX && /^[A-Za-z0-9._-]+$/.test(rawCode) ? rawCode : "internal";
|
|
101
|
+
return { code, message: "Prompt submission failed." };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createPromptReconciliation(options: { now?: () => number } = {}): PromptReconciliation {
|
|
105
|
+
const now = options.now ?? Date.now;
|
|
106
|
+
const records = new Map<string, PromptReconciliationRecord>();
|
|
107
|
+
const clientRefIndex = new Map<string, string>();
|
|
108
|
+
const reservedClientRefs = new Set<string>();
|
|
109
|
+
// One identity-bound entry per admission. Transitions consume exactly one
|
|
110
|
+
// matching entry and stale/duplicate transitions are no-ops, so reservation
|
|
111
|
+
// accounting is exactly-once even under repeated calls.
|
|
112
|
+
const reservations: Array<string | undefined> = [];
|
|
113
|
+
const keyOf = (correlation: PromptCorrelation) => `${correlation.commandId}:${correlation.turnId}`;
|
|
114
|
+
|
|
115
|
+
const remove = (key: string) => {
|
|
116
|
+
const record = records.get(key);
|
|
117
|
+
if (!record) return;
|
|
118
|
+
records.delete(key);
|
|
119
|
+
if (record.clientRef !== undefined && clientRefIndex.get(record.clientRef) === key)
|
|
120
|
+
clientRefIndex.delete(record.clientRef);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const cleanup = () => {
|
|
124
|
+
const at = now();
|
|
125
|
+
for (const [key, record] of records)
|
|
126
|
+
if (record.terminalAt !== undefined && record.terminalAt + PROMPT_RECONCILIATION_TERMINAL_TTL_MS <= at)
|
|
127
|
+
remove(key);
|
|
128
|
+
const terminalEntries = [...records.entries()].filter(([, record]) => record.terminalAt !== undefined);
|
|
129
|
+
if (terminalEntries.length <= PROMPT_RECONCILIATION_TERMINAL_CAPACITY) return;
|
|
130
|
+
// Evict oldest-terminal-first by terminalAt, not acceptance/insertion
|
|
131
|
+
// order; the stable sort keeps acceptance order for exact ties.
|
|
132
|
+
terminalEntries.sort((a, b) => (a[1].terminalAt as number) - (b[1].terminalAt as number));
|
|
133
|
+
for (const [key] of terminalEntries.slice(0, terminalEntries.length - PROMPT_RECONCILIATION_TERMINAL_CAPACITY))
|
|
134
|
+
remove(key);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const activeCount = () => {
|
|
138
|
+
let count = 0;
|
|
139
|
+
for (const record of records.values()) if (record.terminalAt === undefined) count++;
|
|
140
|
+
return count;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** Consume one reservation matching clientRef; no-op when none is outstanding. */
|
|
144
|
+
const consumeReservation = (clientRef?: string) => {
|
|
145
|
+
const index = reservations.indexOf(clientRef);
|
|
146
|
+
if (index === -1) return;
|
|
147
|
+
reservations.splice(index, 1);
|
|
148
|
+
if (clientRef !== undefined && !reservations.includes(clientRef)) reservedClientRefs.delete(clientRef);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const admit = (clientRef?: string) => {
|
|
152
|
+
cleanup();
|
|
153
|
+
if (clientRef !== undefined && (clientRefIndex.has(clientRef) || reservedClientRefs.has(clientRef)))
|
|
154
|
+
throw Object.assign(
|
|
155
|
+
new Error("A prompt with this clientRef is already retained; never reuse a clientRef for retry."),
|
|
156
|
+
{ code: "client_ref_conflict" },
|
|
157
|
+
);
|
|
158
|
+
if (activeCount() + reservations.length >= PROMPT_RECONCILIATION_ACTIVE_CAPACITY)
|
|
159
|
+
throw Object.assign(new Error("Too many active prompt submissions; reconcile or await terminal state."), {
|
|
160
|
+
code: "reconciliation_capacity",
|
|
161
|
+
});
|
|
162
|
+
reservations.push(clientRef);
|
|
163
|
+
if (clientRef !== undefined) reservedClientRefs.add(clientRef);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const releaseAdmission = (clientRef?: string) => {
|
|
167
|
+
consumeReservation(clientRef);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const noteAccepted = (correlation: PromptCorrelation, clientRef?: string) => {
|
|
171
|
+
cleanup();
|
|
172
|
+
consumeReservation(clientRef);
|
|
173
|
+
const at = now();
|
|
174
|
+
const key = keyOf(correlation);
|
|
175
|
+
records.set(key, {
|
|
176
|
+
commandId: correlation.commandId,
|
|
177
|
+
turnId: correlation.turnId,
|
|
178
|
+
...(clientRef !== undefined ? { clientRef } : {}),
|
|
179
|
+
status: "accepted",
|
|
180
|
+
acceptedAt: at,
|
|
181
|
+
});
|
|
182
|
+
if (clientRef !== undefined) clientRefIndex.set(clientRef, key);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const noteTransition = (
|
|
186
|
+
correlation: PromptCorrelation | undefined,
|
|
187
|
+
frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
|
|
188
|
+
) => {
|
|
189
|
+
if (!correlation) return;
|
|
190
|
+
const record = records.get(keyOf(correlation));
|
|
191
|
+
if (!record || record.terminalAt !== undefined) return;
|
|
192
|
+
if (frame.type === "agent_start") {
|
|
193
|
+
if (record.status === "accepted") {
|
|
194
|
+
record.status = "in_flight";
|
|
195
|
+
record.startedAt = now();
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
record.terminalAt = now();
|
|
200
|
+
if (frame.type === "agent_failed") {
|
|
201
|
+
record.status = "failed";
|
|
202
|
+
record.error = sanitizePromptFailure(frame.error);
|
|
203
|
+
} else {
|
|
204
|
+
record.status = "terminal_ok";
|
|
205
|
+
}
|
|
206
|
+
// Enforce terminal retention immediately at settlement, not lazily.
|
|
207
|
+
cleanup();
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const lookup = (selector: { commandId?: string; turnId?: string; clientRef?: string }): TurnPromptReconciliation => {
|
|
211
|
+
cleanup();
|
|
212
|
+
const key =
|
|
213
|
+
selector.clientRef !== undefined
|
|
214
|
+
? clientRefIndex.get(selector.clientRef)
|
|
215
|
+
: selector.commandId !== undefined && selector.turnId !== undefined
|
|
216
|
+
? keyOf({ commandId: selector.commandId, turnId: selector.turnId })
|
|
217
|
+
: undefined;
|
|
218
|
+
const record = key === undefined ? undefined : records.get(key);
|
|
219
|
+
if (!record) return { status: "unknown" };
|
|
220
|
+
const identity = {
|
|
221
|
+
commandId: record.commandId,
|
|
222
|
+
turnId: record.turnId,
|
|
223
|
+
...(record.clientRef !== undefined ? { clientRef: record.clientRef } : {}),
|
|
224
|
+
acceptedAt: record.acceptedAt,
|
|
225
|
+
};
|
|
226
|
+
if (record.status === "accepted") return { status: "accepted", ...identity };
|
|
227
|
+
if (record.status === "in_flight")
|
|
228
|
+
return { status: "in_flight", ...identity, startedAt: record.startedAt as number };
|
|
229
|
+
const terminal = {
|
|
230
|
+
...identity,
|
|
231
|
+
...(record.startedAt !== undefined ? { startedAt: record.startedAt } : {}),
|
|
232
|
+
terminalAt: record.terminalAt as number,
|
|
233
|
+
};
|
|
234
|
+
if (record.status === "terminal_ok") return { status: "terminal_ok", ...terminal };
|
|
235
|
+
return { status: "failed", ...terminal, error: record.error ?? sanitizePromptFailure(undefined) };
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
return { admit, releaseAdmission, noteAccepted, noteTransition, lookup, cleanup, activeCount };
|
|
239
|
+
}
|
|
@@ -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
|
}
|
|
@@ -26,7 +26,17 @@ export const NOTIFICATION_PROTOCOL_VERSION = 3;
|
|
|
26
26
|
* legacy stopped-lock reclamation in generation 14, Windows expected-identity
|
|
27
27
|
* ACL verification and repair in generation 15, identity-fenced stale endpoint
|
|
28
28
|
* startup recovery in generation 16, Telegram topic recovery authority fencing
|
|
29
|
-
* in generation 17,
|
|
30
|
-
* stop fencing in generation 18
|
|
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).
|
|
31
41
|
*/
|
|
32
|
-
export const DAEMON_GENERATION =
|
|
42
|
+
export const DAEMON_GENERATION = 26;
|