@code-yeongyu/senpi 2026.8.26 → 2026.8.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +3 -3
- package/dist/cli/args.d.ts +3 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +14 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +10 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +67 -29
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/credential-pool/classify.d.ts +42 -0
- package/dist/core/credential-pool/classify.d.ts.map +1 -0
- package/dist/core/credential-pool/classify.js +75 -0
- package/dist/core/credential-pool/classify.js.map +1 -0
- package/dist/core/credential-pool/env-slots.d.ts +22 -0
- package/dist/core/credential-pool/env-slots.d.ts.map +1 -0
- package/dist/core/credential-pool/env-slots.js +37 -0
- package/dist/core/credential-pool/env-slots.js.map +1 -0
- package/dist/core/credential-pool/failover.d.ts +54 -0
- package/dist/core/credential-pool/failover.d.ts.map +1 -0
- package/dist/core/credential-pool/failover.js +106 -0
- package/dist/core/credential-pool/failover.js.map +1 -0
- package/dist/core/credential-pool/rotation-stream.d.ts +43 -0
- package/dist/core/credential-pool/rotation-stream.d.ts.map +1 -0
- package/dist/core/credential-pool/rotation-stream.js +96 -0
- package/dist/core/credential-pool/rotation-stream.js.map +1 -0
- package/dist/core/credential-pool/state-store.d.ts +89 -0
- package/dist/core/credential-pool/state-store.d.ts.map +1 -0
- package/dist/core/credential-pool/state-store.js +166 -0
- package/dist/core/credential-pool/state-store.js.map +1 -0
- package/dist/core/extensions/builtin/compaction/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/index.js +67 -0
- package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/log.d.ts +1 -1
- package/dist/core/extensions/builtin/compaction/log.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/log.js +1 -0
- package/dist/core/extensions/builtin/compaction/log.js.map +1 -1
- package/dist/core/extensions/builtin/config-reload/index.js +14 -22
- package/dist/core/extensions/builtin/config-reload/index.js.map +1 -1
- package/dist/core/extensions/builtin/goal/continuation-recovery.d.ts +1 -0
- package/dist/core/extensions/builtin/goal/continuation-recovery.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/continuation-recovery.js +2 -0
- package/dist/core/extensions/builtin/goal/continuation-recovery.js.map +1 -1
- package/dist/core/extensions/builtin/goal/continuation.d.ts +8 -1
- package/dist/core/extensions/builtin/goal/continuation.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/continuation.js +15 -0
- package/dist/core/extensions/builtin/goal/continuation.js.map +1 -1
- package/dist/core/extensions/builtin/goal/direct-input-lifecycle.js +2 -2
- package/dist/core/extensions/builtin/goal/direct-input-lifecycle.js.map +1 -1
- package/dist/core/extensions/builtin/goal/format.d.ts +6 -1
- package/dist/core/extensions/builtin/goal/format.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/format.js +6 -0
- package/dist/core/extensions/builtin/goal/format.js.map +1 -1
- package/dist/core/extensions/builtin/goal/lifecycle-helpers.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/lifecycle-helpers.js +5 -2
- package/dist/core/extensions/builtin/goal/lifecycle-helpers.js.map +1 -1
- package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/monitor-continuation.js +31 -7
- package/dist/core/extensions/builtin/goal/monitor-continuation.js.map +1 -1
- package/dist/core/extensions/builtin/goal/persistence.js +2 -0
- package/dist/core/extensions/builtin/goal/persistence.js.map +1 -1
- package/dist/core/extensions/builtin/goal/renderers.d.ts +14 -0
- package/dist/core/extensions/builtin/goal/renderers.d.ts.map +1 -0
- package/dist/core/extensions/builtin/goal/renderers.js +153 -0
- package/dist/core/extensions/builtin/goal/renderers.js.map +1 -0
- package/dist/core/extensions/builtin/goal/store.d.ts +6 -2
- package/dist/core/extensions/builtin/goal/store.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/store.js +8 -2
- package/dist/core/extensions/builtin/goal/store.js.map +1 -1
- package/dist/core/extensions/builtin/goal/tool-registration.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/tool-registration.js +16 -6
- package/dist/core/extensions/builtin/goal/tool-registration.js.map +1 -1
- package/dist/core/extensions/builtin/goal/types.d.ts +1 -0
- package/dist/core/extensions/builtin/goal/types.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/types.js.map +1 -1
- package/dist/core/extensions/builtin/todotools/index.js +1 -1
- package/dist/core/extensions/builtin/todotools/index.js.map +1 -1
- package/dist/core/extensions/builtin/todotools/native-todo-mirror.d.ts +1 -1
- package/dist/core/extensions/builtin/todotools/native-todo-mirror.d.ts.map +1 -1
- package/dist/core/extensions/builtin/todotools/native-todo-mirror.js +1 -1
- package/dist/core/extensions/builtin/todotools/native-todo-mirror.js.map +1 -1
- package/dist/core/hidden-stdout-log.d.ts +10 -0
- package/dist/core/hidden-stdout-log.d.ts.map +1 -1
- package/dist/core/hidden-stdout-log.js +36 -5
- package/dist/core/hidden-stdout-log.js.map +1 -1
- package/dist/core/model-config-schema.d.ts +85 -12
- package/dist/core/model-config-schema.d.ts.map +1 -1
- package/dist/core/model-config-schema.js +25 -0
- package/dist/core/model-config-schema.js.map +1 -1
- package/dist/core/model-runtime.d.ts +34 -1
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +98 -5
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts +2 -0
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +32 -7
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +26 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +1 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution-fallback.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution-fallback.js +82 -1
- package/dist/modes/interactive/components/tool-execution-fallback.js.map +1 -1
- package/dist/modes/interactive/interactive-host-runtime.d.ts +29 -0
- package/dist/modes/interactive/interactive-host-runtime.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-host-runtime.js +244 -0
- package/dist/modes/interactive/interactive-host-runtime.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +46 -8
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/connection-handler.d.ts.map +1 -1
- package/dist/modes/rpc/connection-handler.js +7 -1
- package/dist/modes/rpc/connection-handler.js.map +1 -1
- package/dist/modes/rpc/host-ensure.d.ts +41 -0
- package/dist/modes/rpc/host-ensure.d.ts.map +1 -0
- package/dist/modes/rpc/host-ensure.js +245 -0
- package/dist/modes/rpc/host-ensure.js.map +1 -0
- package/dist/modes/rpc/host-lifecycle.d.ts +57 -0
- package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -0
- package/dist/modes/rpc/host-lifecycle.js +491 -0
- package/dist/modes/rpc/host-lifecycle.js.map +1 -0
- package/dist/modes/rpc/host-watchdog.d.ts +33 -0
- package/dist/modes/rpc/host-watchdog.d.ts.map +1 -0
- package/dist/modes/rpc/host-watchdog.js +142 -0
- package/dist/modes/rpc/host-watchdog.js.map +1 -0
- package/dist/modes/rpc/multi-session-host.d.ts +7 -1
- package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
- package/dist/modes/rpc/multi-session-host.js +206 -32
- package/dist/modes/rpc/multi-session-host.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +25 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +83 -10
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/modes/rpc/rpc-mode.js +1 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +2 -1
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
- package/dist/modes/rpc/session-command-router.js +12 -2
- package/dist/modes/rpc/session-command-router.js.map +1 -1
- package/dist/modes/rpc/session-event-writer.d.ts +13 -2
- package/dist/modes/rpc/session-event-writer.d.ts.map +1 -1
- package/dist/modes/rpc/session-event-writer.js +65 -25
- package/dist/modes/rpc/session-event-writer.js.map +1 -1
- package/docs/rpc.md +53 -1
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +30 -0
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-indirect-eval.js +711 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +10 -10
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-eval-result.ts +9 -3
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +6 -1
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-pairs.d.ts +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-pairs.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-pairs.js +46 -14
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-pairs.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts +11 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js +30 -16
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/classify.d.ts +16 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/classify.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/classify.js +75 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/classify.js.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.d.ts +45 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.js +81 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.js.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/select.d.ts +31 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/select.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/select.js +65 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/select.js.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.d.ts +12 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.js +37 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.d.ts +6 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.js +32 -6
- package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.d.ts +6 -0
- package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js +6 -1
- package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts +75 -0
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +75 -0
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/baseten.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/huggingface.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/minimax-cn.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/minimax.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/mistral.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/nvidia.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opengateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai-coding-cn.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +4 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +5 -5
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts +5 -0
- package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/terminal.js +90 -1
- package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui.js +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AssistantMessageEvent, Credential } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type SlotHasher } from "@earendil-works/pi-ai/auth/pool/select";
|
|
3
|
+
import { type RunSlot } from "./failover.ts";
|
|
4
|
+
import type { CredentialSlotRepository } from "./state-store.ts";
|
|
5
|
+
/** The exact hash the claude-sdk-oauth affinity oracle uses, so pools never remap. */
|
|
6
|
+
export declare const sha256SlotHasher: SlotHasher;
|
|
7
|
+
export type RotationLane = "stored" | "env";
|
|
8
|
+
export type RotationSlot = RunSlot & {
|
|
9
|
+
lane: RotationLane;
|
|
10
|
+
/** Env-lane key material for the attempt; never serialized or persisted. */
|
|
11
|
+
envKey?: string;
|
|
12
|
+
envVarName?: string;
|
|
13
|
+
};
|
|
14
|
+
export type RotationSources = {
|
|
15
|
+
providerId: string;
|
|
16
|
+
credential: Credential | undefined;
|
|
17
|
+
env: (name: string) => string | undefined;
|
|
18
|
+
repository: CredentialSlotRepository;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Lists the provider's rotation slots with sidecar health overlaid. Stored
|
|
23
|
+
* credentials own the lane when present; env slots participate only when
|
|
24
|
+
* nothing is stored, preserving today's resolution precedence. An env slot's
|
|
25
|
+
* persisted health applies only while its HMAC revision still matches the
|
|
26
|
+
* current env value, so rotating a key in place clears its own stale block.
|
|
27
|
+
*/
|
|
28
|
+
export declare function listRotationSlots(sources: RotationSources): Promise<RotationSlot[]>;
|
|
29
|
+
export type CredentialRotationOptions = {
|
|
30
|
+
sources: RotationSources;
|
|
31
|
+
/** Stable session key keeps a session on its slot; absent, each request distributes. */
|
|
32
|
+
affinityKey?: string;
|
|
33
|
+
hasher?: SlotHasher;
|
|
34
|
+
runAttempt: (slot: RotationSlot) => AsyncIterable<AssistantMessageEvent> | Promise<AsyncIterable<AssistantMessageEvent>>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* In-lane credential rotation for one provider request. Selection follows the
|
|
38
|
+
* HRW order for the affinity key; only the `start` bookkeeping event counts as
|
|
39
|
+
* pre-commit, so any delta bars silent rotation (default-DENY) and failures
|
|
40
|
+
* after output carry the turn-retry suppression marker.
|
|
41
|
+
*/
|
|
42
|
+
export declare function streamWithCredentialRotation(options: CredentialRotationOptions): AsyncGenerator<AssistantMessageEvent>;
|
|
43
|
+
//# sourceMappingURL=rotation-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotation-stream.d.ts","sourceRoot":"","sources":["../../../src/core/credential-pool/rotation-stream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EAAmB,KAAK,UAAU,EAAE,MAAM,wCAAwC,CAAC;AAI1F,OAAO,EAAE,KAAK,OAAO,EAAyB,MAAM,eAAe,CAAC;AACpE,OAAO,KAAK,EAAE,wBAAwB,EAAuB,MAAM,kBAAkB,CAAC;AAEtF,sFAAsF;AACtF,eAAO,MAAM,gBAAgB,EAAE,UAAsF,CAAC;AAEtH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE5C,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG;IACpC,IAAI,EAAE,YAAY,CAAC;IACnB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC;IACnC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC1C,UAAU,EAAE,wBAAwB,CAAC;IACrC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB,CAAC;AAYF;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAqBzF;AAoBD,MAAM,MAAM,yBAAyB,GAAG;IACvC,OAAO,EAAE,eAAe,CAAC;IACzB,wFAAwF;IACxF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,UAAU,EAAE,CACX,IAAI,EAAE,YAAY,KACd,aAAa,CAAC,qBAAqB,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC,CAAC;CAC1F,CAAC;AAQF;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,yBAAyB,GAChC,cAAc,CAAC,qBAAqB,CAAC,CA6BvC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { rendezvousOrder } from "@earendil-works/pi-ai/auth/pool/select";
|
|
3
|
+
import { listSlots as listCredentialSlots } from "@earendil-works/pi-ai/auth/pool/slots";
|
|
4
|
+
import { classifyCredentialFailure } from "./classify.js";
|
|
5
|
+
import { discoverEnvSlots } from "./env-slots.js";
|
|
6
|
+
import { runCredentialFailover } from "./failover.js";
|
|
7
|
+
/** The exact hash the claude-sdk-oauth affinity oracle uses, so pools never remap. */
|
|
8
|
+
export const sha256SlotHasher = (input) => createHash("sha256").update(input).digest().readBigUInt64BE(0);
|
|
9
|
+
function overlayState(slot, state) {
|
|
10
|
+
if (!state)
|
|
11
|
+
return slot;
|
|
12
|
+
return {
|
|
13
|
+
...slot,
|
|
14
|
+
...(state.blockedUntil === undefined ? {} : { blockedUntil: state.blockedUntil }),
|
|
15
|
+
...(state.blockReason === undefined ? {} : { blockReason: state.blockReason }),
|
|
16
|
+
...(state.failureCount === undefined ? {} : { failureCount: state.failureCount }),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Lists the provider's rotation slots with sidecar health overlaid. Stored
|
|
21
|
+
* credentials own the lane when present; env slots participate only when
|
|
22
|
+
* nothing is stored, preserving today's resolution precedence. An env slot's
|
|
23
|
+
* persisted health applies only while its HMAC revision still matches the
|
|
24
|
+
* current env value, so rotating a key in place clears its own stale block.
|
|
25
|
+
*/
|
|
26
|
+
export async function listRotationSlots(sources) {
|
|
27
|
+
const { providerId, credential, env, repository } = sources;
|
|
28
|
+
if (credential) {
|
|
29
|
+
const state = await repository.listSlots(providerId, "stored");
|
|
30
|
+
return listCredentialSlots(credential).map((slot) => overlayState({ name: slot.name, lane: "stored" }, state[slot.name]));
|
|
31
|
+
}
|
|
32
|
+
const envSlots = discoverEnvSlots(providerId, env);
|
|
33
|
+
if (envSlots.length === 0)
|
|
34
|
+
return [];
|
|
35
|
+
const state = await repository.listSlots(providerId, "env");
|
|
36
|
+
const slots = [];
|
|
37
|
+
for (const slot of envSlots) {
|
|
38
|
+
const persisted = state[slot.name];
|
|
39
|
+
const revision = await repository.envCredentialRevision(slot.envVarName, slot.key);
|
|
40
|
+
const applicable = persisted?.credentialRevision === revision ? persisted : undefined;
|
|
41
|
+
slots.push(overlayState({ name: slot.name, lane: "env", envKey: slot.key, envVarName: slot.envVarName }, applicable));
|
|
42
|
+
}
|
|
43
|
+
return slots;
|
|
44
|
+
}
|
|
45
|
+
function blockPatch(block, current, now, credentialRevision) {
|
|
46
|
+
const failureCount = (current?.failureCount ?? 0) + 1;
|
|
47
|
+
const base = {
|
|
48
|
+
failureCount,
|
|
49
|
+
...(credentialRevision === undefined ? {} : { credentialRevision }),
|
|
50
|
+
...(current?.lastSuccessAt === undefined ? {} : { lastSuccessAt: current.lastSuccessAt }),
|
|
51
|
+
};
|
|
52
|
+
if (block.reason === "rate_limit") {
|
|
53
|
+
return { ...base, blockedUntil: now + block.cooldownMs, blockReason: "rate_limit" };
|
|
54
|
+
}
|
|
55
|
+
return { ...base, blockReason: block.reason };
|
|
56
|
+
}
|
|
57
|
+
function errorFromEvent(event) {
|
|
58
|
+
if (event.type !== "error")
|
|
59
|
+
return undefined;
|
|
60
|
+
const message = event.error.errorMessage ?? "provider stream error";
|
|
61
|
+
return new Error(message);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* In-lane credential rotation for one provider request. Selection follows the
|
|
65
|
+
* HRW order for the affinity key; only the `start` bookkeeping event counts as
|
|
66
|
+
* pre-commit, so any delta bars silent rotation (default-DENY) and failures
|
|
67
|
+
* after output carry the turn-retry suppression marker.
|
|
68
|
+
*/
|
|
69
|
+
export function streamWithCredentialRotation(options) {
|
|
70
|
+
const { sources, runAttempt } = options;
|
|
71
|
+
const hasher = options.hasher ?? sha256SlotHasher;
|
|
72
|
+
const affinityKey = options.affinityKey ?? randomUUID();
|
|
73
|
+
const now = sources.now ?? Date.now;
|
|
74
|
+
return runCredentialFailover({
|
|
75
|
+
listSlots: () => listRotationSlots(sources),
|
|
76
|
+
select: (candidates) => {
|
|
77
|
+
const ordered = rendezvousOrder(affinityKey, candidates, hasher);
|
|
78
|
+
const winner = ordered[0];
|
|
79
|
+
if (!winner)
|
|
80
|
+
throw new Error("credential rotation selected from an empty candidate set");
|
|
81
|
+
return winner;
|
|
82
|
+
},
|
|
83
|
+
runAttempt,
|
|
84
|
+
isCommittedOutput: (event) => event.type !== "start",
|
|
85
|
+
errorFromEvent,
|
|
86
|
+
classify: classifyCredentialFailure,
|
|
87
|
+
persistBlock: async (slot, block) => {
|
|
88
|
+
const revision = slot.lane === "env" && slot.envVarName !== undefined && slot.envKey !== undefined
|
|
89
|
+
? await sources.repository.envCredentialRevision(slot.envVarName, slot.envKey)
|
|
90
|
+
: undefined;
|
|
91
|
+
await sources.repository.mutateSlotState(sources.providerId, slot.lane, slot.name, (current) => blockPatch(block, current, now(), revision));
|
|
92
|
+
},
|
|
93
|
+
now,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=rotation-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotation-stream.js","sourceRoot":"","sources":["../../../src/core/credential-pool/rotation-stream.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport type { AssistantMessageEvent, Credential } from \"@earendil-works/pi-ai\";\nimport { rendezvousOrder, type SlotHasher } from \"@earendil-works/pi-ai/auth/pool/select\";\nimport { listSlots as listCredentialSlots } from \"@earendil-works/pi-ai/auth/pool/slots\";\nimport { type CredentialBlock, classifyCredentialFailure } from \"./classify.ts\";\nimport { discoverEnvSlots } from \"./env-slots.ts\";\nimport { type RunSlot, runCredentialFailover } from \"./failover.ts\";\nimport type { CredentialSlotRepository, CredentialSlotState } from \"./state-store.ts\";\n\n/** The exact hash the claude-sdk-oauth affinity oracle uses, so pools never remap. */\nexport const sha256SlotHasher: SlotHasher = (input) => createHash(\"sha256\").update(input).digest().readBigUInt64BE(0);\n\nexport type RotationLane = \"stored\" | \"env\";\n\nexport type RotationSlot = RunSlot & {\n\tlane: RotationLane;\n\t/** Env-lane key material for the attempt; never serialized or persisted. */\n\tenvKey?: string;\n\tenvVarName?: string;\n};\n\nexport type RotationSources = {\n\tproviderId: string;\n\tcredential: Credential | undefined;\n\tenv: (name: string) => string | undefined;\n\trepository: CredentialSlotRepository;\n\tnow?: () => number;\n};\n\nfunction overlayState(slot: RotationSlot, state: CredentialSlotState | undefined): RotationSlot {\n\tif (!state) return slot;\n\treturn {\n\t\t...slot,\n\t\t...(state.blockedUntil === undefined ? {} : { blockedUntil: state.blockedUntil }),\n\t\t...(state.blockReason === undefined ? {} : { blockReason: state.blockReason }),\n\t\t...(state.failureCount === undefined ? {} : { failureCount: state.failureCount }),\n\t};\n}\n\n/**\n * Lists the provider's rotation slots with sidecar health overlaid. Stored\n * credentials own the lane when present; env slots participate only when\n * nothing is stored, preserving today's resolution precedence. An env slot's\n * persisted health applies only while its HMAC revision still matches the\n * current env value, so rotating a key in place clears its own stale block.\n */\nexport async function listRotationSlots(sources: RotationSources): Promise<RotationSlot[]> {\n\tconst { providerId, credential, env, repository } = sources;\n\tif (credential) {\n\t\tconst state = await repository.listSlots(providerId, \"stored\");\n\t\treturn listCredentialSlots(credential).map((slot) =>\n\t\t\toverlayState({ name: slot.name, lane: \"stored\" }, state[slot.name]),\n\t\t);\n\t}\n\tconst envSlots = discoverEnvSlots(providerId, env);\n\tif (envSlots.length === 0) return [];\n\tconst state = await repository.listSlots(providerId, \"env\");\n\tconst slots: RotationSlot[] = [];\n\tfor (const slot of envSlots) {\n\t\tconst persisted = state[slot.name];\n\t\tconst revision = await repository.envCredentialRevision(slot.envVarName, slot.key);\n\t\tconst applicable = persisted?.credentialRevision === revision ? persisted : undefined;\n\t\tslots.push(\n\t\t\toverlayState({ name: slot.name, lane: \"env\", envKey: slot.key, envVarName: slot.envVarName }, applicable),\n\t\t);\n\t}\n\treturn slots;\n}\n\nfunction blockPatch(\n\tblock: CredentialBlock,\n\tcurrent: CredentialSlotState | undefined,\n\tnow: number,\n\tcredentialRevision?: string,\n): Omit<CredentialSlotState, \"stateVersion\"> {\n\tconst failureCount = (current?.failureCount ?? 0) + 1;\n\tconst base = {\n\t\tfailureCount,\n\t\t...(credentialRevision === undefined ? {} : { credentialRevision }),\n\t\t...(current?.lastSuccessAt === undefined ? {} : { lastSuccessAt: current.lastSuccessAt }),\n\t};\n\tif (block.reason === \"rate_limit\") {\n\t\treturn { ...base, blockedUntil: now + block.cooldownMs, blockReason: \"rate_limit\" };\n\t}\n\treturn { ...base, blockReason: block.reason };\n}\n\nexport type CredentialRotationOptions = {\n\tsources: RotationSources;\n\t/** Stable session key keeps a session on its slot; absent, each request distributes. */\n\taffinityKey?: string;\n\thasher?: SlotHasher;\n\trunAttempt: (\n\t\tslot: RotationSlot,\n\t) => AsyncIterable<AssistantMessageEvent> | Promise<AsyncIterable<AssistantMessageEvent>>;\n};\n\nfunction errorFromEvent(event: AssistantMessageEvent): unknown {\n\tif (event.type !== \"error\") return undefined;\n\tconst message = event.error.errorMessage ?? \"provider stream error\";\n\treturn new Error(message);\n}\n\n/**\n * In-lane credential rotation for one provider request. Selection follows the\n * HRW order for the affinity key; only the `start` bookkeeping event counts as\n * pre-commit, so any delta bars silent rotation (default-DENY) and failures\n * after output carry the turn-retry suppression marker.\n */\nexport function streamWithCredentialRotation(\n\toptions: CredentialRotationOptions,\n): AsyncGenerator<AssistantMessageEvent> {\n\tconst { sources, runAttempt } = options;\n\tconst hasher = options.hasher ?? sha256SlotHasher;\n\tconst affinityKey = options.affinityKey ?? randomUUID();\n\tconst now = sources.now ?? Date.now;\n\n\treturn runCredentialFailover<AssistantMessageEvent, RotationSlot>({\n\t\tlistSlots: () => listRotationSlots(sources),\n\t\tselect: (candidates) => {\n\t\t\tconst ordered = rendezvousOrder(affinityKey, candidates, hasher);\n\t\t\tconst winner = ordered[0];\n\t\t\tif (!winner) throw new Error(\"credential rotation selected from an empty candidate set\");\n\t\t\treturn winner;\n\t\t},\n\t\trunAttempt,\n\t\tisCommittedOutput: (event) => event.type !== \"start\",\n\t\terrorFromEvent,\n\t\tclassify: classifyCredentialFailure,\n\t\tpersistBlock: async (slot, block) => {\n\t\t\tconst revision =\n\t\t\t\tslot.lane === \"env\" && slot.envVarName !== undefined && slot.envKey !== undefined\n\t\t\t\t\t? await sources.repository.envCredentialRevision(slot.envVarName, slot.envKey)\n\t\t\t\t\t: undefined;\n\t\t\tawait sources.repository.mutateSlotState(sources.providerId, slot.lane, slot.name, (current) =>\n\t\t\t\tblockPatch(block, current, now(), revision),\n\t\t\t);\n\t\t},\n\t\tnow,\n\t});\n}\n"]}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const CREDENTIAL_POOL_STATE_FILENAME = "credential-pool-state.json";
|
|
3
|
+
declare const slotStateSchema: z.ZodObject<{
|
|
4
|
+
stateVersion: z.ZodNumber;
|
|
5
|
+
blockedUntil: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
blockReason: z.ZodOptional<z.ZodEnum<{
|
|
7
|
+
auth_error: "auth_error";
|
|
8
|
+
account_disabled: "account_disabled";
|
|
9
|
+
rate_limit: "rate_limit";
|
|
10
|
+
}>>;
|
|
11
|
+
failureCount: z.ZodOptional<z.ZodNumber>;
|
|
12
|
+
lastSuccessAt: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
credentialRevision: z.ZodOptional<z.ZodString>;
|
|
14
|
+
lease: z.ZodOptional<z.ZodObject<{
|
|
15
|
+
id: z.ZodString;
|
|
16
|
+
expiresAt: z.ZodNumber;
|
|
17
|
+
}, z.core.$strict>>;
|
|
18
|
+
}, z.core.$strict>;
|
|
19
|
+
export type CredentialSlotState = Readonly<z.infer<typeof slotStateSchema>>;
|
|
20
|
+
declare const documentSchema: z.ZodObject<{
|
|
21
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
22
|
+
installationKey: z.ZodString;
|
|
23
|
+
providers: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
24
|
+
lanes: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
25
|
+
slots: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
26
|
+
stateVersion: z.ZodNumber;
|
|
27
|
+
blockedUntil: z.ZodOptional<z.ZodNumber>;
|
|
28
|
+
blockReason: z.ZodOptional<z.ZodEnum<{
|
|
29
|
+
auth_error: "auth_error";
|
|
30
|
+
account_disabled: "account_disabled";
|
|
31
|
+
rate_limit: "rate_limit";
|
|
32
|
+
}>>;
|
|
33
|
+
failureCount: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
lastSuccessAt: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
credentialRevision: z.ZodOptional<z.ZodString>;
|
|
36
|
+
lease: z.ZodOptional<z.ZodObject<{
|
|
37
|
+
id: z.ZodString;
|
|
38
|
+
expiresAt: z.ZodNumber;
|
|
39
|
+
}, z.core.$strict>>;
|
|
40
|
+
}, z.core.$strict>>;
|
|
41
|
+
}, z.core.$strict>>;
|
|
42
|
+
}, z.core.$strict>>;
|
|
43
|
+
}, z.core.$strict>;
|
|
44
|
+
export type CredentialPoolStateDocument = z.infer<typeof documentSchema>;
|
|
45
|
+
export declare function credentialPoolStatePath(agentDir?: string): string;
|
|
46
|
+
export type SlotHealth = "ready" | "blocked" | "half_open";
|
|
47
|
+
/** Deadlines are absolute so a restart never re-enables a cooling credential early. */
|
|
48
|
+
export declare function slotHealth(state: CredentialSlotState | undefined, now: number): SlotHealth;
|
|
49
|
+
/**
|
|
50
|
+
* File-locked health sidecar for credential pools. Never stores credential
|
|
51
|
+
* material: env slots are keyed by an installation-local HMAC revision so a
|
|
52
|
+
* rotated env value clears its own stale block without persisting a raw key
|
|
53
|
+
* hash anywhere.
|
|
54
|
+
*/
|
|
55
|
+
export declare class CredentialSlotRepository {
|
|
56
|
+
private readonly path;
|
|
57
|
+
constructor(path?: string);
|
|
58
|
+
private ensureFile;
|
|
59
|
+
private withDocument;
|
|
60
|
+
installationKey(): Promise<string>;
|
|
61
|
+
/**
|
|
62
|
+
* Derives the persistence key for an env slot's health. HMAC over the
|
|
63
|
+
* installation key means the revision is useless outside this installation
|
|
64
|
+
* and never equals a raw hash of the key material.
|
|
65
|
+
*/
|
|
66
|
+
envCredentialRevision(envVarName: string, envValue: string): Promise<string>;
|
|
67
|
+
listSlots(providerId: string, laneId: string): Promise<Record<string, CredentialSlotState>>;
|
|
68
|
+
/**
|
|
69
|
+
* Atomic read-modify-write for one slot's health. `stateVersion` increments
|
|
70
|
+
* on every write so concurrent mutators can detect lost updates; returning
|
|
71
|
+
* undefined removes the slot state entirely.
|
|
72
|
+
*/
|
|
73
|
+
mutateSlotState(providerId: string, laneId: string, slotId: string, fn: (current: CredentialSlotState | undefined) => Omit<CredentialSlotState, "stateVersion"> | undefined): Promise<CredentialSlotState | undefined>;
|
|
74
|
+
}
|
|
75
|
+
export type HalfOpenLease = {
|
|
76
|
+
leaseId: string;
|
|
77
|
+
expiresAt: number;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Transitions an expired cooldown atomically to half_open: exactly one caller
|
|
81
|
+
* wins the probe lease; everyone else keeps treating the slot as unavailable
|
|
82
|
+
* until the lease expires or the probe settles the block.
|
|
83
|
+
*/
|
|
84
|
+
export declare function acquireHalfOpenLease(repository: CredentialSlotRepository, providerId: string, laneId: string, slotId: string, options?: {
|
|
85
|
+
now?: number;
|
|
86
|
+
leaseTtlMs?: number;
|
|
87
|
+
}): Promise<HalfOpenLease | undefined>;
|
|
88
|
+
export {};
|
|
89
|
+
//# sourceMappingURL=state-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-store.d.ts","sourceRoot":"","sources":["../../../src/core/credential-pool/state-store.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,8BAA8B,+BAA+B,CAAC;AAS3E,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;kBAQnB,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC;AAE5E,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;kBAclB,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEzE,wBAAgB,uBAAuB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAEhF;AAsBD,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;AAE3D,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,KAAK,EAAE,mBAAmB,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,CAS1F;AAED;;;;;GAKG;AACH,qBAAa,wBAAwB;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;gBAElB,IAAI,GAAE,MAAkC;IAIpD,OAAO,CAAC,UAAU;YAMJ,YAAY;IAepB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;IAIxC;;;;OAIG;IACG,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK5E,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAMjG;;;;OAIG;IACG,eAAe,CACpB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,KAAK,IAAI,CAAC,mBAAmB,EAAE,cAAc,CAAC,GAAG,SAAS,GACrG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;CAwB3C;AAED,MAAM,MAAM,aAAa,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE;;;;GAIG;AACH,wBAAsB,oBAAoB,CACzC,UAAU,EAAE,wBAAwB,EACpC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GACjD,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAapC"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import lockfile from "proper-lockfile";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { getAgentDir } from "../../config.js";
|
|
7
|
+
import { FILE_STORAGE_LOCK_OPTIONS } from "../lockfile-policy.js";
|
|
8
|
+
export const CREDENTIAL_POOL_STATE_FILENAME = "credential-pool-state.json";
|
|
9
|
+
const HEX_256_BIT = /^[0-9a-f]{64}$/;
|
|
10
|
+
const leaseSchema = z.strictObject({
|
|
11
|
+
id: z.string().min(1).max(128),
|
|
12
|
+
expiresAt: z.number().int().positive(),
|
|
13
|
+
});
|
|
14
|
+
const slotStateSchema = z.strictObject({
|
|
15
|
+
stateVersion: z.number().int().nonnegative(),
|
|
16
|
+
blockedUntil: z.number().int().positive().optional(),
|
|
17
|
+
blockReason: z.enum(["auth_error", "rate_limit", "account_disabled"]).optional(),
|
|
18
|
+
failureCount: z.number().int().nonnegative().optional(),
|
|
19
|
+
lastSuccessAt: z.number().int().positive().optional(),
|
|
20
|
+
credentialRevision: z.string().regex(HEX_256_BIT).optional(),
|
|
21
|
+
lease: leaseSchema.optional(),
|
|
22
|
+
});
|
|
23
|
+
const documentSchema = z.strictObject({
|
|
24
|
+
schemaVersion: z.literal(1),
|
|
25
|
+
installationKey: z.string().regex(HEX_256_BIT),
|
|
26
|
+
providers: z.record(z.string(), z.strictObject({
|
|
27
|
+
lanes: z.record(z.string(), z.strictObject({
|
|
28
|
+
slots: z.record(z.string(), slotStateSchema),
|
|
29
|
+
})),
|
|
30
|
+
})),
|
|
31
|
+
});
|
|
32
|
+
export function credentialPoolStatePath(agentDir = getAgentDir()) {
|
|
33
|
+
return join(agentDir, CREDENTIAL_POOL_STATE_FILENAME);
|
|
34
|
+
}
|
|
35
|
+
const FILE_WRITE_OPTIONS = { encoding: "utf-8", mode: 0o600 };
|
|
36
|
+
function freshDocument() {
|
|
37
|
+
return { schemaVersion: 1, installationKey: randomBytes(32).toString("hex"), providers: {} };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The sidecar holds only health, so an unreadable or invalid document resets to
|
|
41
|
+
* a fresh one instead of failing auth resolution; the worst outcome of a reset
|
|
42
|
+
* is a cleared cooldown, never lost credential material.
|
|
43
|
+
*/
|
|
44
|
+
function parseDocument(content) {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = documentSchema.safeParse(JSON.parse(content));
|
|
47
|
+
return parsed.success ? parsed.data : freshDocument();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return freshDocument();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Deadlines are absolute so a restart never re-enables a cooling credential early. */
|
|
54
|
+
export function slotHealth(state, now) {
|
|
55
|
+
if (!state)
|
|
56
|
+
return "ready";
|
|
57
|
+
// Auth and billing blocks have no expiry; only credential replacement clears them.
|
|
58
|
+
if (state.blockReason === "auth_error" || state.blockReason === "account_disabled")
|
|
59
|
+
return "blocked";
|
|
60
|
+
if (state.blockedUntil !== undefined && state.blockedUntil > now)
|
|
61
|
+
return "blocked";
|
|
62
|
+
if (state.blockedUntil !== undefined && state.lease !== undefined && state.lease.expiresAt > now) {
|
|
63
|
+
return "half_open";
|
|
64
|
+
}
|
|
65
|
+
return "ready";
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* File-locked health sidecar for credential pools. Never stores credential
|
|
69
|
+
* material: env slots are keyed by an installation-local HMAC revision so a
|
|
70
|
+
* rotated env value clears its own stale block without persisting a raw key
|
|
71
|
+
* hash anywhere.
|
|
72
|
+
*/
|
|
73
|
+
export class CredentialSlotRepository {
|
|
74
|
+
constructor(path = credentialPoolStatePath()) {
|
|
75
|
+
this.path = path;
|
|
76
|
+
}
|
|
77
|
+
ensureFile() {
|
|
78
|
+
if (existsSync(this.path))
|
|
79
|
+
return;
|
|
80
|
+
mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
81
|
+
writeFileSync(this.path, JSON.stringify(freshDocument(), null, 2), FILE_WRITE_OPTIONS);
|
|
82
|
+
}
|
|
83
|
+
async withDocument(fn) {
|
|
84
|
+
this.ensureFile();
|
|
85
|
+
const release = await lockfile.lock(this.path, FILE_STORAGE_LOCK_OPTIONS);
|
|
86
|
+
try {
|
|
87
|
+
const document = parseDocument(readFileSync(this.path, "utf-8"));
|
|
88
|
+
const { result, next } = fn(document);
|
|
89
|
+
if (next)
|
|
90
|
+
writeFileSync(this.path, JSON.stringify(next, null, 2), FILE_WRITE_OPTIONS);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await release();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async installationKey() {
|
|
98
|
+
return this.withDocument((document) => ({ result: document.installationKey, next: document }));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Derives the persistence key for an env slot's health. HMAC over the
|
|
102
|
+
* installation key means the revision is useless outside this installation
|
|
103
|
+
* and never equals a raw hash of the key material.
|
|
104
|
+
*/
|
|
105
|
+
async envCredentialRevision(envVarName, envValue) {
|
|
106
|
+
const key = await this.installationKey();
|
|
107
|
+
return createHmac("sha256", key).update(`${envVarName}\0${envValue}`).digest("hex");
|
|
108
|
+
}
|
|
109
|
+
async listSlots(providerId, laneId) {
|
|
110
|
+
return this.withDocument((document) => ({
|
|
111
|
+
result: { ...document.providers[providerId]?.lanes[laneId]?.slots },
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Atomic read-modify-write for one slot's health. `stateVersion` increments
|
|
116
|
+
* on every write so concurrent mutators can detect lost updates; returning
|
|
117
|
+
* undefined removes the slot state entirely.
|
|
118
|
+
*/
|
|
119
|
+
async mutateSlotState(providerId, laneId, slotId, fn) {
|
|
120
|
+
return this.withDocument((document) => {
|
|
121
|
+
const provider = document.providers[providerId] ?? { lanes: {} };
|
|
122
|
+
const lane = provider.lanes[laneId] ?? { slots: {} };
|
|
123
|
+
const current = lane.slots[slotId];
|
|
124
|
+
const mutated = fn(current);
|
|
125
|
+
const slots = { ...lane.slots };
|
|
126
|
+
let result;
|
|
127
|
+
if (mutated === undefined) {
|
|
128
|
+
delete slots[slotId];
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
result = { ...mutated, stateVersion: (current?.stateVersion ?? 0) + 1 };
|
|
132
|
+
slots[slotId] = result;
|
|
133
|
+
}
|
|
134
|
+
const next = {
|
|
135
|
+
...document,
|
|
136
|
+
providers: {
|
|
137
|
+
...document.providers,
|
|
138
|
+
[providerId]: { lanes: { ...provider.lanes, [laneId]: { slots } } },
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
return { result, next };
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Transitions an expired cooldown atomically to half_open: exactly one caller
|
|
147
|
+
* wins the probe lease; everyone else keeps treating the slot as unavailable
|
|
148
|
+
* until the lease expires or the probe settles the block.
|
|
149
|
+
*/
|
|
150
|
+
export async function acquireHalfOpenLease(repository, providerId, laneId, slotId, options = {}) {
|
|
151
|
+
const now = options.now ?? Date.now();
|
|
152
|
+
const leaseTtlMs = options.leaseTtlMs ?? 30_000;
|
|
153
|
+
let lease;
|
|
154
|
+
await repository.mutateSlotState(providerId, laneId, slotId, (current) => {
|
|
155
|
+
if (!current)
|
|
156
|
+
return current;
|
|
157
|
+
const expired = current.blockedUntil !== undefined && current.blockedUntil <= now;
|
|
158
|
+
const leaseLive = current.lease !== undefined && current.lease.expiresAt > now;
|
|
159
|
+
if (!expired || leaseLive || current.blockReason === "auth_error")
|
|
160
|
+
return current;
|
|
161
|
+
lease = { leaseId: randomUUID(), expiresAt: now + leaseTtlMs };
|
|
162
|
+
return { ...current, lease: { id: lease.leaseId, expiresAt: lease.expiresAt } };
|
|
163
|
+
});
|
|
164
|
+
return lease;
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=state-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-store.js","sourceRoot":"","sources":["../../../src/core/credential-pool/state-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHmac, randomBytes, randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport lockfile from \"proper-lockfile\";\nimport { z } from \"zod\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { FILE_STORAGE_LOCK_OPTIONS } from \"../lockfile-policy.ts\";\n\nexport const CREDENTIAL_POOL_STATE_FILENAME = \"credential-pool-state.json\";\n\nconst HEX_256_BIT = /^[0-9a-f]{64}$/;\n\nconst leaseSchema = z.strictObject({\n\tid: z.string().min(1).max(128),\n\texpiresAt: z.number().int().positive(),\n});\n\nconst slotStateSchema = z.strictObject({\n\tstateVersion: z.number().int().nonnegative(),\n\tblockedUntil: z.number().int().positive().optional(),\n\tblockReason: z.enum([\"auth_error\", \"rate_limit\", \"account_disabled\"]).optional(),\n\tfailureCount: z.number().int().nonnegative().optional(),\n\tlastSuccessAt: z.number().int().positive().optional(),\n\tcredentialRevision: z.string().regex(HEX_256_BIT).optional(),\n\tlease: leaseSchema.optional(),\n});\n\nexport type CredentialSlotState = Readonly<z.infer<typeof slotStateSchema>>;\n\nconst documentSchema = z.strictObject({\n\tschemaVersion: z.literal(1),\n\tinstallationKey: z.string().regex(HEX_256_BIT),\n\tproviders: z.record(\n\t\tz.string(),\n\t\tz.strictObject({\n\t\t\tlanes: z.record(\n\t\t\t\tz.string(),\n\t\t\t\tz.strictObject({\n\t\t\t\t\tslots: z.record(z.string(), slotStateSchema),\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t),\n});\n\nexport type CredentialPoolStateDocument = z.infer<typeof documentSchema>;\n\nexport function credentialPoolStatePath(agentDir: string = getAgentDir()): string {\n\treturn join(agentDir, CREDENTIAL_POOL_STATE_FILENAME);\n}\n\nconst FILE_WRITE_OPTIONS = { encoding: \"utf-8\", mode: 0o600 } as const;\n\nfunction freshDocument(): CredentialPoolStateDocument {\n\treturn { schemaVersion: 1, installationKey: randomBytes(32).toString(\"hex\"), providers: {} };\n}\n\n/**\n * The sidecar holds only health, so an unreadable or invalid document resets to\n * a fresh one instead of failing auth resolution; the worst outcome of a reset\n * is a cleared cooldown, never lost credential material.\n */\nfunction parseDocument(content: string): CredentialPoolStateDocument {\n\ttry {\n\t\tconst parsed = documentSchema.safeParse(JSON.parse(content));\n\t\treturn parsed.success ? parsed.data : freshDocument();\n\t} catch {\n\t\treturn freshDocument();\n\t}\n}\n\nexport type SlotHealth = \"ready\" | \"blocked\" | \"half_open\";\n\n/** Deadlines are absolute so a restart never re-enables a cooling credential early. */\nexport function slotHealth(state: CredentialSlotState | undefined, now: number): SlotHealth {\n\tif (!state) return \"ready\";\n\t// Auth and billing blocks have no expiry; only credential replacement clears them.\n\tif (state.blockReason === \"auth_error\" || state.blockReason === \"account_disabled\") return \"blocked\";\n\tif (state.blockedUntil !== undefined && state.blockedUntil > now) return \"blocked\";\n\tif (state.blockedUntil !== undefined && state.lease !== undefined && state.lease.expiresAt > now) {\n\t\treturn \"half_open\";\n\t}\n\treturn \"ready\";\n}\n\n/**\n * File-locked health sidecar for credential pools. Never stores credential\n * material: env slots are keyed by an installation-local HMAC revision so a\n * rotated env value clears its own stale block without persisting a raw key\n * hash anywhere.\n */\nexport class CredentialSlotRepository {\n\tprivate readonly path: string;\n\n\tconstructor(path: string = credentialPoolStatePath()) {\n\t\tthis.path = path;\n\t}\n\n\tprivate ensureFile(): void {\n\t\tif (existsSync(this.path)) return;\n\t\tmkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });\n\t\twriteFileSync(this.path, JSON.stringify(freshDocument(), null, 2), FILE_WRITE_OPTIONS);\n\t}\n\n\tprivate async withDocument<T>(\n\t\tfn: (document: CredentialPoolStateDocument) => { result: T; next?: CredentialPoolStateDocument },\n\t): Promise<T> {\n\t\tthis.ensureFile();\n\t\tconst release = await lockfile.lock(this.path, FILE_STORAGE_LOCK_OPTIONS);\n\t\ttry {\n\t\t\tconst document = parseDocument(readFileSync(this.path, \"utf-8\"));\n\t\t\tconst { result, next } = fn(document);\n\t\t\tif (next) writeFileSync(this.path, JSON.stringify(next, null, 2), FILE_WRITE_OPTIONS);\n\t\t\treturn result;\n\t\t} finally {\n\t\t\tawait release();\n\t\t}\n\t}\n\n\tasync installationKey(): Promise<string> {\n\t\treturn this.withDocument((document) => ({ result: document.installationKey, next: document }));\n\t}\n\n\t/**\n\t * Derives the persistence key for an env slot's health. HMAC over the\n\t * installation key means the revision is useless outside this installation\n\t * and never equals a raw hash of the key material.\n\t */\n\tasync envCredentialRevision(envVarName: string, envValue: string): Promise<string> {\n\t\tconst key = await this.installationKey();\n\t\treturn createHmac(\"sha256\", key).update(`${envVarName}\\0${envValue}`).digest(\"hex\");\n\t}\n\n\tasync listSlots(providerId: string, laneId: string): Promise<Record<string, CredentialSlotState>> {\n\t\treturn this.withDocument((document) => ({\n\t\t\tresult: { ...document.providers[providerId]?.lanes[laneId]?.slots },\n\t\t}));\n\t}\n\n\t/**\n\t * Atomic read-modify-write for one slot's health. `stateVersion` increments\n\t * on every write so concurrent mutators can detect lost updates; returning\n\t * undefined removes the slot state entirely.\n\t */\n\tasync mutateSlotState(\n\t\tproviderId: string,\n\t\tlaneId: string,\n\t\tslotId: string,\n\t\tfn: (current: CredentialSlotState | undefined) => Omit<CredentialSlotState, \"stateVersion\"> | undefined,\n\t): Promise<CredentialSlotState | undefined> {\n\t\treturn this.withDocument((document) => {\n\t\t\tconst provider = document.providers[providerId] ?? { lanes: {} };\n\t\t\tconst lane = provider.lanes[laneId] ?? { slots: {} };\n\t\t\tconst current = lane.slots[slotId];\n\t\t\tconst mutated = fn(current);\n\t\t\tconst slots = { ...lane.slots };\n\t\t\tlet result: CredentialSlotState | undefined;\n\t\t\tif (mutated === undefined) {\n\t\t\t\tdelete slots[slotId];\n\t\t\t} else {\n\t\t\t\tresult = { ...mutated, stateVersion: (current?.stateVersion ?? 0) + 1 };\n\t\t\t\tslots[slotId] = result;\n\t\t\t}\n\t\t\tconst next: CredentialPoolStateDocument = {\n\t\t\t\t...document,\n\t\t\t\tproviders: {\n\t\t\t\t\t...document.providers,\n\t\t\t\t\t[providerId]: { lanes: { ...provider.lanes, [laneId]: { slots } } },\n\t\t\t\t},\n\t\t\t};\n\t\t\treturn { result, next };\n\t\t});\n\t}\n}\n\nexport type HalfOpenLease = { leaseId: string; expiresAt: number };\n\n/**\n * Transitions an expired cooldown atomically to half_open: exactly one caller\n * wins the probe lease; everyone else keeps treating the slot as unavailable\n * until the lease expires or the probe settles the block.\n */\nexport async function acquireHalfOpenLease(\n\trepository: CredentialSlotRepository,\n\tproviderId: string,\n\tlaneId: string,\n\tslotId: string,\n\toptions: { now?: number; leaseTtlMs?: number } = {},\n): Promise<HalfOpenLease | undefined> {\n\tconst now = options.now ?? Date.now();\n\tconst leaseTtlMs = options.leaseTtlMs ?? 30_000;\n\tlet lease: HalfOpenLease | undefined;\n\tawait repository.mutateSlotState(providerId, laneId, slotId, (current) => {\n\t\tif (!current) return current;\n\t\tconst expired = current.blockedUntil !== undefined && current.blockedUntil <= now;\n\t\tconst leaseLive = current.lease !== undefined && current.lease.expiresAt > now;\n\t\tif (!expired || leaseLive || current.blockReason === \"auth_error\") return current;\n\t\tlease = { leaseId: randomUUID(), expiresAt: now + leaseTtlMs };\n\t\treturn { ...current, lease: { id: lease.leaseId, expiresAt: lease.expiresAt } };\n\t});\n\treturn lease;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/compaction/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAEX,YAAY,EAIZ,MAAM,gBAAgB,CAAC;AA4BxB,OAAO,EAEN,KAAK,kCAAkC,EAIvC,MAAM,oBAAoB,CAAC;AAgD5B,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMnG;AAuFD,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAC1C,EAAE,EAAE,YAAY,EAChB,4BAA4B,GAAE,kCAAuC,GACnE,IAAI,CA+
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/compaction/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAEX,YAAY,EAIZ,MAAM,gBAAgB,CAAC;AA4BxB,OAAO,EAEN,KAAK,kCAAkC,EAIvC,MAAM,oBAAoB,CAAC;AAgD5B,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMnG;AAuFD,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAC1C,EAAE,EAAE,YAAY,EAChB,4BAA4B,GAAE,kCAAuC,GACnE,IAAI,CA+1BN"}
|
|
@@ -136,6 +136,12 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
|
136
136
|
}
|
|
137
137
|
let idleWarmupTimer;
|
|
138
138
|
let idleWarmupAttempt = 0;
|
|
139
|
+
// True from the `agent_end` idle trigger until the next turn starts (or the
|
|
140
|
+
// session shuts down). The idle-apply watcher below is fenced on it so a
|
|
141
|
+
// summary that lands after the user has already prompted is never applied
|
|
142
|
+
// out from under the turn that is starting; the warm-consume path in
|
|
143
|
+
// `before_agent_start` owns the job from that point on.
|
|
144
|
+
let sessionIdleSinceAgentEnd = false;
|
|
139
145
|
function cancelIdleWarmupRetry() {
|
|
140
146
|
if (idleWarmupTimer === undefined)
|
|
141
147
|
return;
|
|
@@ -212,9 +218,66 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
|
212
218
|
invalidateSpeculativeCompaction(ctx);
|
|
213
219
|
startSpeculativeCompaction(ctx, idle.IDLE_COMPACTION_INSTRUCTIONS);
|
|
214
220
|
armIdleWarmupRetry(ctx);
|
|
221
|
+
armIdleApply(ctx);
|
|
215
222
|
}, idleRetry.IDLE_WARMUP_RETRY_DELAY_MS);
|
|
216
223
|
});
|
|
217
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Apply the idle warm summary as soon as it finishes generating, while the
|
|
227
|
+
* session is still idle. Holding it warm until the next `before_agent_start`
|
|
228
|
+
* makes the user watch their own prompt wait behind a compaction they could
|
|
229
|
+
* not see coming; applying during the idle gap renders the [compaction] block
|
|
230
|
+
* first and lets the next message stack below it.
|
|
231
|
+
*
|
|
232
|
+
* Every guard is re-read at continuation time, because generation takes long
|
|
233
|
+
* enough for all of them to change: the session may no longer be idle, the
|
|
234
|
+
* job may have been invalidated or claimed, the context may have dropped
|
|
235
|
+
* below the threshold, the lane may have been handed to the SDK, or the
|
|
236
|
+
* breaker may have tripped. A refused apply (stale anchor/revision) silently
|
|
237
|
+
* keeps the warm hold, so the next prompt consumes it exactly as before.
|
|
238
|
+
*/
|
|
239
|
+
function armIdleApply(ctx) {
|
|
240
|
+
const job = speculativeJob;
|
|
241
|
+
if (!job)
|
|
242
|
+
return;
|
|
243
|
+
// Never throws out of the continuation: a retired context, a refused apply,
|
|
244
|
+
// or a provider failure all resolve into a silent stand-down.
|
|
245
|
+
void job.promise
|
|
246
|
+
.then(async (compaction) => {
|
|
247
|
+
if (!compaction)
|
|
248
|
+
return;
|
|
249
|
+
if (speculativeJob !== job)
|
|
250
|
+
return;
|
|
251
|
+
if (!sessionIdleSinceAgentEnd)
|
|
252
|
+
return;
|
|
253
|
+
if (isContextRetired(ctx))
|
|
254
|
+
return;
|
|
255
|
+
if (!ctx.isIdle())
|
|
256
|
+
return;
|
|
257
|
+
if (lanePolicy.disablesSenpiCompaction(ctx))
|
|
258
|
+
return;
|
|
259
|
+
if (breaker.isTripped(state, Date.now()))
|
|
260
|
+
return;
|
|
261
|
+
if (cap.shouldRejectByCap(state).cancel)
|
|
262
|
+
return;
|
|
263
|
+
const usage = ctx.getContextUsage();
|
|
264
|
+
const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
265
|
+
if (!usage ||
|
|
266
|
+
!policy.shouldTriggerCompaction(usage, contextWindow, ctx.getCompactionSettings(), state.lastYield ?? undefined)) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const result = await applyGeneratedCompaction(ctx, job.snapshot, () => speculativeGeneration, compaction);
|
|
270
|
+
if (!result.applied)
|
|
271
|
+
return;
|
|
272
|
+
// The job is consumed: clearing it here is also what stands the idle
|
|
273
|
+
// retry watcher down for this generation.
|
|
274
|
+
if (speculativeJob === job)
|
|
275
|
+
speculativeJob = undefined;
|
|
276
|
+
cancelIdleWarmupRetry();
|
|
277
|
+
getLogger(ctx).debug("idle_applied", { generation: job.generation, origin: "speculative" });
|
|
278
|
+
})
|
|
279
|
+
.catch(() => { });
|
|
280
|
+
}
|
|
218
281
|
function isSameModelIdentity(left, right) {
|
|
219
282
|
return (right !== undefined &&
|
|
220
283
|
left.api === right.api &&
|
|
@@ -661,6 +724,7 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
|
661
724
|
state = breaker.recordFailure(state, Date.now(), { route: compactEvent.reason });
|
|
662
725
|
});
|
|
663
726
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
727
|
+
sessionIdleSinceAgentEnd = false;
|
|
664
728
|
cancelIdleWarmupRetry();
|
|
665
729
|
const message = checkpointState.attachRestorationDirective(restorationDirectiveState, recentCheckpoint(ctx), restoration.consumePendingPayload(restorationState));
|
|
666
730
|
const usage = ctx.getContextUsage();
|
|
@@ -781,8 +845,10 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
|
781
845
|
})) {
|
|
782
846
|
getLogger(ctx).debug("idle_trigger", { contextWindow, tokens: usage?.tokens ?? 0 });
|
|
783
847
|
idleWarmupAttempt = 0;
|
|
848
|
+
sessionIdleSinceAgentEnd = true;
|
|
784
849
|
startSpeculativeCompaction(ctx, idle.IDLE_COMPACTION_INSTRUCTIONS);
|
|
785
850
|
armIdleWarmupRetry(ctx);
|
|
851
|
+
armIdleApply(ctx);
|
|
786
852
|
}
|
|
787
853
|
});
|
|
788
854
|
pi.on("message_end", async (event, ctx) => {
|
|
@@ -809,6 +875,7 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
|
809
875
|
// warm-up watcher down here rather than leaving a timer armed against a
|
|
810
876
|
// context that is about to start throwing on every read.
|
|
811
877
|
pi.on("session_shutdown", () => {
|
|
878
|
+
sessionIdleSinceAgentEnd = false;
|
|
812
879
|
cancelIdleWarmupRetry();
|
|
813
880
|
idleWarmupAttempt = 0;
|
|
814
881
|
speculativeJob?.controller.abort();
|