@code-yeongyu/senpi 2026.10.19-2 → 2026.10.20-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/bundle/chunks/{anthropic-messages-VVRTT7O5.js → anthropic-messages-3QYMAEH2.js} +1 -1
- package/dist/bundle/chunks/{args-35XTBPC7.js → args-NHDT5JG3.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-BUSMTCQ5.js → azure-openai-responses-2DCY6QKO.js} +1 -1
- package/dist/bundle/chunks/chunk-2JGY3V4H.js +395 -0
- package/dist/bundle/chunks/{chunk-2RLZXLXS.js → chunk-2LQZJFKQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-2IK7QUJX.js → chunk-2SM4TR4H.js} +1 -1
- package/dist/bundle/chunks/{chunk-DXOSXUVI.js → chunk-3YKKXTJM.js} +1 -1
- package/dist/bundle/chunks/chunk-6L3H4UVN.js +2 -0
- package/dist/bundle/chunks/{chunk-46NW3UKO.js → chunk-7R4CJDWT.js} +1 -1
- package/dist/bundle/chunks/chunk-AA2L2RHH.js +2 -0
- package/dist/bundle/chunks/chunk-DKBJM22C.js +5 -0
- package/dist/bundle/chunks/{chunk-Z32CYNHC.js → chunk-F4YDU66P.js} +1 -1
- package/dist/bundle/chunks/chunk-FC2YWHEC.js +6 -0
- package/dist/bundle/chunks/{chunk-T2EE5G4S.js → chunk-FRFGJC3I.js} +1 -1
- package/dist/bundle/chunks/{chunk-57RKQONO.js → chunk-GBAS4FFM.js} +1 -1
- package/dist/bundle/chunks/{chunk-FDCYQYBP.js → chunk-JVLIKUBL.js} +1 -1
- package/dist/bundle/chunks/{chunk-EKDY4H2J.js → chunk-JXCHRJA7.js} +1 -1
- package/dist/bundle/chunks/{chunk-YID5W42N.js → chunk-KXTWMPCP.js} +1 -1
- package/dist/bundle/chunks/{chunk-ADNKYKU4.js → chunk-RYQ2FFF5.js} +1 -1
- package/dist/bundle/chunks/chunk-SOMFLLPD.js +3 -0
- package/dist/bundle/chunks/{chunk-4CJJ4SK6.js → chunk-SOWFP3I7.js} +1 -1
- package/dist/bundle/chunks/{chunk-GX7MTG25.js → chunk-TK6RKAB4.js} +4 -4
- package/dist/bundle/chunks/{chunk-NN44F5QF.js → chunk-V6RY5A6T.js} +1 -1
- package/dist/bundle/chunks/{chunk-77PVTNLJ.js → chunk-W557SQYL.js} +2 -2
- package/dist/bundle/chunks/{chunk-F4ZU4RBE.js → chunk-YIRQJCPA.js} +45 -46
- package/dist/bundle/chunks/cli-main-JCQTFUHO.js +3 -0
- package/dist/bundle/chunks/cloudflare-workers-ai-system-one-4QEQ7MGK.js +1 -1
- package/dist/bundle/chunks/{dispatch-R37L3FW5.js → dispatch-YPCDRQYI.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-4UOGH7RT.js → google-generative-ai-3SKUSCIL.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-2A6KZ5TX.js → google-vertex-YHSIIE4G.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-Z5JGXNCS.js → help-fast-path-TUCMQOOF.js} +1 -1
- package/dist/bundle/chunks/{host-command-IP3QAZYI.js → host-command-WEOS6VD6.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-FRQK5GAC.js → host-lifecycle-ZDCO62Z4.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +6 -6
- package/dist/bundle/chunks/host-session-command-IGTNZ4F6.js +20 -0
- package/dist/bundle/chunks/interactive-mode-5KYT7LMW.js +2 -0
- package/dist/bundle/chunks/{multi-session-host-EMI54PTJ.js → multi-session-host-25XQFXJC.js} +5 -5
- package/dist/bundle/chunks/openai-codex-responses-4BIPACKV.js +10 -0
- package/dist/bundle/chunks/{openai-completions-AGTH2RRA.js → openai-completions-NHDMDAS7.js} +1 -1
- package/dist/bundle/chunks/{openai-images-5UGZHY27.js → openai-images-PNXQDBSP.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-KGXDGHPI.js → openai-responses-2UOPEJLP.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-TK44VHGK.js → package-manager-cli-CF53LR5X.js} +1 -1
- package/dist/bundle/chunks/{role-F6IEUF2N.js → role-OK7RKBGF.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-XX2NXYL3.js → rotation-stream-QRRJ7G5Y.js} +1 -1
- package/dist/bundle/chunks/rpc-mode-LOODTK3J.js +2 -0
- package/dist/bundle/chunks/{schedule-command-DYHRFIX2.js → schedule-command-LKL6DLOT.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-MLANJVB2.js +3 -0
- package/dist/bundle/chunks/session-picker-RQDVWJRL.js +2 -0
- package/dist/bundle/chunks/session-worker.js +240 -240
- package/dist/bundle/chunks/session-write-fence-66EMOO4Y.js +2 -0
- package/dist/bundle/chunks/typesafe-system-one-W2ZTYF7X.js +1 -1
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.js +6 -6
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session.d.ts +0 -2
- package/dist/core/agent-session.js +4 -13
- package/dist/core/credential-pool/classify.d.ts +7 -0
- package/dist/core/credential-pool/classify.js +12 -4
- package/dist/core/credential-pool/failover.js +16 -1
- package/dist/core/credential-pool/rotation-stream.js +2 -0
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +8 -62
- package/dist/core/extensions/builtin/anthropic-subscription/context-window.d.ts +25 -0
- package/dist/core/extensions/builtin/anthropic-subscription/context-window.js +38 -0
- package/dist/core/extensions/builtin/anthropic-subscription/index.d.ts +2 -2
- package/dist/core/extensions/builtin/anthropic-subscription/index.js +4 -6
- package/dist/core/extensions/builtin/anthropic-subscription/long-context-retry.d.ts +18 -0
- package/dist/core/extensions/builtin/anthropic-subscription/long-context-retry.js +74 -0
- package/dist/core/extensions/builtin/anthropic-subscription/long-context.js +13 -5
- package/dist/core/extensions/builtin/anthropic-subscription/options.d.ts +0 -10
- package/dist/core/extensions/builtin/anthropic-subscription/options.js +8 -16
- package/dist/core/extensions/builtin/anthropic-subscription/session-registry-wiring.js +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/session-sync.js +3 -0
- package/dist/core/extensions/builtin/terminal/lease-file.d.ts +1 -0
- package/dist/core/extensions/builtin/terminal/lease-file.js +5 -3
- package/dist/core/foreign-session-holders.d.ts +2 -3
- package/dist/core/provider-failure.d.ts +2 -0
- package/dist/core/provider-failure.js +3 -0
- package/dist/core/reserved-keywords.js +1 -1
- package/dist/core/session-hold-errors.d.ts +2 -0
- package/dist/core/session-holder-records.d.ts +21 -0
- package/dist/core/session-holder-records.js +76 -0
- package/dist/core/session-holders.d.ts +3 -3
- package/dist/core/session-holders.js +4 -41
- package/dist/core/session-manager.d.ts +1 -0
- package/dist/core/session-manager.js +8 -1
- package/dist/core/session-write-fence.d.ts +18 -0
- package/dist/core/session-write-fence.js +71 -0
- package/dist/core/user-text-blocks.d.ts +11 -0
- package/dist/core/user-text-blocks.js +19 -1
- package/dist/modes/rpc/host-daemon-registration.js +2 -1
- package/dist/modes/rpc/host-generations.d.ts +6 -0
- package/dist/modes/rpc/host-generations.js +35 -4
- package/dist/modes/rpc/host-lifecycle-scratch.d.ts +9 -2
- package/dist/modes/rpc/host-lifecycle-scratch.js +26 -6
- package/dist/modes/rpc/host-lifecycle.js +1 -1
- package/dist/modes/rpc/host-reservations.d.ts +5 -0
- package/dist/modes/rpc/host-reservations.js +54 -12
- package/dist/modes/rpc/host-stalled-evidence.d.ts +27 -1
- package/dist/modes/rpc/host-stalled-evidence.js +52 -4
- package/dist/modes/rpc/multi-session-host.js +10 -1
- package/dist/modes/rpc/session-command-router.js +2 -0
- package/dist/modes/rpc/session-held.js +2 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-74V6LCWU.js +0 -2
- package/dist/bundle/chunks/chunk-DDQJSVIZ.js +0 -5
- package/dist/bundle/chunks/chunk-DY2EM44T.js +0 -395
- package/dist/bundle/chunks/chunk-HOXW5IHD.js +0 -6
- package/dist/bundle/chunks/chunk-UAQW2T7G.js +0 -2
- package/dist/bundle/chunks/cli-main-RXZVFXK7.js +0 -3
- package/dist/bundle/chunks/host-session-command-KGCQPMES.js +0 -20
- package/dist/bundle/chunks/interactive-mode-DQOPIZSI.js +0 -2
- package/dist/bundle/chunks/openai-codex-responses-I2NSK6AD.js +0 -10
- package/dist/bundle/chunks/rpc-mode-X6J3FC3N.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-XN4BAZTU.js +0 -3
- package/dist/bundle/chunks/session-picker-US5YPKC7.js +0 -2
- /package/dist/bundle/chunks/{chunk-KW6654UH.js → chunk-RPAHOJ6F.js} +0 -0
|
@@ -2,25 +2,11 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { CONFIG_DIR_NAME, getAgentDir } from "../../../../config.js";
|
|
4
4
|
import { PROJECT_RULES_END_MARKER, PROJECT_RULES_HEADING, PROJECT_RULES_REGION_END_MARKER, PROJECT_RULES_REGION_START_MARKER, PROJECT_RULES_START_MARKER, } from "../rules/rules/constants.js";
|
|
5
|
+
import { plannedContextWindow, sdkModelId } from "./context-window.js";
|
|
5
6
|
import { presetAppendDeprecationGuidance } from "./guidance.js";
|
|
6
|
-
import { longContextAvailable } from "./long-context.js";
|
|
7
7
|
import { loadAnthropicSubscriptionProviderSettingsFromDisk, resolveCompactionOwner, resolveSystemPromptMode, } from "./settings.js";
|
|
8
8
|
import { loadOverrideSystemPrompt, resolveCustomSystemPrompt } from "./system-prompt.js";
|
|
9
9
|
import { BUILTIN_SDK_TOOLS, canUseTool, HOST_TOOL_DENIAL_HOOKS } from "./tools.js";
|
|
10
|
-
/** Claude Code's default window when a request does not ask for the long context (its `A68`). */
|
|
11
|
-
export const CLAUDE_CODE_DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
12
|
-
/**
|
|
13
|
-
* The model id Claude Code is given. A subscription (OAuth) session ignores `options.betas` ("Custom
|
|
14
|
-
* betas are only available for API key users"); the `[1m]` suffix is Claude Code's own way to ask for
|
|
15
|
-
* the 1M window, and it then sends `context-1m-2025-08-07` on the wire. A model above the default
|
|
16
|
-
* window gets the suffix while the account has not refused it, so the window senpi plans against is
|
|
17
|
-
* the window the request gets (senpi#3408).
|
|
18
|
-
*/
|
|
19
|
-
export function sdkModelId(model) {
|
|
20
|
-
return model.contextWindow > CLAUDE_CODE_DEFAULT_CONTEXT_WINDOW && longContextAvailable()
|
|
21
|
-
? `${model.id}[1m]`
|
|
22
|
-
: model.id;
|
|
23
|
-
}
|
|
24
10
|
const ADAPTIVE_THINKING_MODEL_MARKERS = [
|
|
25
11
|
"opus-4-6",
|
|
26
12
|
"opus-4.6",
|
|
@@ -241,8 +227,14 @@ export function buildAnthropicSubscriptionQueryOptions(input) {
|
|
|
241
227
|
// Claude Code's per-turn token-budget reminder changes the prompt every turn and defeats
|
|
242
228
|
// prompt-cache reuse; senpi's compaction carries its own budget reminder. The auth lane
|
|
243
229
|
// layers this over the subprocess environment it builds.
|
|
230
|
+
// With DISABLE_COMPACT set, Claude Code takes CLAUDE_CODE_MAX_CONTEXT_TOKENS over every model rule of
|
|
231
|
+
// its own, so it plans the window senpi plans (senpi#3801), and its native compaction stays off.
|
|
244
232
|
if (senpiOwnsCompaction)
|
|
245
|
-
queryOptions.env = {
|
|
233
|
+
queryOptions.env = {
|
|
234
|
+
CLAUDE_CODE_TOTAL_TOKENS_REMINDER: "off",
|
|
235
|
+
CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(plannedContextWindow(input.model)),
|
|
236
|
+
DISABLE_COMPACT: "1",
|
|
237
|
+
};
|
|
246
238
|
if (input.pathToClaudeCodeExecutable)
|
|
247
239
|
queryOptions.pathToClaudeCodeExecutable = input.pathToClaudeCodeExecutable;
|
|
248
240
|
if (toolLessRequest)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { convertToLlm } from "../../../messages.js";
|
|
2
2
|
import { ANTHROPIC_SUBSCRIPTION_PROVIDER_ID } from "./account-management.js";
|
|
3
3
|
import { restoreColdSeedCalibration } from "./cold-seed-budget.js";
|
|
4
|
-
import { sdkModelId } from "./
|
|
4
|
+
import { sdkModelId } from "./context-window.js";
|
|
5
5
|
import { BINDING_ENTRY_TYPE, BINDING_MARKER, bindingFromStoredBranch, invalidationReasonFromBranch, storedBindingFromBinding, storedBindingFromEntry, } from "./session-binding.js";
|
|
6
6
|
import { deleteStoredBinding, readStoredBinding, writeStoredBinding } from "./session-binding-store.js";
|
|
7
7
|
import { AssistantCommitBoundary, assistantContentHash, isResidentAssistant, isTerminalFailure, } from "./session-commit-boundary.js";
|
|
@@ -117,6 +117,9 @@ export function configFingerprint(options, context, authLane, accountName) {
|
|
|
117
117
|
// A refused 1M window must restart the resident process at the standard one (senpi#3408). Only
|
|
118
118
|
// the refusal state is hashed: a live model switch reaches the process through setModel.
|
|
119
119
|
longContextRefused: !longContextAvailable(),
|
|
120
|
+
// Claude Code reads its window once at spawn, so a switch to a model planned at another window
|
|
121
|
+
// restarts the resident process at that window instead of keeping the old one (senpi#3801).
|
|
122
|
+
contextWindow: options.env?.CLAUDE_CODE_MAX_CONTEXT_TOKENS,
|
|
120
123
|
extraArgs: options.extraArgs,
|
|
121
124
|
pathToClaudeCodeExecutable: options.pathToClaudeCodeExecutable,
|
|
122
125
|
includePartialMessages: options.includePartialMessages,
|
|
@@ -131,9 +131,11 @@ export async function reclaimLockState(lock, probes = {}) {
|
|
|
131
131
|
if (!(probes.isProcessAlive ?? pidAlive)(holder.pid))
|
|
132
132
|
return { state: "stale", raw };
|
|
133
133
|
const observed = await (probes.readProcessStartMs ?? defaultReadProcessStartMs)(holder.pid).catch(() => undefined);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
// An unreadable start time cannot tell the holder from a process wearing its reused pid: held, but
|
|
135
|
+
// `unproven`, so a caller with positive evidence of the holder's death may still reclaim it.
|
|
136
|
+
if (observed === undefined)
|
|
137
|
+
return { state: "held", holder, unproven: true };
|
|
138
|
+
return sameProcessStart(holder.processStartedAtMs, observed) ? { state: "held", holder } : { state: "stale", raw };
|
|
137
139
|
}
|
|
138
140
|
/** Remove a stale lock only if it still holds `raw`; a lock someone took meanwhile is put back. */
|
|
139
141
|
export async function breakStaleLock(lock, raw) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type SessionHolder } from "./session-holders.ts";
|
|
1
|
+
import { type SessionHolder, type SessionHolderProbes } from "./session-holders.ts";
|
|
3
2
|
/** Fresh lease evidence for each admission; a clean exit or stale identity clears the next retry. */
|
|
4
|
-
export declare function foreignSessionHolders(sessionFile: string | undefined, sessionId?: string, probes?:
|
|
3
|
+
export declare function foreignSessionHolders(sessionFile: string | undefined, sessionId?: string, probes?: SessionHolderProbes): Promise<SessionHolder[]>;
|
|
5
4
|
/** One terminal-safe line; cwd can contain newlines or escape sequences. */
|
|
6
5
|
export declare function sessionHolderWarning(sessionFile: string | undefined, sessionId?: string): Promise<string | undefined>;
|
|
7
6
|
//# sourceMappingURL=foreign-session-holders.d.ts.map
|
|
@@ -10,6 +10,8 @@ export interface ProviderFailure {
|
|
|
10
10
|
readonly provider: string;
|
|
11
11
|
readonly httpStatus?: number;
|
|
12
12
|
readonly code?: string;
|
|
13
|
+
/** When the limit lifts (epoch ms), for a `quota` or `rate_limit` failure whose provider named it. */
|
|
14
|
+
readonly resetsAt?: number;
|
|
13
15
|
readonly reason: string;
|
|
14
16
|
readonly action: string;
|
|
15
17
|
}
|
|
@@ -26,12 +26,15 @@ export function describeProviderFailure(diagnostic, provider) {
|
|
|
26
26
|
action: "retry once; if it repeats, switch the model or report the code",
|
|
27
27
|
}
|
|
28
28
|
: ACTIONS[category];
|
|
29
|
+
const resetsAt = diagnostic?.resetsAt;
|
|
29
30
|
return {
|
|
30
31
|
category,
|
|
31
32
|
provider,
|
|
32
33
|
...(diagnostic?.httpStatus === undefined ? {} : { httpStatus: diagnostic.httpStatus }),
|
|
33
34
|
...(diagnostic?.code === undefined ? {} : { code: diagnostic.code }),
|
|
35
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
34
36
|
...detail,
|
|
37
|
+
...(resetsAt === undefined ? {} : { reason: `${detail.reason} until ${new Date(resetsAt).toISOString()}` }),
|
|
35
38
|
};
|
|
36
39
|
}
|
|
37
40
|
/**
|
|
@@ -5,7 +5,7 @@ const NOT_AFTER_IDENTIFIER = String.raw `(?<![A-Za-z0-9][-_.]|[/\\])`;
|
|
|
5
5
|
const NOT_BEFORE_PATH = String.raw `(?![-_.]?[A-Za-z0-9]*[/\\])`;
|
|
6
6
|
// The name must end its own word (`ulw-plan-refactor`, `ulw-plan.md` do not count); a chained
|
|
7
7
|
// skill word may follow if it, too, ends there (`mass ulw-loop`).
|
|
8
|
-
const SEGMENT_END =
|
|
8
|
+
const SEGMENT_END = "(?![-_.]?[A-Za-z0-9])";
|
|
9
9
|
const NOT_INTO_IDENTIFIER = String.raw `(?${SEGMENT_END})[A-Za-z0-9]|\.[A-Za-z0-9])`;
|
|
10
10
|
const keywordPattern = (body) => new RegExp(String.raw `${NOT_AFTER_IDENTIFIER}\b${body}\b${NOT_INTO_IDENTIFIER}${NOT_BEFORE_PATH}`, "gi");
|
|
11
11
|
const RESERVED_KEYWORD_PATTERNS = [
|
|
@@ -8,6 +8,8 @@ export interface SessionHolder {
|
|
|
8
8
|
export interface SessionHolderEndpoint {
|
|
9
9
|
readonly host_socket?: string;
|
|
10
10
|
readonly host_instance: string;
|
|
11
|
+
/** The daemon directory of that generation; kept in the holder record only, never reported. */
|
|
12
|
+
readonly host_daemon_dir?: string;
|
|
11
13
|
}
|
|
12
14
|
export declare class SessionHeldError extends Error {
|
|
13
15
|
readonly code: "ESESSIONHELD";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type LockProbes } from "./extensions/builtin/terminal/lease-file.ts";
|
|
2
|
+
import type { SessionHolder } from "./session-hold-errors.ts";
|
|
3
|
+
/** The host generation a holder record names, as written by `holdSessionFile`. */
|
|
4
|
+
export interface HolderGeneration {
|
|
5
|
+
readonly host_instance?: string;
|
|
6
|
+
readonly host_daemon_dir?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SessionHolderProbes extends LockProbes {
|
|
9
|
+
/**
|
|
10
|
+
* Positive evidence that the holder's host generation has ended. Asked only about a holder whose pid
|
|
11
|
+
* is alive but whose start time cannot be compared, so a reused pid is not mistaken for it forever.
|
|
12
|
+
*/
|
|
13
|
+
readonly holderEnded?: (generation: HolderGeneration) => Promise<boolean>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `fenceSession` advances the session file's write fence (senpi#3652). A holder released on `holderEnded` may only be
|
|
17
|
+
* frozen, so it is released only after the fence moved: if it resumes, its writes are refused. Without a fence, or when
|
|
18
|
+
* it cannot be written, the holder stands.
|
|
19
|
+
*/
|
|
20
|
+
export declare function liveHolders(dir: string, probes?: SessionHolderProbes, fenceSession?: () => Promise<void>): Promise<SessionHolder[]>;
|
|
21
|
+
//# sourceMappingURL=session-holder-records.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the holder records of one session: which processes hold it right now. A record of a dead or
|
|
3
|
+
* reused pid is reclaimed on the way, and so is one whose pid is alive but unprovable once the caller
|
|
4
|
+
* has positive evidence that its host generation ended (senpi#3658).
|
|
5
|
+
*/
|
|
6
|
+
import { readdir } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { breakStaleLock, readLeaseText, reclaimLockState, } from "./extensions/builtin/terminal/lease-file.js";
|
|
9
|
+
const HOLDER_RECORD = /^\d+\.json$/;
|
|
10
|
+
function parseCwd(raw) {
|
|
11
|
+
const parsed = JSON.parse(raw);
|
|
12
|
+
if (typeof parsed !== "object" || parsed === null || !("cwd" in parsed))
|
|
13
|
+
return undefined;
|
|
14
|
+
return typeof parsed.cwd === "string" ? parsed.cwd : undefined;
|
|
15
|
+
}
|
|
16
|
+
async function fenced(fenceSession) {
|
|
17
|
+
if (fenceSession === undefined)
|
|
18
|
+
return false;
|
|
19
|
+
return fenceSession().then(() => true, () => false);
|
|
20
|
+
}
|
|
21
|
+
function holderGeneration(parsed) {
|
|
22
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
23
|
+
return {};
|
|
24
|
+
return {
|
|
25
|
+
...("host_instance" in parsed && typeof parsed.host_instance === "string"
|
|
26
|
+
? { host_instance: parsed.host_instance }
|
|
27
|
+
: {}),
|
|
28
|
+
...("host_daemon_dir" in parsed && typeof parsed.host_daemon_dir === "string"
|
|
29
|
+
? { host_daemon_dir: parsed.host_daemon_dir }
|
|
30
|
+
: {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* `fenceSession` advances the session file's write fence (senpi#3652). A holder released on `holderEnded` may only be
|
|
35
|
+
* frozen, so it is released only after the fence moved: if it resumes, its writes are refused. Without a fence, or when
|
|
36
|
+
* it cannot be written, the holder stands.
|
|
37
|
+
*/
|
|
38
|
+
export async function liveHolders(dir, probes = {}, fenceSession) {
|
|
39
|
+
const holders = [];
|
|
40
|
+
for (const name of await readdir(dir)) {
|
|
41
|
+
if (!HOLDER_RECORD.test(name))
|
|
42
|
+
continue;
|
|
43
|
+
const path = join(dir, name);
|
|
44
|
+
const before = await readLeaseText(path);
|
|
45
|
+
const state = await reclaimLockState(path, probes);
|
|
46
|
+
if (state.state === "stale")
|
|
47
|
+
await breakStaleLock(path, state.raw);
|
|
48
|
+
if (state.state !== "held" || state.holder === undefined)
|
|
49
|
+
continue;
|
|
50
|
+
const raw = await readLeaseText(path);
|
|
51
|
+
// Never publish endpoint labels from a record replaced during the identity probe.
|
|
52
|
+
const stable = raw !== undefined && raw === before;
|
|
53
|
+
const parsed = stable ? JSON.parse(raw) : undefined;
|
|
54
|
+
if (stable &&
|
|
55
|
+
state.unproven === true &&
|
|
56
|
+
(await probes.holderEnded?.(holderGeneration(parsed))) === true &&
|
|
57
|
+
(await fenced(fenceSession))) {
|
|
58
|
+
await breakStaleLock(path, raw);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const endpoint = typeof parsed === "object" &&
|
|
62
|
+
parsed !== null &&
|
|
63
|
+
"host_instance" in parsed &&
|
|
64
|
+
typeof parsed.host_instance === "string"
|
|
65
|
+
? {
|
|
66
|
+
host_instance: parsed.host_instance,
|
|
67
|
+
...("host_socket" in parsed && typeof parsed.host_socket === "string"
|
|
68
|
+
? { host_socket: parsed.host_socket }
|
|
69
|
+
: {}),
|
|
70
|
+
}
|
|
71
|
+
: {};
|
|
72
|
+
holders.push({ pid: state.holder.pid, cwd: stable ? parseCwd(raw) : undefined, ...endpoint });
|
|
73
|
+
}
|
|
74
|
+
return holders;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=session-holder-records.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type LockProbes } from "./extensions/builtin/terminal/lease-file.ts";
|
|
2
1
|
import { SessionHeldError, type SessionHolder, type SessionHolderEndpoint, SessionMovedError, SessionMovingError } from "./session-hold-errors.ts";
|
|
3
|
-
|
|
2
|
+
import { type HolderGeneration, type SessionHolderProbes } from "./session-holder-records.ts";
|
|
3
|
+
export { type HolderGeneration, SessionHeldError, type SessionHolder, type SessionHolderProbes, SessionMovedError, SessionMovingError, };
|
|
4
4
|
export interface SessionHold {
|
|
5
5
|
release(): void;
|
|
6
6
|
}
|
|
@@ -18,7 +18,7 @@ export declare function holdSessionFile(sessionFile: string, sessionId: string,
|
|
|
18
18
|
* Live processes that have `sessionFile` open right now (stale records of dead or reused pids are
|
|
19
19
|
* reclaimed on the way). A caller about to start another writer uses it to wait instead of racing one.
|
|
20
20
|
*/
|
|
21
|
-
export declare function liveSessionHolders(sessionFile: string, sessionId: string, probes?:
|
|
21
|
+
export declare function liveSessionHolders(sessionFile: string, sessionId: string, probes?: SessionHolderProbes): Promise<SessionHolder[]>;
|
|
22
22
|
/**
|
|
23
23
|
* Runs `move` holding the session's move lock, after proving no live process holds the session.
|
|
24
24
|
* Waits up to `waitMs` for a concurrent move to finish, then fails with `SessionMovingError`.
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import { readdir } from "node:fs/promises";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
5
4
|
import { resolveMovedPath } from "./extensions/builtin/moved-path-guard/resolve.js";
|
|
6
5
|
import { breakStaleLock, errorCode, publishExclusive, readLeaseText, reclaimLockState, unlinkIfPresent, } from "./extensions/builtin/terminal/lease-file.js";
|
|
7
6
|
import { ownProcessStartedAtMs, processBootAtMs } from "./extensions/builtin/terminal/process-identity.js";
|
|
8
7
|
import { SessionHeldError, SessionMovedError, SessionMovingError, } from "./session-hold-errors.js";
|
|
8
|
+
import { liveHolders } from "./session-holder-records.js";
|
|
9
9
|
import { encodedSessionId } from "./session-sidecar-store.js";
|
|
10
|
-
|
|
10
|
+
import { advanceSessionFence } from "./session-write-fence.js";
|
|
11
|
+
export { SessionHeldError, SessionMovedError, SessionMovingError, };
|
|
11
12
|
/**
|
|
12
13
|
* Cross-process ownership of a session file. Every process that has a session open publishes a holder
|
|
13
14
|
* record `<sessionDir>/session-holders/<id>/<pid>.json` carrying its process identity; moving the session
|
|
@@ -19,7 +20,6 @@ export { SessionHeldError, SessionMovedError, SessionMovingError };
|
|
|
19
20
|
*/
|
|
20
21
|
const HOLDERS_DIR = "session-holders";
|
|
21
22
|
const MOVE_LOCK = "move.lock";
|
|
22
|
-
const HOLDER_RECORD = /^\d+\.json$/;
|
|
23
23
|
// A move rewrites one file and a few sidecars; a lock older than this belongs to a mover that died.
|
|
24
24
|
const MOVE_LOCK_STALE_MS = 30_000;
|
|
25
25
|
const DEFAULT_MOVE_LOCK_WAIT_MS = 5_000;
|
|
@@ -108,12 +108,6 @@ function parsePid(raw) {
|
|
|
108
108
|
throw error;
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
function parseCwd(raw) {
|
|
112
|
-
const parsed = JSON.parse(raw);
|
|
113
|
-
if (typeof parsed !== "object" || parsed === null || !("cwd" in parsed))
|
|
114
|
-
return undefined;
|
|
115
|
-
return typeof parsed.cwd === "string" ? parsed.cwd : undefined;
|
|
116
|
-
}
|
|
117
111
|
/**
|
|
118
112
|
* Publishes this process as a holder of `sessionFile` until `release()`. Throws `SessionMovingError`
|
|
119
113
|
* while another process is moving the session, and `SessionMovedError` when a session that was read
|
|
@@ -169,44 +163,13 @@ function publishHolderRecord(dir, record, content) {
|
|
|
169
163
|
}
|
|
170
164
|
}
|
|
171
165
|
}
|
|
172
|
-
async function liveHolders(dir, probes = {}) {
|
|
173
|
-
const holders = [];
|
|
174
|
-
for (const name of await readdir(dir)) {
|
|
175
|
-
if (!HOLDER_RECORD.test(name))
|
|
176
|
-
continue;
|
|
177
|
-
const path = join(dir, name);
|
|
178
|
-
const before = await readLeaseText(path);
|
|
179
|
-
const state = await reclaimLockState(path, probes);
|
|
180
|
-
if (state.state === "stale")
|
|
181
|
-
await breakStaleLock(path, state.raw);
|
|
182
|
-
if (state.state !== "held" || state.holder === undefined)
|
|
183
|
-
continue;
|
|
184
|
-
const raw = await readLeaseText(path);
|
|
185
|
-
// Never publish endpoint labels from a record replaced during the identity probe.
|
|
186
|
-
const stable = raw !== undefined && raw === before;
|
|
187
|
-
const parsed = stable ? JSON.parse(raw) : undefined;
|
|
188
|
-
const endpoint = typeof parsed === "object" &&
|
|
189
|
-
parsed !== null &&
|
|
190
|
-
"host_instance" in parsed &&
|
|
191
|
-
typeof parsed.host_instance === "string"
|
|
192
|
-
? {
|
|
193
|
-
host_instance: parsed.host_instance,
|
|
194
|
-
...("host_socket" in parsed && typeof parsed.host_socket === "string"
|
|
195
|
-
? { host_socket: parsed.host_socket }
|
|
196
|
-
: {}),
|
|
197
|
-
}
|
|
198
|
-
: {};
|
|
199
|
-
holders.push({ pid: state.holder.pid, cwd: stable ? parseCwd(raw) : undefined, ...endpoint });
|
|
200
|
-
}
|
|
201
|
-
return holders;
|
|
202
|
-
}
|
|
203
166
|
/**
|
|
204
167
|
* Live processes that have `sessionFile` open right now (stale records of dead or reused pids are
|
|
205
168
|
* reclaimed on the way). A caller about to start another writer uses it to wait instead of racing one.
|
|
206
169
|
*/
|
|
207
170
|
export async function liveSessionHolders(sessionFile, sessionId, probes = {}) {
|
|
208
171
|
try {
|
|
209
|
-
return await liveHolders(holdersDir(sessionFile, sessionId), probes);
|
|
172
|
+
return await liveHolders(holdersDir(sessionFile, sessionId), probes, () => advanceSessionFence(sessionFile));
|
|
210
173
|
}
|
|
211
174
|
catch (error) {
|
|
212
175
|
if (errorCode(error) === "ENOENT")
|
|
@@ -18,6 +18,7 @@ import { HistoryReductions } from "./session-history-reduce.js";
|
|
|
18
18
|
import { planLoadedTail } from "./session-loaded-tail.js";
|
|
19
19
|
import { SessionWriteGuard } from "./session-move/write-guard.js";
|
|
20
20
|
import { ResidentStringStore } from "./session-resident-store.js";
|
|
21
|
+
import { SessionWriteFence } from "./session-write-fence.js";
|
|
21
22
|
import { discardFailedFirstFlush, discardFailedFirstFlushAsync, truncateToLastCompleteLine, } from "./session-write-recovery.js";
|
|
22
23
|
import { hasOtherLiveSessionWriter, registerSessionWriter, reserveSessionWrite, unregisterSessionWriter, } from "./session-write-reservation.js";
|
|
23
24
|
import { isVirtualModel } from "./virtual-models.js";
|
|
@@ -680,6 +681,7 @@ export class SessionManager {
|
|
|
680
681
|
constructor(cwd, sessionDir, sessionFile, persist, newSessionOptions, preloadedFileEntries) {
|
|
681
682
|
this.sessionId = "";
|
|
682
683
|
this.writeGuard = new SessionWriteGuard();
|
|
684
|
+
this.writeFence = new SessionWriteFence();
|
|
683
685
|
this.flushed = false;
|
|
684
686
|
// Set when an append to the flushed file failed and may have left a partial last line.
|
|
685
687
|
this.tailMayBeTorn = false;
|
|
@@ -839,8 +841,10 @@ export class SessionManager {
|
|
|
839
841
|
/** Points this manager at `sessionFile`; the path when it exists, undefined when a new session was started there. */
|
|
840
842
|
_beginSessionFile(sessionFile, newSessionOptions) {
|
|
841
843
|
const resolvedFile = resolvePath(sessionFile);
|
|
842
|
-
if (this.persist)
|
|
844
|
+
if (this.persist) {
|
|
843
845
|
reserveSessionWrite(resolvedFile);
|
|
846
|
+
this.writeFence.opened(resolvedFile);
|
|
847
|
+
}
|
|
844
848
|
this.sessionFile = resolvedFile;
|
|
845
849
|
this.mirrorTrimmed = false;
|
|
846
850
|
this.trimmedIds.clear();
|
|
@@ -1056,6 +1060,7 @@ export class SessionManager {
|
|
|
1056
1060
|
if (!this.persist || !this.sessionFile)
|
|
1057
1061
|
return;
|
|
1058
1062
|
this.writeGuard.assertRewritable(this.fileEntries, this.sessionFile);
|
|
1063
|
+
this.writeFence.assertOwned(this.sessionFile);
|
|
1059
1064
|
reserveSessionWrite(this.sessionFile);
|
|
1060
1065
|
replaceFileAtomically(this.sessionFile, this._serializedFileEntries());
|
|
1061
1066
|
}
|
|
@@ -1134,6 +1139,7 @@ export class SessionManager {
|
|
|
1134
1139
|
return true;
|
|
1135
1140
|
}
|
|
1136
1141
|
async _writeHeaderAsync(sessionFile) {
|
|
1142
|
+
this.writeFence.assertOwned(sessionFile);
|
|
1137
1143
|
reserveSessionWrite(sessionFile);
|
|
1138
1144
|
const entries = this.fileEntries;
|
|
1139
1145
|
const serialize = (batch) => batch.map((e) => `${JSON.stringify(this.residentStore.materialize(e))}\n`).join("");
|
|
@@ -1185,6 +1191,7 @@ export class SessionManager {
|
|
|
1185
1191
|
if (!this.persist || !this.sessionFile)
|
|
1186
1192
|
return;
|
|
1187
1193
|
this.writeGuard.assertWritable(this.fileEntries, entry, this.sessionFile);
|
|
1194
|
+
this.writeFence.assertOwned(this.sessionFile);
|
|
1188
1195
|
reserveSessionWrite(this.sessionFile);
|
|
1189
1196
|
const persistedEntry = this.residentStore.materialize(entry);
|
|
1190
1197
|
if (this.flushed) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function sessionFenceFile(sessionFile: string): string;
|
|
2
|
+
/** The fence token now on disk; empty when the file was never taken over. Any other read failure throws. */
|
|
3
|
+
export declare function readSessionFence(sessionFile: string): string;
|
|
4
|
+
/** Gives the file a new token, so every writer that opened it before this call refuses its next write. */
|
|
5
|
+
export declare function advanceSessionFence(sessionFile: string): Promise<void>;
|
|
6
|
+
/** One writer's view of its file's fence: the token seen when the file was opened, re-checked before every write. */
|
|
7
|
+
export declare class SessionWriteFence {
|
|
8
|
+
private seen;
|
|
9
|
+
private fencedFile;
|
|
10
|
+
opened(sessionFile: string): void;
|
|
11
|
+
assertOwned(sessionFile: string): void;
|
|
12
|
+
}
|
|
13
|
+
export declare class SessionFencedError extends Error {
|
|
14
|
+
readonly code = "session_fenced";
|
|
15
|
+
readonly sessionFile: string;
|
|
16
|
+
constructor(sessionFile: string);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=session-write-fence.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write fence of one session file (senpi#3652).
|
|
3
|
+
*
|
|
4
|
+
* A host may take a session file over from another generation whose claim it could not prove alive, for example a
|
|
5
|
+
* Windows generation that stopped heartbeating while an unrelated process wears its recycled pid. That verdict can be
|
|
6
|
+
* wrong: a generation whose event loop froze, or whose machine slept, also stops heartbeating, and it would resume
|
|
7
|
+
* writing the same file. The fence makes that second writer impossible instead of unlikely. A takeover replaces the
|
|
8
|
+
* token in `<session file>.fence` before the new owner opens the file; every writer remembers the token it saw when it
|
|
9
|
+
* opened the file and re-reads it, synchronously, right before each write. A writer that finds another token refuses
|
|
10
|
+
* the write and every later one: the file is no longer its own.
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { rename, rm, writeFile } from "node:fs/promises";
|
|
15
|
+
export function sessionFenceFile(sessionFile) {
|
|
16
|
+
return `${sessionFile}.fence`;
|
|
17
|
+
}
|
|
18
|
+
/** The fence token now on disk; empty when the file was never taken over. Any other read failure throws. */
|
|
19
|
+
export function readSessionFence(sessionFile) {
|
|
20
|
+
try {
|
|
21
|
+
return readFileSync(sessionFenceFile(sessionFile), "utf8").trim();
|
|
22
|
+
}
|
|
23
|
+
catch (cause) {
|
|
24
|
+
if (cause.code === "ENOENT")
|
|
25
|
+
return "";
|
|
26
|
+
throw cause;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Gives the file a new token, so every writer that opened it before this call refuses its next write. */
|
|
30
|
+
export async function advanceSessionFence(sessionFile) {
|
|
31
|
+
const file = sessionFenceFile(sessionFile);
|
|
32
|
+
const temporary = `${file}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
33
|
+
try {
|
|
34
|
+
await writeFile(temporary, `${randomUUID()}\n`, { mode: 0o600 });
|
|
35
|
+
await rename(temporary, file);
|
|
36
|
+
}
|
|
37
|
+
catch (cause) {
|
|
38
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
39
|
+
throw cause;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** One writer's view of its file's fence: the token seen when the file was opened, re-checked before every write. */
|
|
43
|
+
export class SessionWriteFence {
|
|
44
|
+
opened(sessionFile) {
|
|
45
|
+
if (this.fencedFile !== sessionFile)
|
|
46
|
+
this.fencedFile = undefined;
|
|
47
|
+
this.seen = { file: sessionFile, token: readSessionFence(sessionFile) };
|
|
48
|
+
}
|
|
49
|
+
assertOwned(sessionFile) {
|
|
50
|
+
if (this.fencedFile === sessionFile)
|
|
51
|
+
throw new SessionFencedError(sessionFile);
|
|
52
|
+
// A file this writer starts itself (a new session, a fork) has no earlier owner to fence out.
|
|
53
|
+
if (this.seen?.file !== sessionFile) {
|
|
54
|
+
this.opened(sessionFile);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (readSessionFence(sessionFile) === this.seen.token)
|
|
58
|
+
return;
|
|
59
|
+
this.fencedFile = sessionFile;
|
|
60
|
+
throw new SessionFencedError(sessionFile);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export class SessionFencedError extends Error {
|
|
64
|
+
constructor(sessionFile) {
|
|
65
|
+
super(`${sessionFile} was taken over by another host generation; this writer no longer owns it and will not write to it`);
|
|
66
|
+
this.code = "session_fenced";
|
|
67
|
+
this.name = "SessionFencedError";
|
|
68
|
+
this.sessionFile = sessionFile;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=session-write-fence.js.map
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { TextContent } from "@earendil-works/pi-ai";
|
|
2
|
+
/** The one text a user message's text blocks spell: the blocks joined by a newline. */
|
|
3
|
+
export declare function joinUserTextBlocks(blocks: readonly string[]): string;
|
|
4
|
+
/**
|
|
5
|
+
* The text a queued user message is tracked by. The queue records it on enqueue and matches the
|
|
6
|
+
* started message's content against it on delivery, so both sides must read it here (senpi#3588):
|
|
7
|
+
* `userMessageText(userTextContent(text, textBlocks)) === text` for every queued `text`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function userMessageText(content: string | readonly {
|
|
10
|
+
type: string;
|
|
11
|
+
text?: string;
|
|
12
|
+
}[]): string;
|
|
2
13
|
/**
|
|
3
14
|
* The text content of a user message. Text that arrived as several blocks keeps them while they
|
|
4
15
|
* still spell the final text exactly (joined by a newline); text that an input handler or a
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { parseAskUserAnswerFrame } from "./extensions/builtin/ask-user/format.js";
|
|
2
|
+
/** The one text a user message's text blocks spell: the blocks joined by a newline. */
|
|
3
|
+
export function joinUserTextBlocks(blocks) {
|
|
4
|
+
return blocks.join("\n");
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The text a queued user message is tracked by. The queue records it on enqueue and matches the
|
|
8
|
+
* started message's content against it on delivery, so both sides must read it here (senpi#3588):
|
|
9
|
+
* `userMessageText(userTextContent(text, textBlocks)) === text` for every queued `text`.
|
|
10
|
+
*/
|
|
11
|
+
export function userMessageText(content) {
|
|
12
|
+
if (typeof content === "string")
|
|
13
|
+
return content;
|
|
14
|
+
const blocks = [];
|
|
15
|
+
for (const part of content)
|
|
16
|
+
if (part.type === "text" && part.text !== undefined)
|
|
17
|
+
blocks.push(part.text);
|
|
18
|
+
return joinUserTextBlocks(blocks);
|
|
19
|
+
}
|
|
2
20
|
/**
|
|
3
21
|
* The text content of a user message. Text that arrived as several blocks keeps them while they
|
|
4
22
|
* still spell the final text exactly (joined by a newline); text that an input handler or a
|
|
@@ -6,7 +24,7 @@ import { parseAskUserAnswerFrame } from "./extensions/builtin/ask-user/format.js
|
|
|
6
24
|
* words in separate blocks (senpi#2920).
|
|
7
25
|
*/
|
|
8
26
|
export function userTextContent(text, textBlocks) {
|
|
9
|
-
if (textBlocks !== undefined && textBlocks.length > 1 && textBlocks
|
|
27
|
+
if (textBlocks !== undefined && textBlocks.length > 1 && joinUserTextBlocks(textBlocks) === text) {
|
|
10
28
|
return textBlocks.map((block) => ({ type: "text", text: block }));
|
|
11
29
|
}
|
|
12
30
|
return [{ type: "text", text }];
|
|
@@ -26,7 +26,7 @@ import { rename, rm } from "node:fs/promises";
|
|
|
26
26
|
import { ProcessIdentityUnreadableError, parseDaemonPidFile, processIsLive, processMatchesPidFile, processStartTimeMs, readProcessStartTime, sameProcessStartMs, } from "../../utils/process-identity.js";
|
|
27
27
|
import { createGenerationDirectory, generationPaths, HOST_DAEMON_LAYOUT, sameEndpoint, } from "./host-daemon-paths.js";
|
|
28
28
|
import { isRecord, parseJson, readFileOrUndefined, writeStateFile } from "./host-daemon-state.js";
|
|
29
|
-
import { pruneDeadGenerations } from "./host-generations.js";
|
|
29
|
+
import { endClaimsOfEndedGenerations, pruneDeadGenerations } from "./host-generations.js";
|
|
30
30
|
import { hostChildAlive } from "./host-stalled-evidence.js";
|
|
31
31
|
import { logUnknownHostIdentity } from "./host-supervisor-log.js";
|
|
32
32
|
/**
|
|
@@ -112,6 +112,7 @@ export async function clearHostRegistration(paths) {
|
|
|
112
112
|
if (typeof pointer?.instance_id === "string") {
|
|
113
113
|
if (await hostChildAlive(generationPaths(paths, pointer.instance_id)))
|
|
114
114
|
throw new Error(`refusing to clear generation ${pointer.instance_id} with a live host child`);
|
|
115
|
+
await endClaimsOfEndedGenerations(paths, new Set([pointer.instance_id]));
|
|
115
116
|
await rm(generationPaths(paths, pointer.instance_id).dir, { recursive: true, force: true });
|
|
116
117
|
}
|
|
117
118
|
await rm(paths.settingsFile, { force: true });
|
|
@@ -30,6 +30,12 @@ export interface PrunedDaemonState {
|
|
|
30
30
|
* LEFT - an ensure writing one right now must not be mistaken for a generation that ended.
|
|
31
31
|
*/
|
|
32
32
|
export declare function pruneDeadGenerations(paths: HostDaemonPaths): Promise<PrunedDaemonState>;
|
|
33
|
+
/**
|
|
34
|
+
* Removes every claim whose owner is gone, and every claim of a generation in `ended` (being removed by the caller)
|
|
35
|
+
* whose owner identity cannot be proven. Returns the session paths released. Every path that removes a generation
|
|
36
|
+
* directory calls this first, so no generation leaves claims behind that its directory no longer explains.
|
|
37
|
+
*/
|
|
38
|
+
export declare function endClaimsOfEndedGenerations(paths: HostDaemonDirectory, ended: ReadonlySet<string>): Promise<string[]>;
|
|
33
39
|
/**
|
|
34
40
|
* Every generation of this daemon that is still running, newest ordinal last. A record naming a
|
|
35
41
|
* dead pid is omitted rather than reported as history: `host status` answers who is alive now.
|