@code-yeongyu/senpi 2026.11.6-2 → 2026.11.6-3
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 +30 -0
- package/dist/bundle/chunks/{bun-extension-importer-RCGPQTI2.js → bun-extension-importer-GQ6IKEXD.js} +2 -2
- package/dist/bundle/chunks/{chunk-QN2BHP3M.js → chunk-44BJPS5I.js} +1 -1
- package/dist/bundle/chunks/{chunk-C7OKUC7L.js → chunk-4BATXPB7.js} +1 -1
- package/dist/bundle/chunks/{chunk-RQ6CHXQ2.js → chunk-5LKB732P.js} +1 -1
- package/dist/bundle/chunks/{chunk-X5ZVYNOL.js → chunk-6CEDL5S2.js} +1 -1
- package/dist/bundle/chunks/{chunk-HLM46XEB.js → chunk-7DRWPZ3N.js} +1 -1
- package/dist/bundle/chunks/{chunk-7PSBD5KS.js → chunk-BASS47UD.js} +1 -1
- package/dist/bundle/chunks/{chunk-ECW7VBMB.js → chunk-CFEHJ4VU.js} +1 -1
- package/dist/bundle/chunks/{chunk-TIPKAMSS.js → chunk-EO2VPZ2K.js} +1 -1
- package/dist/bundle/chunks/{chunk-LNPAT22L.js → chunk-F5JLDCYD.js} +8 -7
- package/dist/bundle/chunks/{chunk-GSKCWXXG.js → chunk-JMQOLOC2.js} +1 -1
- package/dist/bundle/chunks/chunk-L7ZIS3MC.js +106 -0
- package/dist/bundle/chunks/{chunk-47PPZKPO.js → chunk-LM6DOWQH.js} +4 -4
- package/dist/bundle/chunks/chunk-LPYBKMPT.js +2 -0
- package/dist/bundle/chunks/{chunk-GNSMNSL7.js → chunk-LUZVGPQ7.js} +2 -2
- package/dist/bundle/chunks/{chunk-7III44PV.js → chunk-MJTCZUXS.js} +1 -1
- package/dist/bundle/chunks/{chunk-2FDW2LQL.js → chunk-SB6MXNO5.js} +1 -1
- package/dist/bundle/chunks/chunk-UODSYQ2D.js +2 -0
- package/dist/bundle/chunks/{chunk-D5MRJWUK.js → chunk-V2QAYGOK.js} +216 -224
- package/dist/bundle/chunks/{chunk-WGRKM3N6.js → chunk-X53PD4ZC.js} +1 -1
- package/dist/bundle/chunks/{cli-main-T74YJGIF.js → cli-main-RWHDZKTQ.js} +1 -1
- package/dist/bundle/chunks/{dispatch-BAK2YZKD.js → dispatch-VDIYSXQN.js} +1 -1
- package/dist/bundle/chunks/{hand-entry-AYRUGE2A.js → hand-entry-G5LYF62Q.js} +1 -1
- package/dist/bundle/chunks/{host-command-YRXZE4TV.js → host-command-I2JZ5JM7.js} +1 -1
- package/dist/bundle/chunks/{host-session-command-N7IN7ZDV.js → host-session-command-Y52NFPP2.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-PA7DGVS6.js +2 -0
- package/dist/bundle/chunks/main-OBL6375C.js +2 -0
- package/dist/bundle/chunks/{multi-session-host-XSUJDHFZ.js → multi-session-host-NM6I5BWZ.js} +20 -17
- package/dist/bundle/chunks/{package-manager-cli-GLUNPQJU.js → package-manager-cli-GNRQ5MVR.js} +1 -1
- package/dist/bundle/chunks/{role-RRFFHZF5.js → role-F56BS2ZQ.js} +1 -1
- package/dist/bundle/chunks/rpc-mode-G4KIP7XS.js +2 -0
- package/dist/bundle/chunks/{run-attached-tui-W236SEOR.js → run-attached-tui-XTGPAGYM.js} +1 -1
- package/dist/bundle/chunks/{runtime-session-OBXYK4PI.js → runtime-session-VO3X4OE3.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-B2EJTBOE.js → schedule-command-L4M3TSQ3.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-YIMLHGLG.js → session-control-endpoint-V46WMCDF.js} +1 -1
- package/dist/bundle/chunks/{session-picker-E6ZONUJB.js → session-picker-AG6KSR5A.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +87 -83
- 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 +2 -0
- package/dist/core/agent-session-runtime.js +9 -2
- package/dist/core/agent-session.d.ts +24 -3
- package/dist/core/agent-session.js +94 -28
- package/dist/core/credential-pool/rotation-slots.d.ts +3 -3
- package/dist/core/credential-pool/state-store.d.ts +1 -1
- package/dist/core/egress/hpke.d.ts +63 -0
- package/dist/core/egress/hpke.js +188 -0
- package/dist/core/egress/placement-claim.d.ts +69 -0
- package/dist/core/egress/placement-claim.js +115 -0
- package/dist/core/egress/pool-state.d.ts +18 -0
- package/dist/core/egress/pool-state.js +5 -0
- package/dist/core/egress/sealed-format.d.ts +101 -0
- package/dist/core/egress/sealed-format.js +174 -0
- package/dist/core/egress/wire.d.ts +226 -0
- package/dist/core/egress/wire.js +47 -0
- package/dist/core/extensions/builtin/ask-user/escape-residue.d.ts +50 -0
- package/dist/core/extensions/builtin/ask-user/escape-residue.js +113 -0
- package/dist/core/extensions/builtin/ask-user/extension.js +7 -1
- package/dist/core/extensions/builtin/ask-user/resume.js +2 -1
- package/dist/core/extensions/builtin/ask-user/tool.d.ts +3 -0
- package/dist/core/extensions/builtin/ask-user/tool.js +8 -0
- package/dist/core/extensions/builtin/goal/index.js +46 -0
- package/dist/core/extensions/builtin/goal/stop-pause.d.ts +1 -0
- package/dist/core/extensions/builtin/goal/stop-pause.js +4 -2
- package/dist/core/extensions/builtin/permission-system/index.js +10 -4
- package/dist/core/extensions/builtin/permission-system/preset-registry.d.ts +2 -0
- package/dist/core/extensions/builtin/permission-system/preset-registry.js +11 -0
- package/dist/core/extensions/builtin/permission-system/settings.d.ts +2 -0
- package/dist/core/extensions/builtin/permission-system/settings.js +10 -1
- package/dist/core/extensions/builtin/terminal/tools/bash.d.ts +1 -1
- package/dist/core/extensions/builtin/terminal/tools/bash.js +38 -9
- package/dist/core/extensions/builtin/terminal/tools/context.d.ts +3 -0
- package/dist/core/extensions/bun-extension-importer.js +4 -3
- package/dist/core/extensions/bun-extension-load-turns.d.ts +4 -0
- package/dist/core/extensions/bun-extension-load-turns.js +45 -0
- package/dist/core/extensions/bun-extension-registry.d.ts +1 -0
- package/dist/core/extensions/bun-extension-registry.js +19 -1
- package/dist/core/extensions/loader.js +5 -0
- package/dist/core/extensions/session-dialog-api.d.ts +5 -0
- package/dist/core/extensions/session-dialog-api.js +13 -0
- package/dist/core/extensions/types.d.ts +14 -1
- package/dist/core/permission-decision.d.ts +22 -0
- package/dist/core/permission-decision.js +53 -0
- package/dist/core/permission-mode.d.ts +56 -0
- package/dist/core/permission-mode.js +133 -0
- package/dist/core/retry-fallback/controller-types.d.ts +6 -0
- package/dist/core/retry-fallback/controller.d.ts +8 -0
- package/dist/core/retry-fallback/controller.js +31 -3
- package/dist/core/retry-fallback/unusable.d.ts +23 -1
- package/dist/core/retry-fallback/unusable.js +80 -1
- package/dist/core/retry-fallback/usage-limit.d.ts +1 -1
- package/dist/core/retry-fallback/usage-limit.js +6 -3
- package/dist/core/sdk.js +2 -0
- package/dist/core/session-log.js +1 -1
- package/dist/core/stall-warning.d.ts +41 -0
- package/dist/core/stall-warning.js +72 -0
- package/dist/core/turn-origin.d.ts +27 -0
- package/dist/core/turn-origin.js +54 -0
- package/dist/modes/rpc/client-input-handler.d.ts +2 -0
- package/dist/modes/rpc/client-input-handler.js +2 -0
- package/dist/modes/rpc/connection-handler.js +16 -5
- package/dist/modes/rpc/connection-roles.d.ts +6 -0
- package/dist/modes/rpc/connection-roles.js +13 -0
- package/dist/modes/rpc/delegated-drive-origin.d.ts +9 -0
- package/dist/modes/rpc/delegated-drive-origin.js +16 -0
- package/dist/modes/rpc/effect-commands.d.ts +6 -0
- package/dist/modes/rpc/host-bare-endpoint-registration.d.ts +7 -0
- package/dist/modes/rpc/host-bare-endpoint-registration.js +35 -0
- package/dist/modes/rpc/host-capabilities.d.ts +2 -0
- package/dist/modes/rpc/host-capabilities.js +3 -0
- package/dist/modes/rpc/host-handover-parked.d.ts +11 -0
- package/dist/modes/rpc/host-handover-parked.js +17 -0
- package/dist/modes/rpc/host-handover-reopen.js +18 -6
- package/dist/modes/rpc/host-handover-resident-writer.d.ts +16 -0
- package/dist/modes/rpc/host-handover-resident-writer.js +57 -0
- package/dist/modes/rpc/host-warm.d.ts +16 -1
- package/dist/modes/rpc/host-warm.js +33 -6
- package/dist/modes/rpc/method-classes-schema-session.d.ts +9 -0
- package/dist/modes/rpc/method-classes-schema-session.js +1 -0
- package/dist/modes/rpc/method-classes-schema.d.ts +9 -0
- package/dist/modes/rpc/method-classes.d.ts +1 -0
- package/dist/modes/rpc/method-classes.js +2 -0
- package/dist/modes/rpc/multi-session-host.js +6 -1
- package/dist/modes/rpc/permission-preset-command.d.ts +6 -0
- package/dist/modes/rpc/permission-preset-command.js +27 -0
- package/dist/modes/rpc/rpc-mutation-response-types.d.ts +7 -0
- package/dist/modes/rpc/rpc-session-command-types.d.ts +4 -0
- package/dist/modes/rpc/rpc-session-event-types.d.ts +5 -2
- package/dist/modes/rpc/rpc-session-state-types.d.ts +2 -1
- package/dist/modes/rpc/rpc-session-state.js +2 -0
- package/dist/modes/rpc/session-attachment-commands.js +3 -3
- package/dist/modes/rpc/session-event-writer-connections.js +1 -0
- package/dist/modes/rpc/session-event-writer.d.ts +1 -1
- package/dist/modes/rpc/session-registry-attach.d.ts +0 -9
- package/dist/modes/rpc/session-registry-attach.js +16 -3
- package/dist/modes/rpc/session-registry-types.d.ts +2 -0
- package/dist/modes/rpc/session-registry.d.ts +2 -0
- package/dist/modes/rpc/session-registry.js +16 -3
- package/dist/modes/rpc/session-router-close.js +43 -6
- package/dist/modes/rpc/session-router-context.js +2 -0
- package/dist/modes/rpc/session-router-occupancy.js +2 -11
- package/dist/modes/rpc/session-router-open-profile.js +4 -0
- package/dist/modes/rpc/session-router-open.d.ts +1 -1
- package/dist/modes/rpc/session-router-open.js +6 -17
- package/dist/modes/rpc/session-router-residency.d.ts +24 -0
- package/dist/modes/rpc/session-router-residency.js +90 -0
- package/dist/modes/rpc/session-router-routing.js +7 -1
- package/dist/modes/rpc/session-status.js +4 -1
- package/dist/modes/rpc/session-worker.js +4 -0
- package/dist/modes/rpc/worker-session-registry.js +5 -2
- package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
- package/docs/engine-socket/fixtures/rpc-inbound-samples.json +12 -2
- package/docs/extensions.md +12 -0
- package/docs/rpc.md +72 -12
- package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
- package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-C7HEEBFH.js +0 -2
- package/dist/bundle/chunks/chunk-XCMII2VQ.js +0 -98
- package/dist/bundle/chunks/interactive-mode-B5SQDLH7.js +0 -2
- package/dist/bundle/chunks/main-UQFVFCNY.js +0 -2
- package/dist/bundle/chunks/rpc-mode-JYN3E7P7.js +0 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { parsePermissionPresetName } from "./extensions/builtin/permission-system/cli.js";
|
|
2
|
+
import { registerDecisionPresetOverride } from "./extensions/builtin/permission-system/preset-registry.js";
|
|
3
|
+
import { decidingTurnIsDelegated } from "./turn-origin.js";
|
|
4
|
+
const liveSessions = new Map();
|
|
5
|
+
/**
|
|
6
|
+
* Binds the remote approval-mode rule to one live session, evaluated at EACH gated decision:
|
|
7
|
+
* 1. a preset a client named for the session wins (no override);
|
|
8
|
+
* 2. delegated-origin work asks, or uses the mode a client recorded in the session file when the operator named none.
|
|
9
|
+
* It is decided before any `spawnedBy` inheritance, because `spawnedBy` is a client-supplied claim: naming a
|
|
10
|
+
* parent, real or made up, can never take a remote device's turn off this rule;
|
|
11
|
+
* 3. a child (`spawnedBy`) follows its live parent's mode for the parent's current turn; a child whose parent is not
|
|
12
|
+
* live in this process asks, unless the operator named a preset for it with the launch flag;
|
|
13
|
+
* 4. anything else keeps the host's own default (no override).
|
|
14
|
+
*/
|
|
15
|
+
export function bindDecisionPreset(session) {
|
|
16
|
+
liveSessions.set(session.sessionManager.getSessionId(), session);
|
|
17
|
+
registerDecisionPresetOverride(session.sessionManager, () => decisionPreset(session, new Set()));
|
|
18
|
+
}
|
|
19
|
+
export function releaseDecisionPreset(sessionManager) {
|
|
20
|
+
const id = sessionManager.getSessionId();
|
|
21
|
+
if (liveSessions.get(id)?.sessionManager === sessionManager)
|
|
22
|
+
liveSessions.delete(id);
|
|
23
|
+
}
|
|
24
|
+
function decisionPreset(session, visited) {
|
|
25
|
+
if (session.namedPreset() !== undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (decidingTurnIsDelegated(session.sessionManager))
|
|
28
|
+
return delegatedPreset(session);
|
|
29
|
+
const parentId = session.sessionManager.getHeader()?.spawnedBy?.parentSessionId;
|
|
30
|
+
if (parentId === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
const parent = liveSessions.get(parentId);
|
|
33
|
+
if (parent === undefined)
|
|
34
|
+
return session.launchFlagNamed() ? undefined : "ask";
|
|
35
|
+
// A `spawnedBy` loop (only a crafted session file can make one) has no parent to follow: it asks.
|
|
36
|
+
if (visited.has(parent) || parent === session)
|
|
37
|
+
return "ask";
|
|
38
|
+
visited.add(session);
|
|
39
|
+
return effectivePresetOf(parent, visited);
|
|
40
|
+
}
|
|
41
|
+
function effectivePresetOf(session, visited) {
|
|
42
|
+
const named = session.namedPreset();
|
|
43
|
+
if (named !== undefined)
|
|
44
|
+
return parsePermissionPresetName(named);
|
|
45
|
+
return decisionPreset(session, visited);
|
|
46
|
+
}
|
|
47
|
+
function delegatedPreset(session) {
|
|
48
|
+
if (session.launchFlagNamed())
|
|
49
|
+
return "ask";
|
|
50
|
+
const recorded = session.recordedPreset();
|
|
51
|
+
return (recorded === undefined ? undefined : parsePermissionPresetName(recorded)) ?? "ask";
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=permission-decision.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PermissionPresetName } from "./extensions/builtin/permission-system/types.ts";
|
|
2
|
+
import type { SessionManager } from "./session-manager.ts";
|
|
3
|
+
import type { SettingsManager } from "./settings-manager.ts";
|
|
4
|
+
/** `client`: a client named a valid preset for this session. `host_default`: the host's own default applies. */
|
|
5
|
+
export type PermissionPresetSource = "client" | "host_default";
|
|
6
|
+
/** `invalid`: the named preset (or the host's settings) cannot be parsed; every gated tool call is refused. */
|
|
7
|
+
export type PermissionPresetStatus = "applied" | "invalid";
|
|
8
|
+
/** The approval mode a session enforces, as `get_state`, the open reply and `set_permission_preset` report it. */
|
|
9
|
+
export interface PermissionMode {
|
|
10
|
+
readonly permissionPreset: string;
|
|
11
|
+
readonly permissionPresetSource: PermissionPresetSource;
|
|
12
|
+
readonly permissionPresetStatus: PermissionPresetStatus;
|
|
13
|
+
/** What the next gated decision uses; `null` while the setup is invalid (every gated call is refused). */
|
|
14
|
+
readonly effectivePermissionPreset: PermissionPresetName | null;
|
|
15
|
+
}
|
|
16
|
+
/** The mode fields `set_permission_preset` and the open reply confirm a requested preset by. */
|
|
17
|
+
export type PermissionModeReply = Pick<PermissionMode, "permissionPreset" | "permissionPresetSource" | "permissionPresetStatus">;
|
|
18
|
+
type ModeSession = Parameters<typeof permissionModeOf>[0] & {
|
|
19
|
+
readonly sessionManager: Pick<SessionManager, "getEntries" | "appendCustomEntry" | "getHeader" | "getSessionId">;
|
|
20
|
+
readonly extensionRunner: {
|
|
21
|
+
emitBusEvent(channel: string, data: unknown): void;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export declare function markHostDefaultPresetFlag(session: object, profile: {
|
|
25
|
+
readonly permissionPresetSource?: "host_default";
|
|
26
|
+
} | undefined): void;
|
|
27
|
+
export declare function permissionModeOf(session: {
|
|
28
|
+
readonly sessionManager: object;
|
|
29
|
+
readonly settingsManager: SettingsManager;
|
|
30
|
+
readonly extensionRunner: {
|
|
31
|
+
getFlagValues(): ReadonlyMap<string, boolean | string>;
|
|
32
|
+
};
|
|
33
|
+
}): PermissionMode;
|
|
34
|
+
/** The preset a launch profile names as a client's choice; an embedder's host default is not one. */
|
|
35
|
+
export declare function clientNamedPreset(profile: {
|
|
36
|
+
readonly permissionPreset?: string;
|
|
37
|
+
readonly permissionPresetSource?: "host_default";
|
|
38
|
+
} | undefined): string | undefined;
|
|
39
|
+
/** A profile whose preset a client just named: the host-default marker no longer applies. */
|
|
40
|
+
export declare function withClientPreset<P extends {
|
|
41
|
+
readonly permissionPreset?: string;
|
|
42
|
+
readonly permissionPresetSource?: "host_default";
|
|
43
|
+
}>(profile: P, preset: string): Readonly<P>;
|
|
44
|
+
export declare function permissionModeReply(mode: PermissionMode): PermissionModeReply;
|
|
45
|
+
export declare function openReplyPermissionMode(state: Partial<PermissionMode>): Partial<PermissionModeReply>;
|
|
46
|
+
/**
|
|
47
|
+
* Binds the preset a client named to the live session: reported as `client`, persisted to its file, and, for a change
|
|
48
|
+
* to a running session (`announce`), published on the session's `permission_preset_changed` bus channel.
|
|
49
|
+
*/
|
|
50
|
+
/** Binds the per-decision approval-mode rule to a session, wherever it was created (runtime, SDK, in-process child). */
|
|
51
|
+
export declare function bindPermissionDecision(session: ModeSession): void;
|
|
52
|
+
export declare function applyClientPermissionMode(session: ModeSession, preset: string | undefined, options: {
|
|
53
|
+
readonly announce: boolean;
|
|
54
|
+
}): void;
|
|
55
|
+
export {};
|
|
56
|
+
//# sourceMappingURL=permission-mode.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { parsePermissionPresetName } from "./extensions/builtin/permission-system/cli.js";
|
|
2
|
+
import { readPermissionPreset } from "./extensions/builtin/permission-system/preset-registry.js";
|
|
3
|
+
import { resolvePermissionPreset } from "./extensions/builtin/permission-system/settings.js";
|
|
4
|
+
import { bindDecisionPreset } from "./permission-decision.js";
|
|
5
|
+
const PERMISSION_PRESET_FLAG = "permission-preset";
|
|
6
|
+
const PERMISSION_PRESET_CHANGED_CHANNEL = "permission_preset_changed";
|
|
7
|
+
const PERMISSION_MODE_ENTRY = "permission_mode";
|
|
8
|
+
const clientPresets = new WeakMap();
|
|
9
|
+
// Sessions whose `permission-preset` flag holds the host's own default (an embedder's host option), not an operator flag.
|
|
10
|
+
const hostDefaultFlagSessions = new WeakSet();
|
|
11
|
+
export function markHostDefaultPresetFlag(session, profile) {
|
|
12
|
+
if (profile?.permissionPresetSource === "host_default")
|
|
13
|
+
hostDefaultFlagSessions.add(session);
|
|
14
|
+
else
|
|
15
|
+
hostDefaultFlagSessions.delete(session);
|
|
16
|
+
}
|
|
17
|
+
function markClientPermissionPreset(session, preset) {
|
|
18
|
+
if (preset === undefined)
|
|
19
|
+
clientPresets.delete(session);
|
|
20
|
+
else
|
|
21
|
+
clientPresets.set(session, preset);
|
|
22
|
+
}
|
|
23
|
+
export function permissionModeOf(session) {
|
|
24
|
+
const named = clientPresets.get(session);
|
|
25
|
+
const namedPreset = named === undefined ? undefined : parsePermissionPresetName(named);
|
|
26
|
+
if (namedPreset !== undefined)
|
|
27
|
+
return clientMode(namedPreset, readPermissionPreset(session.sessionManager));
|
|
28
|
+
const hostFlag = named === undefined ? session.extensionRunner.getFlagValues().get(PERMISSION_PRESET_FLAG) : named;
|
|
29
|
+
const hostPreset = typeof hostFlag === "string" ? parsePermissionPresetName(hostFlag) : undefined;
|
|
30
|
+
if (typeof hostFlag === "string" && hostPreset === undefined)
|
|
31
|
+
return invalidMode(hostFlag);
|
|
32
|
+
let preset;
|
|
33
|
+
try {
|
|
34
|
+
preset = resolvePermissionPreset(session.settingsManager, hostPreset);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return invalidMode(typeof hostFlag === "string" ? hostFlag : "");
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
permissionPreset: preset,
|
|
41
|
+
permissionPresetSource: "host_default",
|
|
42
|
+
permissionPresetStatus: "applied",
|
|
43
|
+
effectivePermissionPreset: readPermissionPreset(session.sessionManager) ?? preset,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// The live permission system is the authority on what it enforces; the named preset stands in only while no
|
|
47
|
+
// permission system has registered for the session yet (it loads with the session's extensions).
|
|
48
|
+
function clientMode(preset, enforced) {
|
|
49
|
+
return {
|
|
50
|
+
permissionPreset: preset,
|
|
51
|
+
permissionPresetSource: "client",
|
|
52
|
+
permissionPresetStatus: "applied",
|
|
53
|
+
effectivePermissionPreset: enforced ?? preset,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// An invalid preset never counts as one a client set: rule 1 of the delegated-default design must never select an
|
|
57
|
+
// unusable preset, so the source stays the host's while every gated call is refused (#2617).
|
|
58
|
+
function invalidMode(preset) {
|
|
59
|
+
return {
|
|
60
|
+
permissionPreset: preset,
|
|
61
|
+
permissionPresetSource: "host_default",
|
|
62
|
+
permissionPresetStatus: "invalid",
|
|
63
|
+
effectivePermissionPreset: null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** The preset a launch profile names as a client's choice; an embedder's host default is not one. */
|
|
67
|
+
export function clientNamedPreset(profile) {
|
|
68
|
+
return profile?.permissionPresetSource === "host_default" ? undefined : profile?.permissionPreset;
|
|
69
|
+
}
|
|
70
|
+
/** A profile whose preset a client just named: the host-default marker no longer applies. */
|
|
71
|
+
export function withClientPreset(profile, preset) {
|
|
72
|
+
const { permissionPresetSource: _hostDefault, ...rest } = profile;
|
|
73
|
+
return Object.freeze({ ...rest, permissionPreset: preset });
|
|
74
|
+
}
|
|
75
|
+
export function permissionModeReply(mode) {
|
|
76
|
+
return {
|
|
77
|
+
permissionPreset: mode.permissionPreset,
|
|
78
|
+
permissionPresetSource: mode.permissionPresetSource,
|
|
79
|
+
permissionPresetStatus: mode.permissionPresetStatus,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function persistedClientPreset(sessionManager) {
|
|
83
|
+
const entries = sessionManager.getEntries();
|
|
84
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
85
|
+
const entry = entries[index];
|
|
86
|
+
if (entry?.type !== "custom" || entry.customType !== PERMISSION_MODE_ENTRY)
|
|
87
|
+
continue;
|
|
88
|
+
const data = entry.data;
|
|
89
|
+
if (typeof data !== "object" || data === null || !("preset" in data))
|
|
90
|
+
return undefined;
|
|
91
|
+
return typeof data.preset === "string" && parsePermissionPresetName(data.preset) ? data.preset : undefined;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
export function openReplyPermissionMode(state) {
|
|
96
|
+
const { permissionPreset, permissionPresetSource, permissionPresetStatus } = state;
|
|
97
|
+
if (permissionPreset === undefined || permissionPresetSource === undefined || permissionPresetStatus === undefined)
|
|
98
|
+
return {};
|
|
99
|
+
return { permissionPreset, permissionPresetSource, permissionPresetStatus };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Binds the preset a client named to the live session: reported as `client`, persisted to its file, and, for a change
|
|
103
|
+
* to a running session (`announce`), published on the session's `permission_preset_changed` bus channel.
|
|
104
|
+
*/
|
|
105
|
+
/** Binds the per-decision approval-mode rule to a session, wherever it was created (runtime, SDK, in-process child). */
|
|
106
|
+
export function bindPermissionDecision(session) {
|
|
107
|
+
bindDecisionPreset({
|
|
108
|
+
sessionManager: session.sessionManager,
|
|
109
|
+
namedPreset: () => clientPresets.get(session),
|
|
110
|
+
launchFlagNamed: () => !hostDefaultFlagSessions.has(session) &&
|
|
111
|
+
typeof session.extensionRunner.getFlagValues().get(PERMISSION_PRESET_FLAG) === "string",
|
|
112
|
+
recordedPreset: () => persistedClientPreset(session.sessionManager),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export function applyClientPermissionMode(session, preset, options) {
|
|
116
|
+
markClientPermissionPreset(session, preset);
|
|
117
|
+
persistClientPreset(session.sessionManager, preset);
|
|
118
|
+
bindPermissionDecision(session);
|
|
119
|
+
if (options.announce)
|
|
120
|
+
session.extensionRunner.emitBusEvent(PERMISSION_PRESET_CHANGED_CHANNEL, permissionModeOf(session));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Records a valid client-named preset as a `permission_mode` custom entry (bookkeeping only: nothing restores it
|
|
124
|
+
* on a later open). Writes only on a change; an invalid preset is never recorded.
|
|
125
|
+
*/
|
|
126
|
+
function persistClientPreset(sessionManager, preset) {
|
|
127
|
+
if (preset === undefined || parsePermissionPresetName(preset) === undefined)
|
|
128
|
+
return;
|
|
129
|
+
if (persistedClientPreset(sessionManager) === preset)
|
|
130
|
+
return;
|
|
131
|
+
sessionManager.appendCustomEntry(PERMISSION_MODE_ENTRY, { preset, source: "client" }, { startupBookkeeping: true });
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=permission-mode.js.map
|
|
@@ -65,6 +65,12 @@ export interface RetryFallbackControllerDeps {
|
|
|
65
65
|
from: string;
|
|
66
66
|
to: string;
|
|
67
67
|
cause?: FallbackRevertCause;
|
|
68
|
+
} | {
|
|
69
|
+
type: "usage_limit_scope_probe";
|
|
70
|
+
provider: string;
|
|
71
|
+
heldFamily: string;
|
|
72
|
+
probeFamily: string;
|
|
73
|
+
outcome: "served" | "widened";
|
|
68
74
|
}): void;
|
|
69
75
|
getCurrentSelector(): {
|
|
70
76
|
model: Model<Api>;
|
|
@@ -19,6 +19,13 @@ export declare class RetryFallbackController {
|
|
|
19
19
|
constructor(deps: RetryFallbackControllerDeps);
|
|
20
20
|
get activeState(): Readonly<ActiveFallbackState> | undefined;
|
|
21
21
|
get exhaustedChainKey(): string | undefined;
|
|
22
|
+
/** The weekly-limit family hold on `model`, while it lasts (senpi#5596). */
|
|
23
|
+
heldFamily(model: Model<Api>): {
|
|
24
|
+
readonly family: string;
|
|
25
|
+
readonly errorMessage: string;
|
|
26
|
+
} | undefined;
|
|
27
|
+
/** The families a no-family limit was confirmed on before it held the whole provider (senpi#5596). */
|
|
28
|
+
widenedFamiliesOf(provider: string): readonly string[] | undefined;
|
|
22
29
|
get turnRefusals(): readonly FallbackRungRefusal[];
|
|
23
30
|
isProviderSpent(provider: string): boolean;
|
|
24
31
|
accountError(provider: string): string | undefined;
|
|
@@ -77,6 +84,7 @@ export declare class RetryFallbackController {
|
|
|
77
84
|
noteProviderAccountsChanged(provider: string): void;
|
|
78
85
|
/** A response this session got from `selector` succeeded: the entry serves it again (senpi#3467). */
|
|
79
86
|
noteServed(selector: string): void;
|
|
87
|
+
private emitScopeProbe;
|
|
80
88
|
/**
|
|
81
89
|
* Records a failure that makes the entry unusable for every session - billing
|
|
82
90
|
* or quota exhaustion, or a transient failure that spent its budget or failed a
|
|
@@ -32,6 +32,14 @@ export class RetryFallbackController {
|
|
|
32
32
|
get exhaustedChainKey() {
|
|
33
33
|
return this.lastExhaustedChainKey;
|
|
34
34
|
}
|
|
35
|
+
/** The weekly-limit family hold on `model`, while it lasts (senpi#5596). */
|
|
36
|
+
heldFamily(model) {
|
|
37
|
+
return this.unusable.heldFamily(model);
|
|
38
|
+
}
|
|
39
|
+
/** The families a no-family limit was confirmed on before it held the whole provider (senpi#5596). */
|
|
40
|
+
widenedFamiliesOf(provider) {
|
|
41
|
+
return this.unusable.widenedFamiliesOf(provider);
|
|
42
|
+
}
|
|
35
43
|
get turnRefusals() {
|
|
36
44
|
return [...this.refusals];
|
|
37
45
|
}
|
|
@@ -209,6 +217,18 @@ export class RetryFallbackController {
|
|
|
209
217
|
noteServed(selector) {
|
|
210
218
|
this.ownFailures.delete(selector);
|
|
211
219
|
this.probes.accept(selector);
|
|
220
|
+
const slash = selector.indexOf("/");
|
|
221
|
+
if (slash > 0)
|
|
222
|
+
this.emitScopeProbe(this.unusable.noteServed({
|
|
223
|
+
provider: selector.slice(0, slash),
|
|
224
|
+
id: selector.slice(slash + 1).split(":")[0] ?? "",
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
emitScopeProbe(probe) {
|
|
228
|
+
if (probe === undefined)
|
|
229
|
+
return;
|
|
230
|
+
this.deps.logger.info("usage_limit_scope_probe", { ...probe });
|
|
231
|
+
this.deps.emit({ type: "usage_limit_scope_probe", ...probe });
|
|
212
232
|
}
|
|
213
233
|
/**
|
|
214
234
|
* Records a failure that makes the entry unusable for every session - billing
|
|
@@ -216,7 +236,7 @@ export class RetryFallbackController {
|
|
|
216
236
|
* half-open probe - independent of whether a fallback candidate remains.
|
|
217
237
|
*/
|
|
218
238
|
noteHealthFailure(model, thinkingLevel, failure) {
|
|
219
|
-
this.unusable.note(model, failure);
|
|
239
|
+
this.emitScopeProbe(this.unusable.note(model, failure));
|
|
220
240
|
if (!this.managedChainKey({ model, thinkingLevel }))
|
|
221
241
|
return;
|
|
222
242
|
// A pure network fault is the transport's, not the model's: it never opens the shared circuit (senpi#3306).
|
|
@@ -275,9 +295,17 @@ export class RetryFallbackController {
|
|
|
275
295
|
// noteHealthFailure) so the final chain entry opens too.
|
|
276
296
|
if (failureObserved && current && reason === "transient")
|
|
277
297
|
this.noteHealthFailure(current.model, current.thinkingLevel, failure);
|
|
278
|
-
const limit = usageLimitScope(failure.errorMessage);
|
|
279
298
|
if (failureObserved && current)
|
|
280
|
-
this.unusable.note(current.model, failure);
|
|
299
|
+
this.emitScopeProbe(this.unusable.note(current.model, failure));
|
|
300
|
+
// A weekly limit that named no family held only the failing family (senpi#5596): report it as that model's
|
|
301
|
+
// limit, so the switch never claims the account's other models were skipped.
|
|
302
|
+
const scopeOfText = usageLimitScope(failure.errorMessage);
|
|
303
|
+
const limit = scopeOfText === "account" &&
|
|
304
|
+
current &&
|
|
305
|
+
!this.unusable.isProviderSpent(current.model.provider) &&
|
|
306
|
+
this.unusable.heldFamily(current.model)
|
|
307
|
+
? "model"
|
|
308
|
+
: scopeOfText;
|
|
281
309
|
// A model-wide park (transient, billing) can rule out the current model's other rungs, so the
|
|
282
310
|
// walk re-scans after noting it; the first scan logs only when no re-scan follows.
|
|
283
311
|
const modelWidePark = !networkBlip && failureObserved && (reason === "transient" || reason === "billing");
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { CircuitFailure } from "./controller-types.ts";
|
|
3
3
|
import type { SelectorCooldowns } from "./cooldown.ts";
|
|
4
|
+
/** A once-per-provider check of a limit that named no family (senpi#5596). */
|
|
5
|
+
export type UsageLimitScopeProbe = {
|
|
6
|
+
readonly provider: string;
|
|
7
|
+
readonly heldFamily: string;
|
|
8
|
+
readonly probeFamily: string;
|
|
9
|
+
readonly outcome: "served" | "widened";
|
|
10
|
+
};
|
|
4
11
|
/**
|
|
5
12
|
* Chain entries this session has seen refuse to serve for a reason retrying cannot
|
|
6
13
|
* fix within the cooldown: an account-wide usage limit or billing failure (every
|
|
@@ -11,8 +18,21 @@ import type { SelectorCooldowns } from "./cooldown.ts";
|
|
|
11
18
|
export declare class UnusableEntries {
|
|
12
19
|
private readonly cooldowns;
|
|
13
20
|
private readonly accountErrors;
|
|
21
|
+
/** Providers whose limit named no family: the family it hit, awaiting one request on another family. */
|
|
22
|
+
private readonly pendingProbes;
|
|
23
|
+
private readonly widenedFamilies;
|
|
24
|
+
private readonly familyErrors;
|
|
14
25
|
constructor(cooldowns: SelectorCooldowns);
|
|
15
|
-
note(model: Model<Api>, failure: CircuitFailure):
|
|
26
|
+
note(model: Model<Api>, failure: CircuitFailure): UsageLimitScopeProbe | undefined;
|
|
27
|
+
/** A response served on `model`: a pending family probe on another family resolves as served. */
|
|
28
|
+
noteServed(model: Pick<Model<Api>, "provider" | "id">): UsageLimitScopeProbe | undefined;
|
|
29
|
+
/** The weekly-limit family hold on `model`, while it lasts (senpi#5596). */
|
|
30
|
+
heldFamily(model: Model<Api>): {
|
|
31
|
+
readonly family: string;
|
|
32
|
+
readonly errorMessage: string;
|
|
33
|
+
} | undefined;
|
|
34
|
+
/** The two families a no-family limit was confirmed on before it widened to the account. */
|
|
35
|
+
widenedFamiliesOf(provider: string): readonly string[] | undefined;
|
|
16
36
|
isProviderSpent(provider: string): boolean;
|
|
17
37
|
accountError(provider: string): string | undefined;
|
|
18
38
|
/** Wall-clock time the provider said its account limit resets; undefined when the hold is a guessed default. */
|
|
@@ -26,5 +46,7 @@ export declare class UnusableEntries {
|
|
|
26
46
|
clear(model: Model<Api>): void;
|
|
27
47
|
/** The provider's accounts changed (a login, an added or re-authorized account): every hold it earned is stale. */
|
|
28
48
|
clearProvider(provider: string, models: readonly Model<Api>[]): void;
|
|
49
|
+
/** A family hold and its pending probe go wherever the provider's own holds go (senpi#5596). */
|
|
50
|
+
private clearFamilyHolds;
|
|
29
51
|
}
|
|
30
52
|
//# sourceMappingURL=unusable.d.ts.map
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { formatSelector } from "./chains.js";
|
|
2
2
|
import { isHealthExhaustionFailure } from "./circuit-probes.js";
|
|
3
|
+
import { rateLimitModelFamily } from "../credential-pool/model-scope.js";
|
|
4
|
+
import { isBillingErrorMessage } from "./billing.js";
|
|
3
5
|
import { usageLimitScope } from "./usage-limit.js";
|
|
6
|
+
// A weekly window binds the account (seven_day) or one family (seven_day_opus/...); the text alone cannot tell.
|
|
7
|
+
const WEEKLY_LIMIT = /\bweekly\b[^.]*\blimit\b/i;
|
|
8
|
+
const MODEL_FAMILY = /(?:^|[^a-z])(opus|sonnet|haiku|fable|mythos)(?:[^a-z]|$)/i;
|
|
9
|
+
/** The model family a model id belongs to, when it names one. */
|
|
10
|
+
function modelFamilyOf(modelId) {
|
|
11
|
+
return MODEL_FAMILY.exec(modelId)?.[1]?.toLowerCase();
|
|
12
|
+
}
|
|
4
13
|
/**
|
|
5
14
|
* Chain entries this session has seen refuse to serve for a reason retrying cannot
|
|
6
15
|
* fix within the cooldown: an account-wide usage limit or billing failure (every
|
|
@@ -11,10 +20,36 @@ import { usageLimitScope } from "./usage-limit.js";
|
|
|
11
20
|
export class UnusableEntries {
|
|
12
21
|
constructor(cooldowns) {
|
|
13
22
|
this.accountErrors = new Map();
|
|
23
|
+
/** Providers whose limit named no family: the family it hit, awaiting one request on another family. */
|
|
24
|
+
this.pendingProbes = new Map();
|
|
25
|
+
this.widenedFamilies = new Map();
|
|
26
|
+
this.familyErrors = new Map();
|
|
14
27
|
this.cooldowns = cooldowns;
|
|
15
28
|
}
|
|
16
29
|
note(model, failure) {
|
|
17
30
|
const scope = usageLimitScope(failure.errorMessage);
|
|
31
|
+
const family = modelFamilyOf(model.id);
|
|
32
|
+
// A subscription limit that names no family may bind only this family: hold it and let one request on
|
|
33
|
+
// another family decide, instead of marking every model of the account spent (senpi#5596).
|
|
34
|
+
if (scope === "account" &&
|
|
35
|
+
family !== undefined &&
|
|
36
|
+
failure.errorMessage !== undefined &&
|
|
37
|
+
WEEKLY_LIMIT.test(failure.errorMessage) &&
|
|
38
|
+
!isBillingErrorMessage(failure.errorMessage) &&
|
|
39
|
+
rateLimitModelFamily(failure.errorMessage) === undefined) {
|
|
40
|
+
const held = this.pendingProbes.get(model.provider);
|
|
41
|
+
if (held === undefined || held === family) {
|
|
42
|
+
this.pendingProbes.set(model.provider, family);
|
|
43
|
+
this.cooldowns.note(familyKey(model.provider, family), failure);
|
|
44
|
+
this.familyErrors.set(familyKey(model.provider, family), failure.errorMessage);
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
this.pendingProbes.delete(model.provider);
|
|
48
|
+
this.widenedFamilies.set(model.provider, [held, family]);
|
|
49
|
+
this.cooldowns.note(accountKey(model.provider), failure);
|
|
50
|
+
this.accountErrors.set(model.provider, failure.errorMessage);
|
|
51
|
+
return { provider: model.provider, heldFamily: held, probeFamily: family, outcome: "widened" };
|
|
52
|
+
}
|
|
18
53
|
if (scope === "account") {
|
|
19
54
|
this.cooldowns.note(accountKey(model.provider), failure);
|
|
20
55
|
if (failure.errorMessage)
|
|
@@ -23,6 +58,29 @@ export class UnusableEntries {
|
|
|
23
58
|
else if (scope === "model" || isHealthExhaustionFailure(failure.errorMessage)) {
|
|
24
59
|
this.cooldowns.note(entryKey(model), failure);
|
|
25
60
|
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/** A response served on `model`: a pending family probe on another family resolves as served. */
|
|
64
|
+
noteServed(model) {
|
|
65
|
+
const held = this.pendingProbes.get(model.provider);
|
|
66
|
+
const family = modelFamilyOf(model.id);
|
|
67
|
+
if (held === undefined || family === undefined || family === held)
|
|
68
|
+
return undefined;
|
|
69
|
+
this.pendingProbes.delete(model.provider);
|
|
70
|
+
return { provider: model.provider, heldFamily: held, probeFamily: family, outcome: "served" };
|
|
71
|
+
}
|
|
72
|
+
/** The weekly-limit family hold on `model`, while it lasts (senpi#5596). */
|
|
73
|
+
heldFamily(model) {
|
|
74
|
+
const family = modelFamilyOf(model.id);
|
|
75
|
+
if (family === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
const key = familyKey(model.provider, family);
|
|
78
|
+
const errorMessage = this.familyErrors.get(key);
|
|
79
|
+
return errorMessage !== undefined && this.cooldowns.isSuppressed(key) ? { family, errorMessage } : undefined;
|
|
80
|
+
}
|
|
81
|
+
/** The two families a no-family limit was confirmed on before it widened to the account. */
|
|
82
|
+
widenedFamiliesOf(provider) {
|
|
83
|
+
return this.isProviderSpent(provider) ? this.widenedFamilies.get(provider) : undefined;
|
|
26
84
|
}
|
|
27
85
|
isProviderSpent(provider) {
|
|
28
86
|
return this.cooldowns.isSuppressed(accountKey(provider));
|
|
@@ -43,16 +101,21 @@ export class UnusableEntries {
|
|
|
43
101
|
return false;
|
|
44
102
|
this.cooldowns.clear(accountKey(provider));
|
|
45
103
|
this.accountErrors.delete(provider);
|
|
104
|
+
this.clearFamilyHolds(provider);
|
|
46
105
|
return true;
|
|
47
106
|
}
|
|
48
107
|
isUnusable(model) {
|
|
49
|
-
|
|
108
|
+
const family = modelFamilyOf(model.id);
|
|
109
|
+
return (this.isProviderSpent(model.provider) ||
|
|
110
|
+
this.cooldowns.isSuppressed(entryKey(model)) ||
|
|
111
|
+
(family !== undefined && this.cooldowns.isSuppressed(familyKey(model.provider, family))));
|
|
50
112
|
}
|
|
51
113
|
clear(model) {
|
|
52
114
|
// A deliberate model selection allows the user to probe after reauth/reset.
|
|
53
115
|
this.cooldowns.clear(entryKey(model));
|
|
54
116
|
this.cooldowns.clear(accountKey(model.provider));
|
|
55
117
|
this.accountErrors.delete(model.provider);
|
|
118
|
+
this.clearFamilyHolds(model.provider);
|
|
56
119
|
}
|
|
57
120
|
/** The provider's accounts changed (a login, an added or re-authorized account): every hold it earned is stale. */
|
|
58
121
|
clearProvider(provider, models) {
|
|
@@ -62,6 +125,19 @@ export class UnusableEntries {
|
|
|
62
125
|
if (model.provider === provider)
|
|
63
126
|
this.cooldowns.clear(entryKey(model));
|
|
64
127
|
}
|
|
128
|
+
this.clearFamilyHolds(provider);
|
|
129
|
+
}
|
|
130
|
+
/** A family hold and its pending probe go wherever the provider's own holds go (senpi#5596). */
|
|
131
|
+
clearFamilyHolds(provider) {
|
|
132
|
+
const prefix = familyKey(provider, "");
|
|
133
|
+
for (const key of [...this.familyErrors.keys()]) {
|
|
134
|
+
if (!key.startsWith(prefix))
|
|
135
|
+
continue;
|
|
136
|
+
this.cooldowns.clear(key);
|
|
137
|
+
this.familyErrors.delete(key);
|
|
138
|
+
}
|
|
139
|
+
this.pendingProbes.delete(provider);
|
|
140
|
+
this.widenedFamilies.delete(provider);
|
|
65
141
|
}
|
|
66
142
|
}
|
|
67
143
|
function accountKey(provider) {
|
|
@@ -70,6 +146,9 @@ function accountKey(provider) {
|
|
|
70
146
|
// than the last surfaced account error. Explicit /model selection permits a probe.
|
|
71
147
|
return `account:${provider}`;
|
|
72
148
|
}
|
|
149
|
+
function familyKey(provider, family) {
|
|
150
|
+
return `family:${provider}:${family}`;
|
|
151
|
+
}
|
|
73
152
|
function entryKey(model) {
|
|
74
153
|
return `exhausted:${formatSelector(model)}`;
|
|
75
154
|
}
|
|
@@ -10,7 +10,7 @@ import type { SessionEntry } from "../session-manager.ts";
|
|
|
10
10
|
*/
|
|
11
11
|
export type UsageLimitScope = "model" | "account";
|
|
12
12
|
export declare class AccountUsageLimitError extends Error {
|
|
13
|
-
constructor(provider: string, raw?: string, resetsAt?: number);
|
|
13
|
+
constructor(provider: string, raw?: string, resetsAt?: number, families?: readonly string[]);
|
|
14
14
|
}
|
|
15
15
|
export declare function usageLimitScope(failure: string | undefined | Pick<AssistantMessage, "errorMessage" | "errorCause">): UsageLimitScope | undefined;
|
|
16
16
|
/** Project advice without mutating the diagnostic-bearing engine message. */
|
|
@@ -2,12 +2,15 @@ import { describeProviderFailureForUser, isQuotaExhaustionMessage } from "@earen
|
|
|
2
2
|
import { rateLimitModelFamily } from "../credential-pool/model-scope.js";
|
|
3
3
|
import { isBillingErrorMessage } from "./billing.js";
|
|
4
4
|
export class AccountUsageLimitError extends Error {
|
|
5
|
-
constructor(provider, raw = "usage limit reached", resetsAt) {
|
|
6
|
-
|
|
5
|
+
constructor(provider, raw = "usage limit reached", resetsAt, families) {
|
|
6
|
+
const message = describeProviderFailureForUser(raw, {
|
|
7
7
|
model: provider,
|
|
8
8
|
usageLimit: "account",
|
|
9
9
|
...(resetsAt === undefined ? {} : { resetsAt: formatResetTime(resetsAt) }),
|
|
10
|
-
})
|
|
10
|
+
});
|
|
11
|
+
super(families === undefined || families.length === 0
|
|
12
|
+
? message
|
|
13
|
+
: `${message} The limit named no model family; it was confirmed on ${families.join(" and ")}.`);
|
|
11
14
|
this.name = "AccountUsageLimitError";
|
|
12
15
|
}
|
|
13
16
|
}
|
package/dist/core/sdk.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { bindPermissionDecision } from "./permission-mode.js";
|
|
2
3
|
import { Agent, createInternalAgentChannel, setDefaultStreamFn, } from "@earendil-works/pi-agent-core";
|
|
3
4
|
import { modelsAreEqual, streamSimple } from "@earendil-works/pi-ai/compat";
|
|
4
5
|
import { getAgentDir } from "../config.js";
|
|
@@ -486,6 +487,7 @@ export async function createAgentSession(options = {}) {
|
|
|
486
487
|
sessionManager.appendThinkingLevelChange(thinkingLevel, thinkingSelection, undefined, model);
|
|
487
488
|
}
|
|
488
489
|
sessionRef.current = session;
|
|
490
|
+
bindPermissionDecision(session);
|
|
489
491
|
const extensionsResult = resourceLoader.getExtensions();
|
|
490
492
|
return {
|
|
491
493
|
session,
|
package/dist/core/session-log.js
CHANGED
|
@@ -7,7 +7,7 @@ const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
|
7
7
|
const MAX_STRING_LENGTH = 200;
|
|
8
8
|
const DEBUG_PREFIX = `[${APP_NAME}-session]`;
|
|
9
9
|
const BLOCKED_KEY = /^(?:__proto__|constructor|prototype|headers?|env(?:ironment)?|authorization|credential(?:s)?|password|secret|token|api_?key|client_?secret)$/i;
|
|
10
|
-
const ALLOWED_DATA_KEY = /^(?:action|attemptId|disposition|stage|error|mode|count|willRetry|deferAdmission|delivered|restored|cause|accepted|skipped|rejectionCause|reason|durationMs|kind|retryable|phase|op|bytes|generation|requestId|tokens|tokensBefore|tokensAfter|contextWindow|attempt|aborted|sessionId|role|provider|model|source|actor|from|to|duringTurn|persistDefault|extension|handlerEvent|queuedId|ageMs|seat|until|shared)$/;
|
|
10
|
+
const ALLOWED_DATA_KEY = /^(?:action|attemptId|disposition|stage|error|mode|count|willRetry|deferAdmission|delivered|restored|cause|accepted|skipped|rejectionCause|reason|durationMs|kind|retryable|phase|op|bytes|generation|requestId|tokens|tokensBefore|tokensAfter|contextWindow|attempt|aborted|sessionId|role|provider|model|source|actor|from|to|duringTurn|persistDefault|extension|handlerEvent|queuedId|ageMs|seat|until|shared|toolCallId|parentToolCallId|toolName|thresholdMs|elapsedMs|queueDepth|queueHead)$/;
|
|
11
11
|
const SENSITIVE_TEXT = /((?:authorization\s*[:=]\s*(?:bearer|basic)\s+)|(?:bearer\s+)|(?:[?&](?:api[_-]?key|token|secret|password|auth(?:orization)?)=))[^\s&,"'}\]]+/gi;
|
|
12
12
|
export function createSessionLogger(agentDir, options = {}) {
|
|
13
13
|
if (typeof agentDir !== "string" || agentDir.length === 0) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deadline-bounded warnings for waits that have no deadline of their own (senpi#6189).
|
|
3
|
+
*
|
|
4
|
+
* A wait that never settles leaves no trace: no timer fires, no result is written, and the
|
|
5
|
+
* session just idles. These helpers only observe such a wait. They log once when it outlives
|
|
6
|
+
* its threshold and never resolve, reject, abort or delay anything themselves.
|
|
7
|
+
*/
|
|
8
|
+
/** A wait still pending after this long is logged once. A bash spawn's own shorter timeout lowers it. */
|
|
9
|
+
export declare const STALL_WARNING_MS = 30000;
|
|
10
|
+
/** The warning threshold for a bash spawn whose own `timeout` (in seconds) may be shorter than {@link STALL_WARNING_MS}. */
|
|
11
|
+
export declare function stallThresholdMs(timeoutSeconds: unknown): number;
|
|
12
|
+
export declare function queueEntryLabel(event: {
|
|
13
|
+
readonly type: string;
|
|
14
|
+
}): string;
|
|
15
|
+
export interface StallWatch {
|
|
16
|
+
stop(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Call `onStall` once if `stop` has not run within `thresholdMs`. The caller keeps its own `await`, so the
|
|
20
|
+
* watched wait's outcome and ordering are unchanged. The timer is unref'd: it never keeps the process alive.
|
|
21
|
+
*/
|
|
22
|
+
export declare function watchForStall(thresholdMs: number, onStall: (elapsedMs: number) => void): StallWatch;
|
|
23
|
+
export interface PendingQueueSnapshot {
|
|
24
|
+
readonly queueDepth: number;
|
|
25
|
+
/** Label of the oldest unsettled entry: the one every later entry waits behind. */
|
|
26
|
+
readonly queueHead?: string;
|
|
27
|
+
readonly ageMs?: number;
|
|
28
|
+
}
|
|
29
|
+
/** The unsettled entries of a promise chain, oldest first, so a stalled wait on the chain can name what it waits behind. */
|
|
30
|
+
export declare class PendingQueueLedger {
|
|
31
|
+
private nextId;
|
|
32
|
+
private readonly entries;
|
|
33
|
+
/**
|
|
34
|
+
* Record `run` as an entry from now until it settles, and return it wrapped for the chain. The wrapper returns
|
|
35
|
+
* exactly what `run` returns or throws, and releases the entry from that result. It never attaches a handler to
|
|
36
|
+
* the chain itself, so a rejection on the chain is exactly as visible (or unhandled) as before.
|
|
37
|
+
*/
|
|
38
|
+
track<A extends unknown[], R>(label: string, run: (...args: A) => R): (...args: A) => R;
|
|
39
|
+
snapshot(): PendingQueueSnapshot;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=stall-warning.d.ts.map
|