@code-yeongyu/senpi 2026.10.16-3 → 2026.10.16-4
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 +28 -0
- package/dist/bundle/chunks/{chunk-I5ZJEXTM.js → chunk-2NO5ZOYE.js} +6 -6
- package/dist/bundle/chunks/{chunk-4R4PRTF5.js → chunk-CJRE5LLR.js} +1 -1
- package/dist/bundle/chunks/{chunk-X5VCHHXR.js → chunk-GY5O7NIQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-AVALXFDW.js → chunk-JD4CV3GH.js} +1 -1
- package/dist/bundle/chunks/{chunk-SSEGIXRL.js → chunk-KUQ3K33L.js} +1 -1
- package/dist/bundle/chunks/{chunk-FMYLLJBQ.js → chunk-KWC3MCLP.js} +1 -1
- package/dist/bundle/chunks/chunk-MLL7AGVM.js +3 -0
- package/dist/bundle/chunks/chunk-PAJKEG2L.js +8 -0
- package/dist/bundle/chunks/{chunk-DPRV3FR7.js → chunk-PWHAOWNH.js} +1 -1
- package/dist/bundle/chunks/{chunk-HJPX7PBL.js → chunk-XCRGEMY2.js} +125 -118
- package/dist/bundle/chunks/{cli-main-JVPFJGP5.js → cli-main-I6JF2GPS.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-SVCMHCYG.js → help-fast-path-APSHUDY6.js} +1 -1
- package/dist/bundle/chunks/{host-session-command-46PTZGIB.js → host-session-command-BUJUJHDA.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-BROZRY34.js → interactive-mode-4Q3TIXOZ.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-SMQR5SCJ.js → multi-session-host-XEZB2XVR.js} +5 -4
- package/dist/bundle/chunks/{package-manager-cli-2QFJY2AS.js → package-manager-cli-YWZHOZOY.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-QX7YSKDU.js → rotation-stream-HY3JPQCX.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-Z45T3NZO.js → rpc-mode-SLWXVXXE.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-2RHSWBQO.js → schedule-command-JRWZGEDU.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-H7FFWF2A.js → session-control-endpoint-E2ZSEWXW.js} +1 -1
- package/dist/bundle/chunks/session-picker-5IZY7E3G.js +2 -0
- package/dist/bundle/chunks/session-worker.js +243 -235
- package/dist/bundle/cli.js +2 -2
- 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-runtime.d.ts +15 -0
- package/dist/core/agent-session-runtime.js +53 -2
- package/dist/core/agent-session.d.ts +12 -0
- package/dist/core/agent-session.js +42 -1
- package/dist/core/context-files.d.ts +21 -0
- package/dist/core/context-files.js +175 -0
- package/dist/core/diagnostics.d.ts +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/account-command.js +26 -3
- package/dist/core/extensions/builtin/anthropic-subscription/account-management.d.ts +5 -0
- package/dist/core/extensions/builtin/anthropic-subscription/account-management.js +40 -0
- package/dist/core/extensions/builtin/anthropic-subscription/accounts.d.ts +3 -0
- package/dist/core/extensions/builtin/anthropic-subscription/accounts.js +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +10 -5
- package/dist/core/extensions/builtin/anthropic-subscription/failover.js +158 -17
- package/dist/core/extensions/builtin/anthropic-subscription/usage-limit-block.d.ts +13 -0
- package/dist/core/extensions/builtin/anthropic-subscription/usage-limit-block.js +89 -0
- package/dist/core/extensions/builtin/permission-system/always-scope.d.ts +34 -0
- package/dist/core/extensions/builtin/permission-system/always-scope.js +77 -0
- package/dist/core/extensions/builtin/permission-system/index.js +3 -1
- package/dist/core/extensions/builtin/permission-system/parsers.d.ts +1 -1
- package/dist/core/extensions/builtin/permission-system/parsers.js +24 -3
- package/dist/core/extensions/builtin/permission-system/prompt.js +5 -1
- package/dist/core/extensions/builtin/permission-system/service.js +1 -1
- package/dist/core/extensions/builtin/permission-system/types.d.ts +7 -0
- package/dist/core/package-manager.js +4 -32
- package/dist/core/project-trust.d.ts +8 -0
- package/dist/core/project-trust.js +12 -0
- package/dist/core/resource-loader.d.ts +5 -7
- package/dist/core/resource-loader.js +11 -84
- package/dist/core/retry-fallback/circuit-probes.d.ts +1 -0
- package/dist/core/retry-fallback/circuit-probes.js +3 -0
- package/dist/core/retry-fallback/circuit.d.ts +5 -0
- package/dist/core/retry-fallback/circuit.js +27 -4
- package/dist/core/retry-fallback/controller.d.ts +9 -4
- package/dist/core/retry-fallback/controller.js +21 -4
- package/dist/core/settings-manager.d.ts +15 -0
- package/dist/core/settings-manager.js +21 -1
- package/dist/core/skills-prompt.d.ts +18 -0
- package/dist/core/skills-prompt.js +131 -0
- package/dist/core/skills.d.ts +1 -9
- package/dist/core/skills.js +1 -59
- package/dist/core/trust-manager.d.ts +13 -4
- package/dist/core/trust-manager.js +52 -21
- package/dist/main.js +23 -4
- package/dist/modes/interactive/interactive-mode.js +10 -3
- package/dist/modes/rpc/abort-grace.d.ts +9 -0
- package/dist/modes/rpc/abort-grace.js +11 -0
- package/dist/modes/rpc/effect-commands.d.ts +0 -2
- package/dist/modes/rpc/effect-commands.js +2 -3
- package/dist/modes/rpc/host-handover-parked.d.ts +6 -0
- package/dist/modes/rpc/host-handover-parked.js +1 -0
- package/dist/modes/rpc/host-handover-reopen.js +11 -2
- package/dist/modes/rpc/rpc-client.d.ts +28 -2
- package/dist/modes/rpc/rpc-client.js +24 -11
- package/dist/modes/rpc/rpc-request-deadline.d.ts +12 -0
- package/dist/modes/rpc/rpc-request-deadline.js +15 -0
- package/dist/modes/rpc/session-command-router.js +7 -4
- package/dist/modes/rpc/session-registry-attach.js +6 -1
- package/dist/modes/rpc/session-registry-claims.d.ts +22 -2
- package/dist/modes/rpc/session-registry-claims.js +68 -0
- package/dist/modes/rpc/session-registry-opening.d.ts +23 -0
- package/dist/modes/rpc/session-registry-opening.js +57 -0
- package/dist/modes/rpc/session-registry.d.ts +1 -0
- package/dist/modes/rpc/session-registry.js +48 -60
- package/docs/rpc.md +6 -2
- package/docs/settings.md +13 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-6MEETY7N.js +0 -8
- package/dist/bundle/chunks/chunk-TEGEPQIA.js +0 -3
- package/dist/bundle/chunks/session-picker-2O2BNBVE.js +0 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, join, sep } from "node:path";
|
|
4
4
|
import lockfile from "proper-lockfile";
|
|
5
5
|
import { CONFIG_DIR_NAME } from "../config.js";
|
|
6
6
|
import { canonicalizePath, canonicalizePathStrict, resolvePath } from "../utils/paths.js";
|
|
@@ -201,35 +201,66 @@ function withTrustFileLock(path, fn) {
|
|
|
201
201
|
release();
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
|
+
/** True when `dir` is the user's home or a folder above it: never a project (#2465). */
|
|
205
|
+
function isHomeOrAboveDir(dir, homeDir) {
|
|
206
|
+
return dir === homeDir || homeDir.startsWith(dir.endsWith(sep) ? dir : `${dir}${sep}`);
|
|
207
|
+
}
|
|
208
|
+
/** The nearest folder at or above `dir` that holds `.git`, as spelled; undefined outside a repository. */
|
|
209
|
+
function findGitRepoRoot(dir) {
|
|
210
|
+
let currentDir = dir;
|
|
211
|
+
while (!existsSync(join(currentDir, ".git"))) {
|
|
212
|
+
const parentDir = dirname(currentDir);
|
|
213
|
+
if (parentDir === currentDir)
|
|
214
|
+
return undefined;
|
|
215
|
+
currentDir = parentDir;
|
|
216
|
+
}
|
|
217
|
+
return currentDir;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The project `.agents/skills` directories for `cwd`, nearest first: one per folder from cwd up to
|
|
221
|
+
* the git root (or the filesystem root), stopping below the user's home. Home and every folder
|
|
222
|
+
* above it are never a project; `~/.agents/skills` is the user's own global skills. The ONE walk
|
|
223
|
+
* both the trust check and the loader use, over the same spelling and the same git root (#3420
|
|
224
|
+
* review): paths stay as spelled (resources are compared as strings), and each folder's real path
|
|
225
|
+
* decides whether it is home or above, so a symlink can't carry the walk past home either way.
|
|
226
|
+
*/
|
|
227
|
+
export function projectAgentsSkillDirs(cwd) {
|
|
228
|
+
const homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir()));
|
|
229
|
+
const startDir = resolvePath(cwd);
|
|
230
|
+
const gitRepoRoot = findGitRepoRoot(startDir);
|
|
231
|
+
const dirs = [];
|
|
232
|
+
let currentDir = startDir;
|
|
233
|
+
while (!isHomeOrAboveDir(canonicalizePath(currentDir), homeDir)) {
|
|
234
|
+
dirs.push(join(currentDir, ".agents", "skills"));
|
|
235
|
+
if (currentDir === gitRepoRoot)
|
|
236
|
+
break;
|
|
237
|
+
const parentDir = dirname(currentDir);
|
|
238
|
+
if (parentDir === currentDir)
|
|
239
|
+
break;
|
|
240
|
+
currentDir = parentDir;
|
|
241
|
+
}
|
|
242
|
+
return dirs;
|
|
243
|
+
}
|
|
204
244
|
/**
|
|
205
245
|
* Returns true when cwd has project-local resources that must be gated by
|
|
206
246
|
* project trust: trust-requiring entries under the project config dir or the
|
|
207
|
-
* legacy cwd/.pi dir (whose resources are also discovered), or
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
247
|
+
* legacy cwd/.pi dir (whose resources are also discovered), or a project
|
|
248
|
+
* `.agents/skills` (`projectAgentsSkillDirs`, the same walk the loader uses).
|
|
249
|
+
* The user's home and every folder above it are never a project: their config
|
|
250
|
+
* dirs and .agents/skills are the user's own global resources (#2465).
|
|
211
251
|
*/
|
|
212
252
|
export function hasTrustRequiringProjectResources(cwd) {
|
|
213
253
|
const homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir()));
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
if (projectCodemodeNamesExecutable(join(currentDir, CONFIG_DIR_NAME)))
|
|
221
|
-
return true;
|
|
222
|
-
while (true) {
|
|
223
|
-
const agentsSkillsDir = join(currentDir, ".agents", "skills");
|
|
224
|
-
if (agentsSkillsDir !== userAgentsSkillsDir && existsSync(agentsSkillsDir)) {
|
|
254
|
+
const currentDir = canonicalizePath(resolvePath(cwd));
|
|
255
|
+
if (!isHomeOrAboveDir(currentDir, homeDir)) {
|
|
256
|
+
const configDirs = [join(currentDir, CONFIG_DIR_NAME), join(currentDir, LEGACY_PROJECT_CONFIG_DIR_NAME)];
|
|
257
|
+
if (configDirs.some((configDir) => TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES.some((entry) => existsSync(join(configDir, entry))))) {
|
|
225
258
|
return true;
|
|
226
259
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
currentDir = parentDir;
|
|
260
|
+
if (projectCodemodeNamesExecutable(join(currentDir, CONFIG_DIR_NAME)))
|
|
261
|
+
return true;
|
|
232
262
|
}
|
|
263
|
+
return projectAgentsSkillDirs(cwd).some((dir) => existsSync(dir));
|
|
233
264
|
}
|
|
234
265
|
export class ProjectTrustStore {
|
|
235
266
|
constructor(agentDir) {
|
package/dist/main.js
CHANGED
|
@@ -47,7 +47,7 @@ import { ModelRuntime } from "./core/model-runtime.js";
|
|
|
47
47
|
import { markMovedSessions, movedHereSessionToContinue, withMovedSessions } from "./core/moved-sessions.js";
|
|
48
48
|
import { restoreStdout, takeOverStdout } from "./core/output-guard.js";
|
|
49
49
|
import { recordProcessLifetime } from "./core/process-crash-record.js";
|
|
50
|
-
import { resolveProjectTrusted } from "./core/project-trust.js";
|
|
50
|
+
import { presetTrustsProject, resolveProjectTrusted } from "./core/project-trust.js";
|
|
51
51
|
import { resolveResumeTarget } from "./core/resume-target.js";
|
|
52
52
|
import { formatMissingSessionCwdPrompt, getMissingSessionCwdIssue, MissingSessionCwdError, } from "./core/session-cwd.js";
|
|
53
53
|
import { sessionExtensionFlagValues } from "./core/session-extension-flags.js";
|
|
@@ -677,13 +677,26 @@ export function createCliRuntimeFactory(configuration, local = {}) {
|
|
|
677
677
|
const projectTrustDiagnostics = [];
|
|
678
678
|
const cachedProjectTrust = projectTrustByCwd.get(cwd);
|
|
679
679
|
const hasTrustRequiringResources = hasTrustRequiringProjectResources(cwd);
|
|
680
|
-
|
|
680
|
+
// A full-access session trusts its project without asking, for that session only: nothing is
|
|
681
|
+
// cached here or saved to the trust store, and a saved "do not trust" still wins (#2465).
|
|
682
|
+
const fullAccessTrust = presetTrustsProject(sessionExtensionFlagValues(parsed.unknownFlags, launchProfile).get("permission-preset"), startupSettingsManager.getGlobalSettings().permissionPreset) && trustStore.get(cwd) !== false;
|
|
683
|
+
const shouldResolveProjectTrust = parsed.projectTrustOverride === undefined &&
|
|
684
|
+
cachedProjectTrust === undefined &&
|
|
685
|
+
!fullAccessTrust &&
|
|
686
|
+
hasTrustRequiringResources;
|
|
681
687
|
const projectTrusted = shouldResolveProjectTrust
|
|
682
688
|
? false
|
|
683
689
|
: (cachedProjectTrust ??
|
|
684
690
|
parsed.projectTrustOverride ??
|
|
685
|
-
(!hasTrustRequiringResources || trustStore.get(cwd) === true));
|
|
686
|
-
|
|
691
|
+
(fullAccessTrust || !hasTrustRequiringResources || trustStore.get(cwd) === true));
|
|
692
|
+
// Trusted only because of Full access (resources need trust and nothing else granted it): leaving
|
|
693
|
+
// Full access withdraws it (#3420 review T5).
|
|
694
|
+
const trustFromFullAccess = fullAccessTrust &&
|
|
695
|
+
hasTrustRequiringResources &&
|
|
696
|
+
cachedProjectTrust === undefined &&
|
|
697
|
+
parsed.projectTrustOverride === undefined &&
|
|
698
|
+
trustStore.get(cwd) !== true;
|
|
699
|
+
const runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted, trustFromFullAccess });
|
|
687
700
|
// The opener's per-session fallback policy is an in-memory override: never saved, never shared.
|
|
688
701
|
if (launchProfile?.retryFallback)
|
|
689
702
|
applyRetryFallbackProfile(runtimeSettingsManager, launchProfile.retryFallback);
|
|
@@ -1194,6 +1207,12 @@ export async function main(args, options) {
|
|
|
1194
1207
|
reportDiagnostics([{ type: "warning", message: formatThinkingClampWarning(startupThinkingClamp) }]);
|
|
1195
1208
|
}
|
|
1196
1209
|
if (appMode !== "interactive") {
|
|
1210
|
+
reportDiagnostics([...(resourceLoader.getAgentsFiles().diagnostics ?? []), ...resourceLoader.getSkills().diagnostics]
|
|
1211
|
+
.filter((diagnostic) => diagnostic.type === "warning" && diagnostic.category === "size-limit")
|
|
1212
|
+
.map((diagnostic) => ({
|
|
1213
|
+
type: "warning",
|
|
1214
|
+
message: diagnostic.path ? `${diagnostic.path}: ${diagnostic.message}` : diagnostic.message,
|
|
1215
|
+
})));
|
|
1197
1216
|
reportDiagnostics(collectAuthDiagnostics(services.authStorage, "runtime creation"));
|
|
1198
1217
|
}
|
|
1199
1218
|
if (runtime.diagnostics.some((diagnostic) => diagnostic.type === "error")) {
|
|
@@ -1952,16 +1952,23 @@ export class InteractiveMode {
|
|
|
1952
1952
|
}
|
|
1953
1953
|
}
|
|
1954
1954
|
if (showDiagnostics) {
|
|
1955
|
-
const addDiagnosticNotice = (title, diagnostics) => {
|
|
1955
|
+
const addDiagnosticNotice = (title, diagnostics, why = "Conflicting or invalid loaded resources were found.") => {
|
|
1956
1956
|
this.loadedResourcesContainer.addChild(buildNoticeBox({
|
|
1957
1957
|
title,
|
|
1958
1958
|
tone: "warning",
|
|
1959
|
-
why
|
|
1959
|
+
why,
|
|
1960
1960
|
extra: this.formatDiagnostics(diagnostics, sourceInfos),
|
|
1961
1961
|
}, { expanded: this.toolOutputExpanded }, theme));
|
|
1962
1962
|
this.loadedResourcesContainer.addChild(new Spacer(1));
|
|
1963
1963
|
};
|
|
1964
|
-
const
|
|
1964
|
+
const contextDiagnostics = [
|
|
1965
|
+
...(this.session.resourceLoader.getAgentsFiles().diagnostics ?? []),
|
|
1966
|
+
...skillsResult.diagnostics.filter((diagnostic) => diagnostic.category === "size-limit"),
|
|
1967
|
+
];
|
|
1968
|
+
if (contextDiagnostics.length > 0) {
|
|
1969
|
+
addDiagnosticNotice("Context size limits", contextDiagnostics, "Context files or skill catalogs were limited to keep requests bounded.");
|
|
1970
|
+
}
|
|
1971
|
+
const skillDiagnostics = skillsResult.diagnostics.filter((diagnostic) => diagnostic.category !== "size-limit");
|
|
1965
1972
|
if (skillDiagnostics.length > 0) {
|
|
1966
1973
|
addDiagnosticNotice("Skill conflicts", skillDiagnostics);
|
|
1967
1974
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `abort { awaitIdle }` grace window, shared by the host that enforces it and the client that sizes its
|
|
3
|
+
* request deadline from it (docs/rpc.md `abort`). Kept free of host imports so the client can load it.
|
|
4
|
+
*/
|
|
5
|
+
export declare const ABORT_DEFAULT_GRACE_MS = 10000;
|
|
6
|
+
export declare const ABORT_MAX_GRACE_MS = 60000;
|
|
7
|
+
/** A `graceMs` the host accepts: a whole number of milliseconds from 0 to {@link ABORT_MAX_GRACE_MS}. */
|
|
8
|
+
export declare function isAbortGraceInRange(graceMs: unknown): graceMs is number;
|
|
9
|
+
//# sourceMappingURL=abort-grace.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `abort { awaitIdle }` grace window, shared by the host that enforces it and the client that sizes its
|
|
3
|
+
* request deadline from it (docs/rpc.md `abort`). Kept free of host imports so the client can load it.
|
|
4
|
+
*/
|
|
5
|
+
export const ABORT_DEFAULT_GRACE_MS = 10_000;
|
|
6
|
+
export const ABORT_MAX_GRACE_MS = 60_000;
|
|
7
|
+
/** A `graceMs` the host accepts: a whole number of milliseconds from 0 to {@link ABORT_MAX_GRACE_MS}. */
|
|
8
|
+
export function isAbortGraceInRange(graceMs) {
|
|
9
|
+
return typeof graceMs === "number" && Number.isInteger(graceMs) && graceMs >= 0 && graceMs <= ABORT_MAX_GRACE_MS;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=abort-grace.js.map
|
|
@@ -18,8 +18,6 @@ export declare function beginEffect(ctx: EffectCommandContext, kind: EffectComma
|
|
|
18
18
|
readonly effectId: string;
|
|
19
19
|
}): Promise<EffectBegin | RpcResponse>;
|
|
20
20
|
export declare function isEffectBegin(value: EffectBegin | RpcResponse): value is EffectBegin;
|
|
21
|
-
export declare const ABORT_DEFAULT_GRACE_MS = 10000;
|
|
22
|
-
export declare const ABORT_MAX_GRACE_MS = 60000;
|
|
23
21
|
/**
|
|
24
22
|
* `abort`: with `clearQueue` the queued inputs are drained (each gets a `cleared` receipt) before
|
|
25
23
|
* the abort; with `awaitIdle` the answer waits until the session is idle or `graceMs` passes. Without
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ModelUsabilityBudgetError } from "../../core/extensions/builtin/compaction/model-usability-budget.js";
|
|
2
2
|
import { ModelSwitchAuthError } from "../../core/model-switch-auth-error.js";
|
|
3
|
+
import { ABORT_DEFAULT_GRACE_MS, ABORT_MAX_GRACE_MS, isAbortGraceInRange } from "./abort-grace.js";
|
|
3
4
|
import { ClientAdmissions } from "./client-admissions.js";
|
|
4
5
|
import { EffectLedger } from "./effect-ledger.js";
|
|
5
6
|
import { contextBudgetOutcome, EFFECT_OUTCOMES, modelSwitchErrorOutcome, providerUnavailableOutcome, unknownModelOutcome, unsupportedThinkingLevelOutcome, } from "./effect-outcome-copy.js";
|
|
@@ -45,13 +46,11 @@ function isResponse(value) {
|
|
|
45
46
|
export function isEffectBegin(value) {
|
|
46
47
|
return !isResponse(value);
|
|
47
48
|
}
|
|
48
|
-
export const ABORT_DEFAULT_GRACE_MS = 10_000;
|
|
49
|
-
export const ABORT_MAX_GRACE_MS = 60_000;
|
|
50
49
|
function invalidGrace(command) {
|
|
51
50
|
const { graceMs } = command;
|
|
52
51
|
if (graceMs === undefined)
|
|
53
52
|
return undefined;
|
|
54
|
-
if (
|
|
53
|
+
if (isAbortGraceInRange(graceMs))
|
|
55
54
|
return undefined;
|
|
56
55
|
const reason = "invalid_grace_ms";
|
|
57
56
|
const action = `send graceMs as a whole number of milliseconds from 0 to ${ABORT_MAX_GRACE_MS}`;
|
|
@@ -30,6 +30,12 @@ export interface HandoverParkedEntry {
|
|
|
30
30
|
readonly sessionContext?: SessionContext;
|
|
31
31
|
readonly autoTitle?: boolean;
|
|
32
32
|
readonly retryFallback?: SessionRetryFallbackProfile;
|
|
33
|
+
/**
|
|
34
|
+
* The host instance that wrote the park. Its own reopen watch skips it: the park is for the NEXT
|
|
35
|
+
* generation, and a host reopening what it just idle-parked races the client's reopen (senpi#3485).
|
|
36
|
+
* Absent on entries an older generation wrote, which every reader treats as a predecessor's park.
|
|
37
|
+
*/
|
|
38
|
+
readonly parkedBy?: string;
|
|
33
39
|
}
|
|
34
40
|
export declare function recordHandoverParkedSession(daemonDir: string, entry: HandoverParkedEntry): Promise<void>;
|
|
35
41
|
export declare function readHandoverParkedSessions(daemonDir: string): Promise<readonly HandoverParkedEntry[]>;
|
|
@@ -35,6 +35,7 @@ function isEntry(value) {
|
|
|
35
35
|
typeof record.retainOnDisconnect === "boolean" &&
|
|
36
36
|
(record.durableSessionId === undefined || typeof record.durableSessionId === "string") &&
|
|
37
37
|
(record.cwd === undefined || typeof record.cwd === "string") &&
|
|
38
|
+
(record.parkedBy === undefined || typeof record.parkedBy === "string") &&
|
|
38
39
|
(record.thinkingLevel === undefined || typeof record.thinkingLevel === "string") &&
|
|
39
40
|
(record.permissionPreset === undefined ||
|
|
40
41
|
(typeof record.permissionPreset === "string" &&
|
|
@@ -46,12 +46,12 @@ export async function reopenHandoverParkedSessions(open, agentDir, socket, holde
|
|
|
46
46
|
// it by path with the same durable id.
|
|
47
47
|
if (holder?.shouldReopen !== undefined && !holder.shouldReopen())
|
|
48
48
|
return;
|
|
49
|
-
const parked = await
|
|
49
|
+
const parked = await inheritedParks(daemonDir, holder);
|
|
50
50
|
for (const entry of parked) {
|
|
51
51
|
sawAnyPark = true;
|
|
52
52
|
await reopenOne(daemonDir, open, entry, reopenAttempts, heldPolls, holder);
|
|
53
53
|
}
|
|
54
|
-
const remaining = (await
|
|
54
|
+
const remaining = (await inheritedParks(daemonDir, holder)).length;
|
|
55
55
|
emptyStreak = remaining === 0 ? emptyStreak + 1 : 0;
|
|
56
56
|
if (sawAnyPark && emptyStreak >= HANDOVER_PARKED_REOPEN_QUIET_STREAK)
|
|
57
57
|
return;
|
|
@@ -60,6 +60,15 @@ export async function reopenHandoverParkedSessions(open, agentDir, socket, holde
|
|
|
60
60
|
await new Promise((resolve) => setTimeout(resolve, HANDOVER_PARKED_REOPEN_POLL_MS));
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The parks a PREVIOUS generation left for this one. A park this host wrote itself (its idle sweep
|
|
65
|
+
* parks retained sessions into the same record) is for its successor: reopening it here would put a
|
|
66
|
+
* second open of the path in flight against the client reopening it (senpi#3485).
|
|
67
|
+
*/
|
|
68
|
+
async function inheritedParks(daemonDir, holder) {
|
|
69
|
+
const parked = await readHandoverParkedSessions(daemonDir);
|
|
70
|
+
return parked.filter((entry) => entry.parkedBy === undefined || entry.parkedBy !== holder?.hostInstance);
|
|
71
|
+
}
|
|
63
72
|
async function reopenOne(daemonDir, open, entry, attempts, heldPolls, holder) {
|
|
64
73
|
await dropHandoverParkedSessionIfFileGone(daemonDir, entry.sessionPath);
|
|
65
74
|
if (!existsSync(entry.sessionPath)) {
|
|
@@ -22,7 +22,7 @@ export declare class RpcCommandError extends Error {
|
|
|
22
22
|
}
|
|
23
23
|
import type { SessionEntry, SessionTreeNode } from "../../core/session-manager.ts";
|
|
24
24
|
import type { JsonAgentSessionEvent } from "../json-event.ts";
|
|
25
|
-
import type { EditAssistantMessageResult, EditUserMessageResult, RpcAccountFailoverEvent, RpcAuthAccountsChangedEvent, RpcCommand, RpcEvalCell, RpcEvalCellSnapshot, RpcExtensionEvent, RpcExtensionUIProgress, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcOpenQueuedEvent, RpcProviderAccount, RpcSessionModelEntry, RpcSessionParkedEvent, RpcSessionReplacedEvent, RpcSessionState, RpcSlashCommand, RpcUnloadAnswer } from "./rpc-types.ts";
|
|
25
|
+
import type { EditAssistantMessageResult, EditUserMessageResult, RpcAccountFailoverEvent, RpcAuthAccountsChangedEvent, RpcCommand, RpcEvalCell, RpcEvalCellSnapshot, RpcExtensionEvent, RpcExtensionUIProgress, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcOpenQueuedEvent, RpcProviderAccount, RpcResponse, RpcSessionModelEntry, RpcSessionParkedEvent, RpcSessionReplacedEvent, RpcSessionState, RpcSlashCommand, RpcUnloadAnswer } from "./rpc-types.ts";
|
|
26
26
|
export interface RpcClientOptions {
|
|
27
27
|
/** Connect to an existing multi-session Unix socket instead of spawning a child. */
|
|
28
28
|
socketPath?: string;
|
|
@@ -72,6 +72,20 @@ export declare class RpcTransportGoneError extends Error {
|
|
|
72
72
|
constructor(message?: string);
|
|
73
73
|
}
|
|
74
74
|
export declare function isTransportGoneError(error: unknown): error is RpcTransportGoneError;
|
|
75
|
+
/** The options of a typed `abort` (see `docs/rpc.md`, `abort`). */
|
|
76
|
+
export type RpcAbortOptions = Omit<Extract<RpcCommand, {
|
|
77
|
+
type: "abort";
|
|
78
|
+
}>, "type" | "id" | "sessionId">;
|
|
79
|
+
/** The host's answer to a typed `abort`; an older host answers with no data. */
|
|
80
|
+
export type RpcAbortAnswer = Extract<RpcResponse, {
|
|
81
|
+
command: "abort";
|
|
82
|
+
success: true;
|
|
83
|
+
}>["data"];
|
|
84
|
+
/** `get_effect_status`: the receipt, or proof the whole transcript holds none. */
|
|
85
|
+
export type RpcEffectStatusAnswer = Extract<RpcResponse, {
|
|
86
|
+
command: "get_effect_status";
|
|
87
|
+
success: true;
|
|
88
|
+
}>["data"];
|
|
75
89
|
export declare class RpcClientOpenInFlightError extends Error {
|
|
76
90
|
readonly code: "open_session_in_flight";
|
|
77
91
|
constructor();
|
|
@@ -222,9 +236,21 @@ export declare class RpcClient {
|
|
|
222
236
|
enqueueOrder?: number;
|
|
223
237
|
}): Promise<QueuedInputDisposition>;
|
|
224
238
|
/**
|
|
225
|
-
* Abort current operation.
|
|
239
|
+
* Abort current operation. With no options this is the fire-and-forget abort it always was (transport
|
|
240
|
+
* loss resolves). With options it is the typed abort: `effectId` records a receipt, `awaitIdle` answers
|
|
241
|
+
* once the turn stopped (or `graceMs` passed), `clearQueue` drains queued input first; the host's
|
|
242
|
+
* answer (`effectId`, `state`, `effect`, `stopped`, `idle`, `cleared`) is returned.
|
|
226
243
|
*/
|
|
227
244
|
abort(): Promise<void>;
|
|
245
|
+
abort(options: RpcAbortOptions): Promise<RpcAbortAnswer>;
|
|
246
|
+
abort(options?: RpcAbortOptions): Promise<RpcAbortAnswer | undefined>;
|
|
247
|
+
/**
|
|
248
|
+
* The newest receipt of one effect (`get_effect_status`, capability `effect_ledger`). With `sessionPath`
|
|
249
|
+
* the host reads that transcript from disk, so a session no host holds is answered without loading it.
|
|
250
|
+
*/
|
|
251
|
+
getEffectStatus(effectId: string, options?: {
|
|
252
|
+
readonly sessionPath?: string;
|
|
253
|
+
}): Promise<RpcEffectStatusAnswer>;
|
|
228
254
|
/** Abort with host refusals and transport loss surfaced to machine callers. */
|
|
229
255
|
abortStrict(): Promise<void>;
|
|
230
256
|
abortCompaction(): Promise<void>;
|
|
@@ -20,7 +20,7 @@ export class RpcCommandError extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
|
|
23
|
-
import { armRequestDeadline, OPEN_AFTER_QUEUED_DEADLINE_MS, openStalledMessage, PROMPT_ACK_MAX_WAIT_MS, PROMPT_AFTER_QUEUED_DEADLINE_MS, PROMPT_COMPACTION_DEADLINE_MS, promptStalledMessage, REQUEST_DEADLINE_MS, RpcRequestTimeoutError, } from "./rpc-request-deadline.js";
|
|
23
|
+
import { armRequestDeadline, OPEN_AFTER_QUEUED_DEADLINE_MS, openStalledMessage, PROMPT_ACK_MAX_WAIT_MS, PROMPT_AFTER_QUEUED_DEADLINE_MS, PROMPT_COMPACTION_DEADLINE_MS, promptStalledMessage, REQUEST_DEADLINE_MS, RpcRequestTimeoutError, requestDeadlineMs, } from "./rpc-request-deadline.js";
|
|
24
24
|
import { RPC_ERROR_UNKNOWN_COMMAND } from "./rpc-types.js";
|
|
25
25
|
import { readSocketSecret, resolveSocketTransportAddress, sendSocketHandshake, socketSecretPath, } from "./socket-transport.js";
|
|
26
26
|
import { socketNeedsHandshake } from "./tui-socket.js";
|
|
@@ -455,17 +455,30 @@ export class RpcClient {
|
|
|
455
455
|
// Older hosts omit the disposition; degrade like prompt() so clients render only canonical events.
|
|
456
456
|
return this.getData(response)?.disposition ?? "handled";
|
|
457
457
|
}
|
|
458
|
+
async abort(options) {
|
|
459
|
+
if (options === undefined) {
|
|
460
|
+
try {
|
|
461
|
+
await this.send({ type: "abort" });
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
if (!isTransportGoneError(error))
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
return this.getData(await this.send({ ...options, type: "abort" }));
|
|
470
|
+
}
|
|
458
471
|
/**
|
|
459
|
-
*
|
|
472
|
+
* The newest receipt of one effect (`get_effect_status`, capability `effect_ledger`). With `sessionPath`
|
|
473
|
+
* the host reads that transcript from disk, so a session no host holds is answered without loading it.
|
|
460
474
|
*/
|
|
461
|
-
async
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
475
|
+
async getEffectStatus(effectId, options = {}) {
|
|
476
|
+
const command = {
|
|
477
|
+
type: "get_effect_status",
|
|
478
|
+
effectId,
|
|
479
|
+
...(options.sessionPath === undefined ? {} : { sessionPath: options.sessionPath }),
|
|
480
|
+
};
|
|
481
|
+
return this.getData(await this.send(command, options.sessionPath === undefined));
|
|
469
482
|
}
|
|
470
483
|
/** Abort with host refusals and transport loss surfaced to machine callers. */
|
|
471
484
|
async abortStrict() {
|
|
@@ -1018,7 +1031,7 @@ export class RpcClient {
|
|
|
1018
1031
|
};
|
|
1019
1032
|
const deadline = armRequestDeadline(command.type === "prompt" && sessionId === this.sessionId && this.isCompacting()
|
|
1020
1033
|
? PROMPT_COMPACTION_DEADLINE_MS
|
|
1021
|
-
:
|
|
1034
|
+
: requestDeadlineMs(command), timeoutMessage, expire);
|
|
1022
1035
|
// Compaction events extend a prompt's wait, but never past this cap from when it was sent.
|
|
1023
1036
|
if (command.type === "prompt") {
|
|
1024
1037
|
promptCap = setTimeout(() => {
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
/** How long any request waits for the host's answer - for an open, for its `queued` acknowledgement. */
|
|
2
2
|
export declare const REQUEST_DEADLINE_MS = 30000;
|
|
3
|
+
/**
|
|
4
|
+
* How long one request may wait for its answer before the client gives up. An awaited abort answers only
|
|
5
|
+
* after the turn stopped or its `graceMs` ran out (up to 60 s), so its wait is that grace plus the
|
|
6
|
+
* ordinary allowance; every other command keeps the plain request deadline. A `graceMs` the host would
|
|
7
|
+
* refuse (out of range, fractional, NaN, Infinity) gets the plain deadline too: the host answers it at once
|
|
8
|
+
* with `invalid_argument`, and the client must still be listening for that refusal.
|
|
9
|
+
*/
|
|
10
|
+
export declare function requestDeadlineMs(command: {
|
|
11
|
+
readonly type: string;
|
|
12
|
+
readonly awaitIdle?: unknown;
|
|
13
|
+
readonly graceMs?: unknown;
|
|
14
|
+
}): number;
|
|
3
15
|
/** Remote compaction may exhaust its budget before a local summary uses its maximum override. */
|
|
4
16
|
export declare const PROMPT_COMPACTION_DEADLINE_MS: number;
|
|
5
17
|
/**
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { SUMMARIZATION_MAX_DURATION_CAP_MS, SUMMARIZATION_TOTAL_BUDGET_MS, } from "../../core/compaction/stream-watchdog.js";
|
|
2
|
+
import { ABORT_DEFAULT_GRACE_MS, isAbortGraceInRange } from "./abort-grace.js";
|
|
2
3
|
/** How long any request waits for the host's answer - for an open, for its `queued` acknowledgement. */
|
|
3
4
|
export const REQUEST_DEADLINE_MS = 30_000;
|
|
5
|
+
/**
|
|
6
|
+
* How long one request may wait for its answer before the client gives up. An awaited abort answers only
|
|
7
|
+
* after the turn stopped or its `graceMs` ran out (up to 60 s), so its wait is that grace plus the
|
|
8
|
+
* ordinary allowance; every other command keeps the plain request deadline. A `graceMs` the host would
|
|
9
|
+
* refuse (out of range, fractional, NaN, Infinity) gets the plain deadline too: the host answers it at once
|
|
10
|
+
* with `invalid_argument`, and the client must still be listening for that refusal.
|
|
11
|
+
*/
|
|
12
|
+
export function requestDeadlineMs(command) {
|
|
13
|
+
if (command.type !== "abort" || command.awaitIdle !== true)
|
|
14
|
+
return REQUEST_DEADLINE_MS;
|
|
15
|
+
if (command.graceMs === undefined)
|
|
16
|
+
return ABORT_DEFAULT_GRACE_MS + REQUEST_DEADLINE_MS;
|
|
17
|
+
return isAbortGraceInRange(command.graceMs) ? command.graceMs + REQUEST_DEADLINE_MS : REQUEST_DEADLINE_MS;
|
|
18
|
+
}
|
|
4
19
|
/** Remote compaction may exhaust its budget before a local summary uses its maximum override. */
|
|
5
20
|
export const PROMPT_COMPACTION_DEADLINE_MS = SUMMARIZATION_TOTAL_BUDGET_MS + SUMMARIZATION_MAX_DURATION_CAP_MS + REQUEST_DEADLINE_MS;
|
|
6
21
|
/**
|
|
@@ -476,10 +476,13 @@ export class SessionCommandRouter {
|
|
|
476
476
|
parkedPath !== undefined &&
|
|
477
477
|
retained?.retainOnDisconnect === true &&
|
|
478
478
|
retained.sessionPath !== undefined) {
|
|
479
|
-
await recordHandoverParkedSession(this.handoverParkedDir,
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
479
|
+
await recordHandoverParkedSession(this.handoverParkedDir, {
|
|
480
|
+
...parkedEntryFromProfile({
|
|
481
|
+
sessionPath: retained.sessionPath,
|
|
482
|
+
...(retained.durableSessionId !== undefined ? { durableSessionId: retained.durableSessionId } : {}),
|
|
483
|
+
}, retained.profile, Object.keys(this.defaults.hostContext ?? {})),
|
|
484
|
+
parkedBy: this.defaults.hostContext?.host_instance ?? hostInstanceId(),
|
|
485
|
+
}).catch((cause) => {
|
|
483
486
|
process.stderr.write(`senpi rpc handover park record ${sessionId} failed: ${String(cause)}\n`);
|
|
484
487
|
});
|
|
485
488
|
}
|
|
@@ -72,7 +72,12 @@ export async function attachToOpenSession(entries, sessionPath, profile, options
|
|
|
72
72
|
}
|
|
73
73
|
return attachToOpenSession(entries, sessionPath, profile, options, pathReservations, now);
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
// The path is still reserved by a session that is not serving (a teardown past its grace
|
|
76
|
+
// window): never a bare refusal, so the caller knows to retry (senpi#3485).
|
|
77
|
+
throw new RpcSessionRegistryError("session_path_in_use", "the session is still being released", {
|
|
78
|
+
reason: "path_still_held",
|
|
79
|
+
retry_after_ms: OPEN_IN_PROGRESS_WAIT_RETRY_MS,
|
|
80
|
+
});
|
|
76
81
|
}
|
|
77
82
|
const [handle, entry] = existing;
|
|
78
83
|
const wasParked = entry.retainOnDisconnect === true && entry.attachments === 0;
|
|
@@ -2,8 +2,28 @@
|
|
|
2
2
|
* The registry's path claims: the in-process reservation set and the cross-generation claim
|
|
3
3
|
* (`host-reservations.ts`) follow whatever file each entry's runtime is actually writing.
|
|
4
4
|
*/
|
|
5
|
-
import type
|
|
6
|
-
import type
|
|
5
|
+
import { type SessionPathReservations } from "./host-reservations.ts";
|
|
6
|
+
import { type RpcSessionEntry } from "./session-registry-types.ts";
|
|
7
|
+
/** Opens between their in-process reservation and their entry, by path: their cross-generation claim is in flight. */
|
|
8
|
+
export type PendingClaims = Map<string, Promise<void>>;
|
|
9
|
+
/**
|
|
10
|
+
* Marks `sessionPath` as mid-claim. The returned settle is called once the open has either recorded
|
|
11
|
+
* its entry or given the reservation back, so a waiter that wakes always finds one or the other.
|
|
12
|
+
*/
|
|
13
|
+
export declare function beginPendingClaim(pending: PendingClaims, sessionPath: string): () => void;
|
|
14
|
+
/**
|
|
15
|
+
* Takes the cross-generation claim for an open that already holds the in-process reservation, and
|
|
16
|
+
* returns the settle `beginPendingClaim` handed out: the caller settles it once its entry is recorded.
|
|
17
|
+
* A live claim another generation holds gives the reservation back and refuses with the owner.
|
|
18
|
+
*/
|
|
19
|
+
export declare function takeCrossGenerationClaim(reservations: Set<string>, pending: PendingClaims, pathReservations: SessionPathReservations, sessionPath: string): Promise<() => void>;
|
|
20
|
+
/**
|
|
21
|
+
* Waits out another open of this path that is still taking its claim (senpi#3485). Without the wait
|
|
22
|
+
* the path looked reserved by no session at all and a reopen was refused for a session about to be
|
|
23
|
+
* served. Bounded by the close grace window: a claim that never settles (a wedged daemon directory)
|
|
24
|
+
* ends this open with a retryable `session_path_in_use { reason: "claim_in_progress" }`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function settlePendingClaim(pending: ReadonlyMap<string, Promise<void>>, sessionPath: string, graceMs: number): Promise<void>;
|
|
7
27
|
/** Reconcile path and durable identity after runtime replacement. */
|
|
8
28
|
export declare function syncRuntimeMetadata(entries: Iterable<RpcSessionEntry>, reservations: Set<string>, pathReservations: SessionPathReservations | undefined): void;
|
|
9
29
|
/**
|
|
@@ -1,4 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry's path claims: the in-process reservation set and the cross-generation claim
|
|
3
|
+
* (`host-reservations.ts`) follow whatever file each entry's runtime is actually writing.
|
|
4
|
+
*/
|
|
5
|
+
import { SESSION_PATH_RETRY_AFTER_MS } from "./host-reservations.js";
|
|
1
6
|
import { canonicalSessionPath } from "./session-path-key.js";
|
|
7
|
+
import { RpcSessionRegistryError } from "./session-registry-types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Marks `sessionPath` as mid-claim. The returned settle is called once the open has either recorded
|
|
10
|
+
* its entry or given the reservation back, so a waiter that wakes always finds one or the other.
|
|
11
|
+
*/
|
|
12
|
+
export function beginPendingClaim(pending, sessionPath) {
|
|
13
|
+
const settled = Promise.withResolvers();
|
|
14
|
+
pending.set(sessionPath, settled.promise);
|
|
15
|
+
return () => {
|
|
16
|
+
if (pending.get(sessionPath) === settled.promise)
|
|
17
|
+
pending.delete(sessionPath);
|
|
18
|
+
settled.resolve();
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Takes the cross-generation claim for an open that already holds the in-process reservation, and
|
|
23
|
+
* returns the settle `beginPendingClaim` handed out: the caller settles it once its entry is recorded.
|
|
24
|
+
* A live claim another generation holds gives the reservation back and refuses with the owner.
|
|
25
|
+
*/
|
|
26
|
+
export async function takeCrossGenerationClaim(reservations, pending, pathReservations, sessionPath) {
|
|
27
|
+
const settle = beginPendingClaim(pending, sessionPath);
|
|
28
|
+
let holder;
|
|
29
|
+
try {
|
|
30
|
+
holder = await pathReservations.claim(sessionPath);
|
|
31
|
+
}
|
|
32
|
+
catch (cause) {
|
|
33
|
+
reservations.delete(sessionPath);
|
|
34
|
+
settle();
|
|
35
|
+
throw cause;
|
|
36
|
+
}
|
|
37
|
+
if (holder === undefined)
|
|
38
|
+
return settle;
|
|
39
|
+
reservations.delete(sessionPath);
|
|
40
|
+
settle();
|
|
41
|
+
throw new RpcSessionRegistryError("session_path_in_use", undefined, {
|
|
42
|
+
owner: holder,
|
|
43
|
+
retry_after_ms: SESSION_PATH_RETRY_AFTER_MS,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Waits out another open of this path that is still taking its claim (senpi#3485). Without the wait
|
|
48
|
+
* the path looked reserved by no session at all and a reopen was refused for a session about to be
|
|
49
|
+
* served. Bounded by the close grace window: a claim that never settles (a wedged daemon directory)
|
|
50
|
+
* ends this open with a retryable `session_path_in_use { reason: "claim_in_progress" }`.
|
|
51
|
+
*/
|
|
52
|
+
export async function settlePendingClaim(pending, sessionPath, graceMs) {
|
|
53
|
+
let timer;
|
|
54
|
+
const late = new Promise((resolve) => {
|
|
55
|
+
timer = setTimeout(() => resolve("late"), graceMs);
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
for (let claim = pending.get(sessionPath); claim !== undefined; claim = pending.get(sessionPath)) {
|
|
59
|
+
if ((await Promise.race([claim.then(() => "settled"), late])) === "late")
|
|
60
|
+
throw new RpcSessionRegistryError("session_path_in_use", "another open of this path is still taking its claim", {
|
|
61
|
+
reason: "claim_in_progress",
|
|
62
|
+
retry_after_ms: SESSION_PATH_RETRY_AFTER_MS,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
2
70
|
/** Reconcile path and durable identity after runtime replacement. */
|
|
3
71
|
export function syncRuntimeMetadata(entries, reservations, pathReservations) {
|
|
4
72
|
for (const entry of entries) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SessionStartEvent } from "../../core/extensions/types.ts";
|
|
2
|
+
import type { PreparableRuntimeFactory } from "./host-warm.ts";
|
|
3
|
+
import { type RpcSessionEntry, type RpcSessionLaunchProfile, type RpcSessionOpenOptions } from "./session-registry-types.ts";
|
|
4
|
+
export interface OpeningEntry {
|
|
5
|
+
readonly entry: RpcSessionEntry;
|
|
6
|
+
readonly storedProfile: RpcSessionLaunchProfile;
|
|
7
|
+
readonly runtimeProfile: RpcSessionLaunchProfile;
|
|
8
|
+
readonly sessionStartEvent: SessionStartEvent | undefined;
|
|
9
|
+
readonly settleOpen: {
|
|
10
|
+
readonly resolve: () => void;
|
|
11
|
+
readonly reject: (cause: unknown) => void;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare function buildOpeningEntry(input: {
|
|
15
|
+
readonly profile: RpcSessionLaunchProfile;
|
|
16
|
+
readonly sessionPath: string | undefined;
|
|
17
|
+
readonly requestedDurableId: string | undefined;
|
|
18
|
+
readonly options: RpcSessionOpenOptions | undefined;
|
|
19
|
+
readonly now: number;
|
|
20
|
+
readonly createRuntime: PreparableRuntimeFactory;
|
|
21
|
+
readonly sync: () => void;
|
|
22
|
+
}): OpeningEntry;
|
|
23
|
+
//# sourceMappingURL=session-registry-opening.d.ts.map
|