@code-yeongyu/senpi 2026.10.18 → 2026.10.19
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 +47 -0
- package/dist/bundle/chunks/{anthropic-messages-DDBWM4JZ.js → anthropic-messages-EMTBHJMM.js} +2 -2
- package/dist/bundle/chunks/{args-2MYPVFPH.js → args-LWR7VFKS.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-3UG5TQU7.js → azure-openai-responses-SNWKFDXK.js} +1 -1
- package/dist/bundle/chunks/chatgpt-subscription.js +1 -1
- package/dist/bundle/chunks/{chunk-SOF44GFZ.js → chunk-35M5MVUZ.js} +149 -145
- package/dist/bundle/chunks/chunk-3DNYAO3P.js +2 -0
- package/dist/bundle/chunks/{chunk-T2MHSEQ2.js → chunk-5AOKDC6G.js} +1 -1
- package/dist/bundle/chunks/{chunk-MYXNQ7YZ.js → chunk-7QPFFAJT.js} +3 -3
- package/dist/bundle/chunks/{chunk-R36U2DJC.js → chunk-AUZXSLZN.js} +1 -1
- package/dist/bundle/chunks/{chunk-PMTG4YNS.js → chunk-BOCN744P.js} +1 -1
- package/dist/bundle/chunks/{chunk-XTAWER7O.js → chunk-CICJOV6I.js} +1 -1
- package/dist/bundle/chunks/{chunk-4UCLDXEF.js → chunk-FM7NDZR5.js} +1 -1
- package/dist/bundle/chunks/{chunk-TBRWPNA4.js → chunk-FSENE7JK.js} +2 -2
- package/dist/bundle/chunks/{chunk-TUD7XRIS.js → chunk-G7LLAQXI.js} +3 -3
- package/dist/bundle/chunks/{chunk-BGD3MZ47.js → chunk-GOKAWZUO.js} +1 -1
- package/dist/bundle/chunks/{chunk-XC6UG5SI.js → chunk-IM6ZWYTW.js} +2 -2
- package/dist/bundle/chunks/{chunk-KHATAB7P.js → chunk-KM4FMQII.js} +1 -1
- package/dist/bundle/chunks/chunk-LAWS3FYQ.js +11 -0
- package/dist/bundle/chunks/{chunk-W6YQYGE2.js → chunk-MVNVDGYF.js} +1 -1
- package/dist/bundle/chunks/{chunk-W4BO6S7F.js → chunk-PMGEQYEP.js} +2 -2
- package/dist/bundle/chunks/chunk-QGJLZ6BI.js +2 -0
- package/dist/bundle/chunks/{chunk-5RQNCWS2.js → chunk-QJ6MUFXY.js} +1 -1
- package/dist/bundle/chunks/{chunk-57Q7Q627.js → chunk-QRGAMVEE.js} +1 -1
- package/dist/bundle/chunks/{chunk-W2257FYP.js → chunk-QXEWAKXA.js} +1 -1
- package/dist/bundle/chunks/{chunk-UUMKAA7S.js → chunk-RL5QMGQD.js} +9 -9
- package/dist/bundle/chunks/{chunk-D2UQYZVK.js → chunk-SHKXJWK3.js} +1 -1
- package/dist/bundle/chunks/chunk-T36LJ6MW.js +3 -0
- package/dist/bundle/chunks/{chunk-S4V72F76.js → chunk-VCD7FD64.js} +1 -1
- package/dist/bundle/chunks/{chunk-MYPTZCPF.js → chunk-VEKNDMUA.js} +1 -1
- package/dist/bundle/chunks/{chunk-IRBAGET4.js → chunk-VPV7ADF3.js} +4 -4
- package/dist/bundle/chunks/{chunk-6DDAEDY7.js → chunk-WEHIEACU.js} +1 -1
- package/dist/bundle/chunks/{chunk-C2HSPFXN.js → chunk-WQLMQGOW.js} +1 -1
- package/dist/bundle/chunks/{chunk-QZIKKUUP.js → chunk-X6TWPBI6.js} +2 -2
- package/dist/bundle/chunks/{chunk-VYWODOAM.js → chunk-YUMRZIKJ.js} +6 -6
- package/dist/bundle/chunks/chunk-ZPSJQHDK.js +395 -0
- package/dist/bundle/chunks/{cli-main-MVVPTGMS.js → cli-main-EABMHWFV.js} +1 -1
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-H4ZPUBX4.js → cloudflare-workers-ai-system-one-PF7FU63D.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-YLAYNQLO.js → google-generative-ai-UFCHYLRE.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-THFXM5R2.js → google-vertex-2FWDY7XX.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-H4ERPCR5.js → help-fast-path-BYWJN6YX.js} +1 -1
- package/dist/bundle/chunks/{host-command-2CMRIKC3.js → host-command-5T34JLBP.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-6Y2EKT4U.js → host-lifecycle-GSRIQP6Z.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +5 -5
- package/dist/bundle/chunks/{host-session-command-MVHY4TKA.js → host-session-command-LF6JODPX.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-2ZQ43N73.js → interactive-mode-GMWHBIUC.js} +1 -1
- package/dist/bundle/chunks/{list-tips-AWDAUHF2.js → list-tips-VSWED3PT.js} +1 -1
- package/dist/bundle/chunks/{mistral-conversations-GUGTVRCL.js → mistral-conversations-QHZLDTSP.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-NO4PIOVG.js → multi-session-host-3J7STKGD.js} +6 -6
- package/dist/bundle/chunks/openai-codex-responses-7YOBPZ2N.js +10 -0
- package/dist/bundle/chunks/{openai-completions-I5N2UIEK.js → openai-completions-HSZPYXEA.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-SGLUJVZW.js → openai-responses-7W2KF3W7.js} +1 -1
- package/dist/bundle/chunks/openrouter.js +1 -1
- package/dist/bundle/chunks/{package-manager-cli-HTZQXYNI.js → package-manager-cli-LWQZSWKL.js} +1 -1
- package/dist/bundle/chunks/radius.js +1 -1
- package/dist/bundle/chunks/{rotation-stream-TOOIK43C.js → rotation-stream-QRRWL73X.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-6ZWGPICX.js → rpc-mode-6LSWRKXR.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-2YW4B655.js → schedule-command-7CXMW4WK.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-XC2LPLCD.js → session-control-endpoint-E265WAIQ.js} +1 -1
- package/dist/bundle/chunks/{session-picker-7W7GIRJT.js → session-picker-XRB3LP5W.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +315 -310
- package/dist/bundle/chunks/{typesafe-system-one-VBWWTJTF.js → typesafe-system-one-YA75435G.js} +1 -1
- 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-run-continuation.d.ts +24 -0
- package/dist/core/agent-run-continuation.js +57 -0
- package/dist/core/agent-session.d.ts +38 -1
- package/dist/core/agent-session.js +122 -13
- package/dist/core/extension-handler-stall-report.d.ts +11 -0
- package/dist/core/extension-handler-stall-report.js +26 -0
- package/dist/core/extensions/builtin/anthropic-subscription/account-command.js +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/affinity.d.ts +13 -1
- package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +15 -4
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +2 -0
- package/dist/core/extensions/builtin/anthropic-subscription/executable.d.ts +5 -0
- package/dist/core/extensions/builtin/anthropic-subscription/executable.js +11 -1
- package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +2 -0
- package/dist/core/extensions/builtin/anthropic-subscription/failover.js +3 -2
- package/dist/core/extensions/builtin/anthropic-subscription/guidance.d.ts +8 -0
- package/dist/core/extensions/builtin/anthropic-subscription/guidance.js +29 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +2 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.js +4 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +4 -2
- package/dist/core/extensions/handler-budget.d.ts +84 -0
- package/dist/core/extensions/handler-budget.js +136 -0
- package/dist/core/extensions/runner.d.ts +23 -0
- package/dist/core/extensions/runner.js +184 -99
- package/dist/core/footer-data-provider.js +2 -1
- package/dist/core/keybindings.d.ts +1 -1
- package/dist/core/keybindings.js +3 -2
- package/dist/core/model-resolver.js +1 -1
- package/dist/core/pending-input-journal.d.ts +6 -1
- package/dist/core/pending-input-journal.js +90 -34
- package/dist/core/pending-input-public.d.ts +11 -0
- package/dist/core/pending-input-public.js +10 -0
- package/dist/core/pending-input-session-port.d.ts +15 -0
- package/dist/core/pending-input-session-port.js +93 -0
- package/dist/core/pending-input-session.d.ts +108 -0
- package/dist/core/pending-input-session.js +220 -0
- package/dist/core/pending-input-store.d.ts +32 -1
- package/dist/core/pending-input-store.js +74 -26
- package/dist/core/pending-input-types.d.ts +25 -4
- package/dist/core/pending-input.js +36 -18
- package/dist/core/session-entry-file.js +1 -0
- package/dist/core/session-log.js +1 -1
- package/dist/core/session-manager.d.ts +1 -0
- package/dist/core/session-manager.js +5 -1
- package/dist/core/session-move/bundle.d.ts +39 -0
- package/dist/core/session-move/bundle.js +108 -0
- package/dist/core/session-move/export.d.ts +18 -0
- package/dist/core/session-move/export.js +83 -0
- package/dist/core/session-move/finish.d.ts +24 -0
- package/dist/core/session-move/finish.js +81 -0
- package/dist/core/session-move/import.d.ts +32 -0
- package/dist/core/session-move/import.js +124 -0
- package/dist/core/session-move/move-ledger.d.ts +47 -0
- package/dist/core/session-move/move-ledger.js +151 -0
- package/dist/core/session-move/ownership.d.ts +61 -0
- package/dist/core/session-move/ownership.js +89 -0
- package/dist/core/session-move/requeue.d.ts +8 -0
- package/dist/core/session-move/requeue.js +18 -0
- package/dist/core/session-move/write-guard.d.ts +22 -0
- package/dist/core/session-move/write-guard.js +44 -0
- package/dist/core/settings-manager.d.ts +13 -0
- package/dist/core/settings-manager.js +17 -0
- package/dist/core/turn-deadline.d.ts +2 -0
- package/dist/core/turn-deadline.js +10 -3
- package/dist/modes/interactive/components/ask-user-async-widget.d.ts +7 -0
- package/dist/modes/interactive/components/ask-user-async-widget.js +20 -6
- package/dist/modes/rpc/connection-handler.js +3 -0
- package/dist/modes/rpc/effect-commands.js +3 -4
- package/dist/modes/rpc/host-decision.d.ts +1 -1
- package/dist/modes/rpc/host-decision.js +2 -0
- package/dist/modes/rpc/host-ensure-liveness.d.ts +8 -0
- package/dist/modes/rpc/host-ensure-liveness.js +18 -1
- package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
- package/dist/modes/rpc/host-ensure.js +8 -2
- package/dist/modes/rpc/host-lifecycle-proxy.d.ts +9 -2
- package/dist/modes/rpc/host-lifecycle-proxy.js +36 -8
- package/dist/modes/rpc/host-observe-request.js +1 -0
- package/dist/modes/rpc/multi-session-host.js +3 -33
- package/dist/modes/rpc/rpc-client.d.ts +10 -0
- package/dist/modes/rpc/rpc-client.js +11 -0
- package/dist/modes/rpc/rpc-input-validation.js +8 -0
- package/dist/modes/rpc/rpc-types.d.ts +25 -1
- package/dist/modes/rpc/session-command-activity.js +1 -0
- package/dist/modes/rpc/session-control-actions-data.js +17 -8
- package/dist/modes/rpc/session-held.js +1 -0
- package/dist/modes/rpc/session-release.js +16 -1
- package/dist/modes/rpc/socket-ownership.d.ts +17 -0
- package/dist/modes/rpc/socket-ownership.js +52 -0
- package/dist/modes/rpc/tool-child/admission.d.ts +42 -0
- package/dist/modes/rpc/tool-child/admission.js +81 -0
- package/dist/modes/rpc/tool-child/admission.test.d.ts +2 -0
- package/dist/modes/rpc/tool-child/admission.test.js +166 -0
- package/dist/modes/rpc/tool-child/codec.d.ts +21 -0
- package/dist/modes/rpc/tool-child/codec.js +137 -0
- package/dist/modes/rpc/tool-child/codec.test.d.ts +2 -0
- package/dist/modes/rpc/tool-child/codec.test.js +234 -0
- package/dist/modes/rpc/tool-child/context.d.ts +42 -0
- package/dist/modes/rpc/tool-child/context.js +23 -0
- package/dist/modes/rpc/tool-child/flow-control.d.ts +24 -0
- package/dist/modes/rpc/tool-child/flow-control.js +80 -0
- package/dist/modes/rpc/tool-child/payload-ref-paths.d.ts +9 -0
- package/dist/modes/rpc/tool-child/payload-ref-paths.js +45 -0
- package/dist/modes/rpc/tool-child/payload-ref.d.ts +58 -0
- package/dist/modes/rpc/tool-child/payload-ref.js +166 -0
- package/dist/modes/rpc/tool-child/payload-ref.test.d.ts +2 -0
- package/dist/modes/rpc/tool-child/payload-ref.test.js +254 -0
- package/dist/modes/rpc/tool-child/payload-wire.d.ts +13 -0
- package/dist/modes/rpc/tool-child/payload-wire.js +22 -0
- package/dist/modes/rpc/tool-child/protocol-guard.d.ts +9 -0
- package/dist/modes/rpc/tool-child/protocol-guard.js +128 -0
- package/dist/modes/rpc/tool-child/protocol.d.ts +192 -0
- package/dist/modes/rpc/tool-child/protocol.js +54 -0
- package/dist/utils/wsl.d.ts +2 -2
- package/dist/utils/wsl.js +4 -4
- package/docs/providers.md +2 -0
- package/docs/rpc.md +20 -2
- package/docs/settings.md +1 -0
- package/docs/terminal-setup.md +2 -0
- package/package.json +10 -6
- package/dist/bundle/chunks/chunk-COJK5FDH.js +0 -2
- package/dist/bundle/chunks/chunk-ES3GHZE5.js +0 -395
- package/dist/bundle/chunks/chunk-IDXW4FAO.js +0 -2
- package/dist/bundle/chunks/chunk-PFNBY43W.js +0 -2
- package/dist/bundle/chunks/chunk-SA2ZLWBD.js +0 -11
- package/dist/bundle/chunks/openai-codex-responses-5LRKZCBW.js +0 -10
|
@@ -51,7 +51,7 @@ export interface AppKeybindings {
|
|
|
51
51
|
"app.tree.filter.cycleBackward": true;
|
|
52
52
|
}
|
|
53
53
|
export type AppKeybinding = keyof AppKeybindings;
|
|
54
|
-
export declare function useWindowsKeybindings(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): boolean;
|
|
54
|
+
export declare function useWindowsKeybindings(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv, procVersionPath?: string): boolean;
|
|
55
55
|
declare module "@earendil-works/pi-tui" {
|
|
56
56
|
interface Keybindings extends AppKeybindings {
|
|
57
57
|
}
|
package/dist/core/keybindings.js
CHANGED
|
@@ -3,8 +3,9 @@ import { existsSync, readFileSync } from "fs";
|
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { getAgentDir } from "../config.js";
|
|
5
5
|
import { stripBom } from "../utils/text.js";
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { isWSL } from "../utils/wsl.js";
|
|
7
|
+
export function useWindowsKeybindings(platform = process.platform, env = process.env, procVersionPath) {
|
|
8
|
+
return platform === "win32" || (platform === "linux" && isWSL(env, procVersionPath));
|
|
8
9
|
}
|
|
9
10
|
const windowsKeybindings = useWindowsKeybindings();
|
|
10
11
|
export const QUESTION_ANSWER_PRIMARY_KEY = "alt+up";
|
|
@@ -11,7 +11,7 @@ import { ultrafastSelectionWarning } from "./ultrafast-lanes.js";
|
|
|
11
11
|
/** Default model IDs for each known provider */
|
|
12
12
|
export const defaultModelPerProvider = {
|
|
13
13
|
"alibaba-token-plan": "qwen3.7-max",
|
|
14
|
-
"amazon-bedrock": "us.anthropic.claude-opus-
|
|
14
|
+
"amazon-bedrock": "us.anthropic.claude-opus-5-5",
|
|
15
15
|
"ant-ling": "Ring-2.6-1T",
|
|
16
16
|
"anthropic-subscription": "claude-opus-5-5",
|
|
17
17
|
anthropic: "claude-opus-5-5",
|
|
@@ -24,8 +24,13 @@ export declare class SessionPendingInputJournal implements PendingInputJournal {
|
|
|
24
24
|
* its directory are synced before this returns.
|
|
25
25
|
*/
|
|
26
26
|
append(record: PendingInputJournalRecord): Promise<void>;
|
|
27
|
-
private refuseCorruptTransitionLines;
|
|
28
27
|
read(): Promise<readonly PendingInputJournalRecord[]>;
|
|
28
|
+
/**
|
|
29
|
+
* A persisted session's transitions come from one streaming pass over its file, never from `getEntries()`:
|
|
30
|
+
* on a trimmed session that would materialize and keep the whole history (senpi#3519, #3535). An in-memory
|
|
31
|
+
* session has no file, so its entries are the journal.
|
|
32
|
+
*/
|
|
33
|
+
private transitionEntries;
|
|
29
34
|
}
|
|
30
35
|
export declare class PendingInputJournalCorruptError extends Error {
|
|
31
36
|
readonly code = "pending_input_journal_corrupt";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { open
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3
4
|
import { PENDING_INPUT_TRANSITION, } from "./pending-input-store.js";
|
|
4
5
|
/** On Windows a directory handle cannot be flushed (EPERM); the file sync is all the platform offers there. */
|
|
5
6
|
export function fsJournalDurability(platform = process.platform) {
|
|
@@ -46,26 +47,8 @@ export class SessionPendingInputJournal {
|
|
|
46
47
|
throw new PendingInputAppendUncertainError(record.cursor, error);
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
|
-
async refuseCorruptTransitionLines() {
|
|
50
|
-
const file = this.session.isPersisted() ? this.session.getSessionFile() : undefined;
|
|
51
|
-
if (file === undefined)
|
|
52
|
-
return;
|
|
53
|
-
const corrupt = await corruptTransitionLine(file).catch((error) => {
|
|
54
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
55
|
-
return undefined;
|
|
56
|
-
throw error;
|
|
57
|
-
});
|
|
58
|
-
if (corrupt !== undefined)
|
|
59
|
-
throw new PendingInputJournalCorruptError(corrupt);
|
|
60
|
-
}
|
|
61
50
|
async read() {
|
|
62
|
-
await this.
|
|
63
|
-
const records = [];
|
|
64
|
-
for (const entry of this.session.getEntries()) {
|
|
65
|
-
if (entry.type !== "custom" || entry.customType !== PENDING_INPUT_TRANSITION)
|
|
66
|
-
continue;
|
|
67
|
-
records.push(parseRecord(entry.data, entry.id));
|
|
68
|
-
}
|
|
51
|
+
const records = (await this.transitionEntries()).map((entry) => parseRecord(entry.data, entry.id));
|
|
69
52
|
records.sort((a, b) => a.cursor - b.cursor);
|
|
70
53
|
// The session loader skips a line that does not parse, so a lost transition shows only as a cursor gap.
|
|
71
54
|
records.forEach((record, index) => {
|
|
@@ -74,6 +57,27 @@ export class SessionPendingInputJournal {
|
|
|
74
57
|
});
|
|
75
58
|
return records;
|
|
76
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* A persisted session's transitions come from one streaming pass over its file, never from `getEntries()`:
|
|
62
|
+
* on a trimmed session that would materialize and keep the whole history (senpi#3519, #3535). An in-memory
|
|
63
|
+
* session has no file, so its entries are the journal.
|
|
64
|
+
*/
|
|
65
|
+
async transitionEntries() {
|
|
66
|
+
// A session whose file is not written yet (a branch with no message so far) holds its transitions only in
|
|
67
|
+
// memory; it was never trimmed, so reading its entries costs nothing (#3586 review H1).
|
|
68
|
+
const file = this.session.isPersisted() && this.session.isTranscriptFlushed() ? this.session.getSessionFile() : undefined;
|
|
69
|
+
if (file === undefined)
|
|
70
|
+
return this.session
|
|
71
|
+
.getEntries()
|
|
72
|
+
.flatMap((entry) => entry.type === "custom" && entry.customType === PENDING_INPUT_TRANSITION
|
|
73
|
+
? [{ data: entry.data, id: entry.id }]
|
|
74
|
+
: []);
|
|
75
|
+
return scanTransitions(file).catch((error) => {
|
|
76
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
77
|
+
return [];
|
|
78
|
+
throw error;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
77
81
|
}
|
|
78
82
|
/**
|
|
79
83
|
* The session loader skips a line that does not parse, so a corrupt transition would vanish with no cursor gap.
|
|
@@ -81,23 +85,71 @@ export class SessionPendingInputJournal {
|
|
|
81
85
|
* corrupt and refused, the last line included. A final segment with no newline is a tail torn by a crash: it was
|
|
82
86
|
* never committed, the loader drops it, and the next append cuts it back, so it is tolerated. A torn line keeps
|
|
83
87
|
* its prefix, and `appendCustomEntry` writes `type` and `customType` first, so the marker survives a tear.
|
|
88
|
+
* Only lines naming a transition are parsed, so the pass holds one line at a time, never the transcript.
|
|
84
89
|
*/
|
|
85
|
-
async function
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
async function scanTransitions(file) {
|
|
91
|
+
const handle = await open(file, "r");
|
|
92
|
+
try {
|
|
93
|
+
const decoder = new StringDecoder("utf8");
|
|
94
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
95
|
+
const entries = [];
|
|
96
|
+
let pending = "";
|
|
97
|
+
let lineNumber = 0;
|
|
98
|
+
while (true) {
|
|
99
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
|
|
100
|
+
if (bytesRead === 0)
|
|
101
|
+
break;
|
|
102
|
+
pending += decoder.write(buffer.subarray(0, bytesRead));
|
|
103
|
+
let start = 0;
|
|
104
|
+
let newline = pending.indexOf("\n", start);
|
|
105
|
+
while (newline !== -1) {
|
|
106
|
+
lineNumber += 1;
|
|
107
|
+
const entry = transitionOf(pending.slice(start, newline), lineNumber);
|
|
108
|
+
if (entry !== undefined)
|
|
109
|
+
entries.push(entry);
|
|
110
|
+
start = newline + 1;
|
|
111
|
+
newline = pending.indexOf("\n", start);
|
|
112
|
+
}
|
|
113
|
+
pending = pending.slice(start);
|
|
98
114
|
}
|
|
115
|
+
// After the last newline is the torn tail of a crash, or nothing. A final line that parses was written whole
|
|
116
|
+
// (a writer outside senpi that omits the newline); the loader keeps it, so it is read too (#3586 review L1).
|
|
117
|
+
const tail = finalSegmentTransition(pending, lineNumber + 1);
|
|
118
|
+
if (tail !== undefined)
|
|
119
|
+
entries.push(tail);
|
|
120
|
+
return entries;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await handle.close();
|
|
99
124
|
}
|
|
100
|
-
|
|
125
|
+
}
|
|
126
|
+
function finalSegmentTransition(segment, lineNumber) {
|
|
127
|
+
if (!segment.includes(PENDING_INPUT_TRANSITION))
|
|
128
|
+
return undefined;
|
|
129
|
+
try {
|
|
130
|
+
JSON.parse(segment);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
return transitionOf(segment, lineNumber);
|
|
136
|
+
}
|
|
137
|
+
function transitionOf(line, lineNumber) {
|
|
138
|
+
if (!line.includes(PENDING_INPUT_TRANSITION))
|
|
139
|
+
return undefined;
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(line);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw new PendingInputJournalCorruptError(lineNumber);
|
|
146
|
+
}
|
|
147
|
+
if (typeof parsed !== "object" ||
|
|
148
|
+
parsed === null ||
|
|
149
|
+
!("type" in parsed && parsed.type === "custom") ||
|
|
150
|
+
!("customType" in parsed && parsed.customType === PENDING_INPUT_TRANSITION))
|
|
151
|
+
return undefined;
|
|
152
|
+
return { data: "data" in parsed ? parsed.data : undefined, id: "id" in parsed ? String(parsed.id) : "" };
|
|
101
153
|
}
|
|
102
154
|
export class PendingInputJournalCorruptError extends Error {
|
|
103
155
|
constructor(lineNumber) {
|
|
@@ -157,11 +209,15 @@ function parseRecord(data, entryId) {
|
|
|
157
209
|
const result = "result" in data ? data.result : undefined;
|
|
158
210
|
if (result !== undefined && !isOperationResult(result))
|
|
159
211
|
throw new PendingInputJournalError(entryId);
|
|
212
|
+
const batch = "batch" in data ? data.batch : undefined;
|
|
213
|
+
if (batch !== undefined && !(Array.isArray(batch) && batch.every(isPendingInput)))
|
|
214
|
+
throw new PendingInputJournalError(entryId);
|
|
160
215
|
return {
|
|
161
216
|
type: PENDING_INPUT_TRANSITION,
|
|
162
217
|
cursor: data.cursor,
|
|
163
218
|
input: data.input,
|
|
164
219
|
...(result === undefined ? {} : { result }),
|
|
220
|
+
...(batch === undefined ? {} : { batch }),
|
|
165
221
|
};
|
|
166
222
|
}
|
|
167
223
|
function isOperationResult(value) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public pending-input lifecycle (senpi#3549), exported as `@code-yeongyu/senpi/pending-input`: the one
|
|
3
|
+
* reducer, its projection and closed types, and the store over an injected journal. Other owners (the desktop
|
|
4
|
+
* server for native providers, the omo child prelaunch queue) embed this exact module instead of reimplementing
|
|
5
|
+
* it. Re-exports only.
|
|
6
|
+
*/
|
|
7
|
+
export { reducePendingInput } from "./pending-input.ts";
|
|
8
|
+
export { type PendingInputAction, type PendingInputProjection, type PendingInputState, type PendingInputStatusCode, projectPendingInput, } from "./pending-input-projection.ts";
|
|
9
|
+
export { PENDING_INPUT_TRANSITION, type PendingInputJournal, type PendingInputJournalRecord, type PendingInputListener, type PendingInputRead, PendingInputStore, type PendingInputStoreOptions, PendingInputStoreUnavailableError, type PendingInputTransaction, type StopItem, type StopReceipt, } from "./pending-input-store.ts";
|
|
10
|
+
export type * from "./pending-input-types.ts";
|
|
11
|
+
//# sourceMappingURL=pending-input-public.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public pending-input lifecycle (senpi#3549), exported as `@code-yeongyu/senpi/pending-input`: the one
|
|
3
|
+
* reducer, its projection and closed types, and the store over an injected journal. Other owners (the desktop
|
|
4
|
+
* server for native providers, the omo child prelaunch queue) embed this exact module instead of reimplementing
|
|
5
|
+
* it. Re-exports only.
|
|
6
|
+
*/
|
|
7
|
+
export { reducePendingInput } from "./pending-input.js";
|
|
8
|
+
export { projectPendingInput, } from "./pending-input-projection.js";
|
|
9
|
+
export { PENDING_INPUT_TRANSITION, PendingInputStore, PendingInputStoreUnavailableError, } from "./pending-input-store.js";
|
|
10
|
+
//# sourceMappingURL=pending-input-public.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { QueuedInputPort } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { PendingInputStore } from "./pending-input-store.ts";
|
|
3
|
+
export interface SessionQueuedInputPortOptions {
|
|
4
|
+
readonly store: PendingInputStore;
|
|
5
|
+
readonly ownerId: string;
|
|
6
|
+
readonly writerGeneration: number;
|
|
7
|
+
readonly nextAttemptId: () => string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The session's side of the agent queues (senpi#3549). Every drain reserves the identified inputs it hands to a
|
|
11
|
+
* turn, every consumption commits them, and every give-back releases them, each under the store's gate together
|
|
12
|
+
* with the native buffer change. Input the store never admitted (no client id, or a legacy id) passes through.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createSessionQueuedInputPort(options: SessionQueuedInputPortOptions): QueuedInputPort;
|
|
15
|
+
//# sourceMappingURL=pending-input-session-port.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session's side of the agent queues (senpi#3549). Every drain reserves the identified inputs it hands to a
|
|
3
|
+
* turn, every consumption commits them, and every give-back releases them, each under the store's gate together
|
|
4
|
+
* with the native buffer change. Input the store never admitted (no client id, or a legacy id) passes through.
|
|
5
|
+
*/
|
|
6
|
+
export function createSessionQueuedInputPort(options) {
|
|
7
|
+
const attempts = new Map();
|
|
8
|
+
const reserve = async (tx, message) => {
|
|
9
|
+
const clientMessageId = clientMessageIdOf(message);
|
|
10
|
+
const input = clientMessageId === undefined ? undefined : tx.current(clientMessageId);
|
|
11
|
+
if (clientMessageId === undefined || input === undefined)
|
|
12
|
+
return true;
|
|
13
|
+
const token = {
|
|
14
|
+
ownerId: options.ownerId,
|
|
15
|
+
writerGeneration: options.writerGeneration,
|
|
16
|
+
lifetimeId: input.handle.lifetimeId,
|
|
17
|
+
attemptId: options.nextAttemptId(),
|
|
18
|
+
clientMessageId,
|
|
19
|
+
};
|
|
20
|
+
await tx.apply(clientMessageId, { type: "reserve", token });
|
|
21
|
+
if (tx.current(clientMessageId)?.attempt?.attemptId !== token.attemptId)
|
|
22
|
+
return false;
|
|
23
|
+
attempts.set(message, token);
|
|
24
|
+
return true;
|
|
25
|
+
};
|
|
26
|
+
// One journal record for the whole batch: a failed write leaves every input as it was, never half delivered.
|
|
27
|
+
const settle = async (tx, messages, type) => {
|
|
28
|
+
const tokens = messages.flatMap((message) => {
|
|
29
|
+
const token = attempts.get(message);
|
|
30
|
+
return token === undefined ? [] : [token];
|
|
31
|
+
});
|
|
32
|
+
if (tokens.length === 0)
|
|
33
|
+
return tokens;
|
|
34
|
+
await tx.applyAll(tokens.map((token) => ({ clientMessageId: token.clientMessageId, event: { type, token } })));
|
|
35
|
+
for (const message of messages)
|
|
36
|
+
attempts.delete(message);
|
|
37
|
+
return tokens;
|
|
38
|
+
};
|
|
39
|
+
// Consumption was never committed, so no consumer can hold these: removing them is fenced (senpi#3549).
|
|
40
|
+
const cancelOf = (clientMessageId) => ({
|
|
41
|
+
clientMessageId,
|
|
42
|
+
event: { type: "cancel", cause: "stop", consumerFenced: true },
|
|
43
|
+
});
|
|
44
|
+
const cancelClaims = async (tx, messages) => {
|
|
45
|
+
const ids = messages.flatMap((message) => {
|
|
46
|
+
const token = attempts.get(message);
|
|
47
|
+
attempts.delete(message);
|
|
48
|
+
return token === undefined ? [] : [token.clientMessageId];
|
|
49
|
+
});
|
|
50
|
+
if (ids.length > 0)
|
|
51
|
+
await tx.applyAll(ids.map(cancelOf));
|
|
52
|
+
};
|
|
53
|
+
const cancelReleased = async (tx, messages) => {
|
|
54
|
+
const ids = messages.flatMap((message) => {
|
|
55
|
+
const clientMessageId = clientMessageIdOf(message);
|
|
56
|
+
return clientMessageId === undefined || tx.current(clientMessageId) === undefined ? [] : [clientMessageId];
|
|
57
|
+
});
|
|
58
|
+
if (ids.length > 0)
|
|
59
|
+
await tx.applyAll(ids.map(cancelOf));
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
claim: (_queue, drain) => options.store.transaction(async (tx) => {
|
|
63
|
+
const claimed = [];
|
|
64
|
+
// A copy the store no longer authorizes (removed, delivered, failed) is stale and is not delivered.
|
|
65
|
+
for (const message of drain())
|
|
66
|
+
if (await reserve(tx, message))
|
|
67
|
+
claimed.push(message);
|
|
68
|
+
return claimed;
|
|
69
|
+
}),
|
|
70
|
+
commitConsumption: (_queue, messages) => options.store.transaction(async (tx) => {
|
|
71
|
+
await settle(tx, messages, "consumed");
|
|
72
|
+
}),
|
|
73
|
+
// Journal first: if the release cannot be recorded the input stays `sending` (uncertain on reopen) and is
|
|
74
|
+
// not put back natively, so it can never be delivered twice. A clear that won keeps the messages out of the
|
|
75
|
+
// queue and their records removed, in one record. Only a clear landing during the release's own append
|
|
76
|
+
// needs the second, removing record.
|
|
77
|
+
release: (_queue, messages, giveBack) => options.store.transaction(async (tx) => {
|
|
78
|
+
if (!giveBack.restorable()) {
|
|
79
|
+
await cancelClaims(tx, messages);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
await settle(tx, messages, "release");
|
|
83
|
+
if (!giveBack.restore())
|
|
84
|
+
await cancelReleased(tx, messages);
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function clientMessageIdOf(message) {
|
|
89
|
+
if (message.role !== "user" || !("clientMessageId" in message))
|
|
90
|
+
return undefined;
|
|
91
|
+
return typeof message.clientMessageId === "string" ? message.clientMessageId : undefined;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=pending-input-session-port.js.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { QueuedInputPort } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ImageContent } from "@earendil-works/pi-ai";
|
|
3
|
+
import { SessionPendingInputJournal } from "./pending-input-journal.ts";
|
|
4
|
+
import type { PendingInputProjection } from "./pending-input-projection.ts";
|
|
5
|
+
import { type PendingInputJournal } from "./pending-input-store.ts";
|
|
6
|
+
import type { PendingInputMode, PendingInputOperationResult } from "./pending-input-types.ts";
|
|
7
|
+
export interface IdentifiedInput {
|
|
8
|
+
readonly clientMessageId: string;
|
|
9
|
+
readonly text: string;
|
|
10
|
+
readonly images?: readonly ImageContent[];
|
|
11
|
+
readonly mode: PendingInputMode;
|
|
12
|
+
}
|
|
13
|
+
export interface SessionPendingInputsOptions {
|
|
14
|
+
readonly sessionManager: ConstructorParameters<typeof SessionPendingInputJournal>[0];
|
|
15
|
+
/** The session's own queue-id counter, so a store handle and the session's queued record share one id. */
|
|
16
|
+
readonly nextQueuedId: () => string;
|
|
17
|
+
/** Receives every committed change, in cursor order. */
|
|
18
|
+
readonly onChange?: (update: PendingInputUpdate) => void;
|
|
19
|
+
/** Defaults to the session JSONL; injectable so tests can fail an append. */
|
|
20
|
+
readonly journal?: PendingInputJournal;
|
|
21
|
+
}
|
|
22
|
+
export interface PendingInputUpdate {
|
|
23
|
+
readonly lifetimeId: string;
|
|
24
|
+
readonly cursor: number;
|
|
25
|
+
readonly changes: readonly PendingInputProjection[];
|
|
26
|
+
}
|
|
27
|
+
/** Every input that still needs an outcome, plus an explicit answer for each id the caller already knows. */
|
|
28
|
+
export interface PendingInputSnapshot {
|
|
29
|
+
readonly lifetimeId: string;
|
|
30
|
+
readonly throughCursor: number;
|
|
31
|
+
readonly inputs: readonly PendingInputProjection[];
|
|
32
|
+
readonly outcomes: readonly ({
|
|
33
|
+
readonly clientMessageId: string;
|
|
34
|
+
readonly status: "admitted";
|
|
35
|
+
readonly projection: PendingInputProjection;
|
|
36
|
+
} | {
|
|
37
|
+
readonly clientMessageId: string;
|
|
38
|
+
readonly status: "not_admitted";
|
|
39
|
+
})[];
|
|
40
|
+
}
|
|
41
|
+
/** The legacy answers of `dequeue_message` / `edit_queued_message`. */
|
|
42
|
+
export type QueuedInputChangeOutcome = "removed" | "edited" | "already_delivered" | "not_found" | "conflict";
|
|
43
|
+
export interface QueuedInputChangeRequest {
|
|
44
|
+
readonly clientMessageId: string;
|
|
45
|
+
/** Undefined removes the input; a string replaces its text. */
|
|
46
|
+
readonly text: string | undefined;
|
|
47
|
+
readonly expectedRevision?: number;
|
|
48
|
+
readonly effectId?: string;
|
|
49
|
+
}
|
|
50
|
+
export declare class ClientMessageIdConflictError extends Error {
|
|
51
|
+
readonly code = "client_message_id_conflict";
|
|
52
|
+
readonly clientMessageId: string;
|
|
53
|
+
constructor(clientMessageId: string);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The pending inputs of one AgentSession (senpi#3549): the store, opened on the first identified input, and the
|
|
57
|
+
* agent queue port over it. Input without a client id never touches the store and keeps the legacy queue path.
|
|
58
|
+
*/
|
|
59
|
+
export declare class SessionPendingInputs {
|
|
60
|
+
readonly lifetimeId: `${string}-${string}-${string}-${string}-${string}`;
|
|
61
|
+
private readonly options;
|
|
62
|
+
private storeOpening;
|
|
63
|
+
private port;
|
|
64
|
+
private attempts;
|
|
65
|
+
/** Bumped synchronously by every clear, so a change can tell a clear it raced from a real divergence. */
|
|
66
|
+
private clears;
|
|
67
|
+
constructor(options: SessionPendingInputsOptions);
|
|
68
|
+
/**
|
|
69
|
+
* How a restored input (from the RPC admission ledger after a reopen) re-enters the native queue: under the
|
|
70
|
+
* store's rebound queue id while it is still queued; not at all once the store retired it (delivered, removed,
|
|
71
|
+
* failed); `undefined` when the store never held it, so the caller restores it as before.
|
|
72
|
+
*/
|
|
73
|
+
restoreHandle(clientMessageId: string): Promise<{
|
|
74
|
+
readonly queuedId: string;
|
|
75
|
+
} | "retired" | undefined>;
|
|
76
|
+
/**
|
|
77
|
+
* Admits an identified input and, only when it is newly admitted, puts its native copy in the agent queue
|
|
78
|
+
* inside the same gated step, so no drain, edit or removal can run between the two. A repeated submit of the
|
|
79
|
+
* same input is answered from the record (`replayed`) and enqueues nothing.
|
|
80
|
+
*/
|
|
81
|
+
admit(input: IdentifiedInput, enqueue: (queuedId: string) => void): Promise<PendingInputOperationResult | undefined>;
|
|
82
|
+
/**
|
|
83
|
+
* Edits or removes an identified input: the store transition is journaled first, and only when it answers
|
|
84
|
+
* `edited`/`removed` does `applyNative` change the native queue, inside the same gated step, so no drain
|
|
85
|
+
* can see one without the other. Returns undefined when the store never admitted this input.
|
|
86
|
+
*/
|
|
87
|
+
change(request: QueuedInputChangeRequest, applyNative: () => QueuedInputChangeOutcome): Promise<QueuedInputChangeOutcome | undefined>;
|
|
88
|
+
/**
|
|
89
|
+
* Records an explicit clear (Stop, `clear_queue`, unload with clearQueue) of identified inputs that just left the
|
|
90
|
+
* native queue: each still-queued one is removed by `stop`, so a reopen never queues it again.
|
|
91
|
+
*/
|
|
92
|
+
recordCleared(clientMessageIds: readonly string[]): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Pending inputs (queued, sending, failed) and the outcome of each known id, consistent through
|
|
95
|
+
* `throughCursor`: apply it, then only updates with a later cursor. Absence from `inputs` is never an outcome.
|
|
96
|
+
*/
|
|
97
|
+
snapshot(knownClientMessageIds?: readonly string[]): Promise<PendingInputSnapshot>;
|
|
98
|
+
/** The agent's queue port. Before the store opens nothing identified is queued, so it drains natively. */
|
|
99
|
+
queuedInputPort(): QueuedInputPort;
|
|
100
|
+
private store;
|
|
101
|
+
private openPort;
|
|
102
|
+
}
|
|
103
|
+
/** The store changed an input that the native queue no longer held: the two views disagree and must not be hidden. */
|
|
104
|
+
export declare class PendingInputDivergenceError extends Error {
|
|
105
|
+
readonly code = "pending_input_divergence";
|
|
106
|
+
constructor(clientMessageId: string, recorded: string, native: string);
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=pending-input-session.d.ts.map
|