@code-yeongyu/senpi 2026.10.18-1 → 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 +29 -0
- package/dist/bundle/chunks/{anthropic-messages-3CFTFCKV.js → anthropic-messages-EMTBHJMM.js} +2 -2
- package/dist/bundle/chunks/{args-SJWMIH2H.js → args-LWR7VFKS.js} +1 -1
- package/dist/bundle/chunks/{chunk-IRKZ6FVC.js → chunk-35M5MVUZ.js} +149 -145
- package/dist/bundle/chunks/chunk-3DNYAO3P.js +2 -0
- package/dist/bundle/chunks/{chunk-T2BEB5SO.js → chunk-7QPFFAJT.js} +1 -1
- package/dist/bundle/chunks/{chunk-4RUVN7K3.js → chunk-BOCN744P.js} +1 -1
- package/dist/bundle/chunks/{chunk-DDHBG2HU.js → chunk-FM7NDZR5.js} +1 -1
- package/dist/bundle/chunks/{chunk-OMY4EC25.js → chunk-FSENE7JK.js} +2 -2
- package/dist/bundle/chunks/{chunk-Z2LCTXS2.js → chunk-G7LLAQXI.js} +3 -3
- package/dist/bundle/chunks/{chunk-NRHOGI4Z.js → chunk-IM6ZWYTW.js} +2 -2
- package/dist/bundle/chunks/{chunk-MLKYJIUK.js → chunk-KM4FMQII.js} +1 -1
- package/dist/bundle/chunks/{chunk-OOWML2KE.js → chunk-PMGEQYEP.js} +2 -2
- package/dist/bundle/chunks/chunk-QGJLZ6BI.js +2 -0
- package/dist/bundle/chunks/{chunk-6EXDMO4S.js → chunk-QJ6MUFXY.js} +1 -1
- package/dist/bundle/chunks/{chunk-FZTO2ICI.js → chunk-QRGAMVEE.js} +1 -1
- package/dist/bundle/chunks/{chunk-3QVP3ZVI.js → chunk-QXEWAKXA.js} +1 -1
- package/dist/bundle/chunks/{chunk-UUMKAA7S.js → chunk-RL5QMGQD.js} +9 -9
- package/dist/bundle/chunks/chunk-T36LJ6MW.js +3 -0
- package/dist/bundle/chunks/{chunk-GAHIHXVE.js → chunk-VEKNDMUA.js} +1 -1
- package/dist/bundle/chunks/{chunk-TFXUF2IL.js → chunk-VPV7ADF3.js} +1 -1
- package/dist/bundle/chunks/{chunk-RELAITQ7.js → chunk-X6TWPBI6.js} +1 -1
- package/dist/bundle/chunks/{chunk-PKDWFVE4.js → chunk-YUMRZIKJ.js} +6 -6
- package/dist/bundle/chunks/chunk-ZPSJQHDK.js +395 -0
- package/dist/bundle/chunks/{cli-main-JCSB54VT.js → cli-main-EABMHWFV.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-FVPGRSYA.js → help-fast-path-BYWJN6YX.js} +1 -1
- package/dist/bundle/chunks/{host-command-X2ZZFTME.js → host-command-5T34JLBP.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-OKWTQFUJ.js → host-lifecycle-GSRIQP6Z.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/{host-session-command-HKJKOAVS.js → host-session-command-LF6JODPX.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-GJERAFGV.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/{multi-session-host-JWMGLKAG.js → multi-session-host-3J7STKGD.js} +4 -4
- package/dist/bundle/chunks/{openai-codex-responses-RLYFSLK6.js → openai-codex-responses-7YOBPZ2N.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-OWNSXMTD.js → openai-completions-HSZPYXEA.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-FSVS2ND2.js → package-manager-cli-LWQZSWKL.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-M6QV2MMB.js → rotation-stream-QRRWL73X.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-TPG3J3ZJ.js → rpc-mode-6LSWRKXR.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-PAVLVMWL.js → schedule-command-7CXMW4WK.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-NSMRMKKK.js → session-control-endpoint-E265WAIQ.js} +1 -1
- package/dist/bundle/chunks/{session-picker-UV6IVWJY.js → session-picker-XRB3LP5W.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +298 -293
- 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/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/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-observe-request.js +1 -0
- 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/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 +16 -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-IDXW4FAO.js +0 -2
- package/dist/bundle/chunks/chunk-PFNBY43W.js +0 -2
- package/dist/bundle/chunks/chunk-QBD4LY7D.js +0 -395
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import lockfile from "proper-lockfile";
|
|
4
|
+
import { FILE_STORAGE_LOCK_RETRY_MAX_DELAY_MS, FILE_STORAGE_LOCK_RETRY_MIN_DELAY_MS, isLockError, } from "../lockfile-policy.js";
|
|
5
|
+
import { serializeByKey } from "../session-sidecar-store.js";
|
|
6
|
+
import { parseMoveId } from "./bundle.js";
|
|
7
|
+
export class MoveFencedError extends Error {
|
|
8
|
+
constructor(moveId, record) {
|
|
9
|
+
super(`Move ${moveId} can no longer be imported here: it was already answered (${record.state}).`);
|
|
10
|
+
this.code = "move_fenced";
|
|
11
|
+
this.name = "MoveFencedError";
|
|
12
|
+
this.record = record;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const LOCK_OPTIONS = { realpath: false, stale: 10_000, update: 2_000, retries: 0 };
|
|
16
|
+
const LOCK_WAIT_MS = 15_000;
|
|
17
|
+
const DECLINE_REASONS = [
|
|
18
|
+
"no_record",
|
|
19
|
+
"expired",
|
|
20
|
+
"cancelled",
|
|
21
|
+
"chunk_without_ready",
|
|
22
|
+
"repository",
|
|
23
|
+
];
|
|
24
|
+
const FAIL_REASONS = ["bundle_invalid", "import_failed"];
|
|
25
|
+
function parseRecord(value) {
|
|
26
|
+
if (typeof value !== "object" || value === null)
|
|
27
|
+
return undefined;
|
|
28
|
+
const { state, reason } = value;
|
|
29
|
+
if (state === "receiving" || state === "accepted")
|
|
30
|
+
return { state };
|
|
31
|
+
const decline = DECLINE_REASONS.find((known) => known === reason);
|
|
32
|
+
if (state === "declined" && decline !== undefined)
|
|
33
|
+
return { state, reason: decline };
|
|
34
|
+
const fail = FAIL_REASONS.find((known) => known === reason);
|
|
35
|
+
if (state === "failed" && fail !== undefined)
|
|
36
|
+
return { state, reason: fail };
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
export class MoveLedger {
|
|
40
|
+
constructor(dir) {
|
|
41
|
+
this.dir = dir;
|
|
42
|
+
}
|
|
43
|
+
/** Starts receiving a move; refused once the move was answered with anything but `receiving`. */
|
|
44
|
+
beginReceive(moveId) {
|
|
45
|
+
return this.locked(moveId, () => {
|
|
46
|
+
const record = this.read(moveId);
|
|
47
|
+
if (record?.state === "accepted" || record?.state === "receiving")
|
|
48
|
+
return;
|
|
49
|
+
if (record !== undefined)
|
|
50
|
+
throw new MoveFencedError(moveId, record);
|
|
51
|
+
this.write(moveId, { state: "receiving" });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** The signed answer a sender gets; `movedIn` is whether the session file here already records the move. */
|
|
55
|
+
answerStatus(moveId, here) {
|
|
56
|
+
return this.locked(moveId, () => {
|
|
57
|
+
const record = this.read(moveId);
|
|
58
|
+
if (here.movedIn || record?.state === "accepted")
|
|
59
|
+
return { state: "accepted" };
|
|
60
|
+
if (record !== undefined)
|
|
61
|
+
return record;
|
|
62
|
+
const declined = { state: "declined", reason: "no_record" };
|
|
63
|
+
this.write(moveId, declined);
|
|
64
|
+
return declined;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Cancel, expiry or a refused chunk: only before the bundle was verified and receiving began, never after. */
|
|
68
|
+
decline(moveId, reason) {
|
|
69
|
+
return this.settle(moveId, { state: "declined", reason }, ["accepted", "receiving"]);
|
|
70
|
+
}
|
|
71
|
+
/** A verified import that could not finish; it may end a receiving move but never an accepted one. */
|
|
72
|
+
fail(moveId, reason) {
|
|
73
|
+
return this.settle(moveId, { state: "failed", reason }, ["accepted"]);
|
|
74
|
+
}
|
|
75
|
+
/** Runs the import's commit (the atomic rename) under the move's lock, then records it accepted. */
|
|
76
|
+
commit(moveId, rename) {
|
|
77
|
+
return this.locked(moveId, async () => {
|
|
78
|
+
const record = this.read(moveId);
|
|
79
|
+
if (record !== undefined && record.state !== "receiving" && record.state !== "accepted") {
|
|
80
|
+
throw new MoveFencedError(moveId, record);
|
|
81
|
+
}
|
|
82
|
+
const result = await rename();
|
|
83
|
+
this.write(moveId, { state: "accepted" });
|
|
84
|
+
return result;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
settle(moveId, next, keeps) {
|
|
88
|
+
return this.locked(moveId, () => {
|
|
89
|
+
const record = this.read(moveId);
|
|
90
|
+
if (record !== undefined && keeps.includes(record.state))
|
|
91
|
+
return record;
|
|
92
|
+
this.write(moveId, next);
|
|
93
|
+
return next;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
path(moveId) {
|
|
97
|
+
return join(this.dir, `${parseMoveId(moveId)}.json`);
|
|
98
|
+
}
|
|
99
|
+
read(moveId) {
|
|
100
|
+
let raw;
|
|
101
|
+
try {
|
|
102
|
+
raw = readFileSync(this.path(moveId), "utf8");
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error.code === "ENOENT")
|
|
106
|
+
return undefined;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
const record = parseRecord(JSON.parse(raw));
|
|
110
|
+
if (record === undefined)
|
|
111
|
+
throw new Error(`Move record ${this.path(moveId)} is not a move record`);
|
|
112
|
+
return record;
|
|
113
|
+
}
|
|
114
|
+
write(moveId, record) {
|
|
115
|
+
const file = this.path(moveId);
|
|
116
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
117
|
+
writeFileSync(temp, `${JSON.stringify({ ...record, at: new Date().toISOString() })}\n`);
|
|
118
|
+
renameSync(temp, file);
|
|
119
|
+
}
|
|
120
|
+
locked(moveId, fn) {
|
|
121
|
+
const file = this.path(moveId);
|
|
122
|
+
return serializeByKey(file, async () => {
|
|
123
|
+
mkdirSync(this.dir, { recursive: true });
|
|
124
|
+
const release = await acquire(file);
|
|
125
|
+
try {
|
|
126
|
+
return await fn();
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await release().catch(() => undefined);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function acquire(file) {
|
|
135
|
+
const startedAt = Date.now();
|
|
136
|
+
for (let attempt = 0;; attempt += 1) {
|
|
137
|
+
try {
|
|
138
|
+
return await lockfile.lock(file, { ...LOCK_OPTIONS, lockfilePath: `${file}.lock` });
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (!isLockError(error))
|
|
142
|
+
throw error;
|
|
143
|
+
const waited = Date.now() - startedAt;
|
|
144
|
+
if (waited >= LOCK_WAIT_MS)
|
|
145
|
+
throw new Error(`Move record ${file} stayed locked for ${waited}ms`, { cause: error });
|
|
146
|
+
const delay = Math.min(FILE_STORAGE_LOCK_RETRY_MIN_DELAY_MS * 2 ** attempt, FILE_STORAGE_LOCK_RETRY_MAX_DELAY_MS);
|
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=move-ledger.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may write a session file when sessions move between devices (sisyphuslabs/omo-desktop-app#3137,
|
|
3
|
+
* senpi#3630). The file itself records ownership as a sequence of transitions, folded in order:
|
|
4
|
+
* - writable (no ownership entry yet, a `takeover` release, `session_moved_in`, `session_reclaimed`);
|
|
5
|
+
* - `moving` after a `move` release: only that move may continue, with `session_moved` (it arrived),
|
|
6
|
+
* `session_reclaimed` (it was given back) or `session_moved_in` (an import of that same move);
|
|
7
|
+
* - `moved` after `session_moved`: nothing may reopen it. A new move id arriving by import replaces the file.
|
|
8
|
+
* A transition the current state does not allow is refused when written and ignored when read. Entries name the
|
|
9
|
+
* session they belong to (`session_id`); a copy or branch under a new id ignores the entries it inherited.
|
|
10
|
+
*/
|
|
11
|
+
/** `customType` of the release entry; shared with `release_session` (`modes/rpc/session-release.ts`). */
|
|
12
|
+
export declare const SESSION_RELEASED = "session_released";
|
|
13
|
+
export declare const SESSION_MOVED_IN = "session_moved_in";
|
|
14
|
+
export declare const SESSION_MOVED = "session_moved";
|
|
15
|
+
export declare const SESSION_RECLAIMED = "session_reclaimed";
|
|
16
|
+
export type MoveId = string & {
|
|
17
|
+
readonly __brand: "MoveId";
|
|
18
|
+
};
|
|
19
|
+
export type RefusedOwnership = {
|
|
20
|
+
readonly writable: false;
|
|
21
|
+
readonly state: "moving";
|
|
22
|
+
readonly moveId: MoveId;
|
|
23
|
+
readonly target: string;
|
|
24
|
+
} | {
|
|
25
|
+
readonly writable: false;
|
|
26
|
+
readonly state: "moved";
|
|
27
|
+
readonly moveId: MoveId;
|
|
28
|
+
readonly target: string;
|
|
29
|
+
};
|
|
30
|
+
export type SessionOwnership = {
|
|
31
|
+
readonly writable: true;
|
|
32
|
+
} | RefusedOwnership;
|
|
33
|
+
export type OwnershipStep = {
|
|
34
|
+
readonly kind: "none";
|
|
35
|
+
} | {
|
|
36
|
+
readonly kind: "apply";
|
|
37
|
+
readonly next: SessionOwnership;
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: "refused";
|
|
40
|
+
};
|
|
41
|
+
interface EntryLike {
|
|
42
|
+
readonly type: string;
|
|
43
|
+
readonly customType?: string;
|
|
44
|
+
readonly data?: unknown;
|
|
45
|
+
readonly id?: unknown;
|
|
46
|
+
}
|
|
47
|
+
export declare function isOwnershipEntryType(customType: string | undefined): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The single transition rule. `sessionId` is the id of the session being written; an entry naming another session
|
|
50
|
+
* (inherited by a copy or branch) is not an ownership step for this one.
|
|
51
|
+
*/
|
|
52
|
+
export declare function ownershipStep(current: SessionOwnership, entry: EntryLike, sessionId: string | undefined): OwnershipStep;
|
|
53
|
+
export declare function headerSessionId(entries: readonly EntryLike[]): string | undefined;
|
|
54
|
+
export declare function sessionOwnership(entries: readonly EntryLike[]): SessionOwnership;
|
|
55
|
+
export declare class SessionMovedAwayError extends Error {
|
|
56
|
+
readonly code = "session_moved_away";
|
|
57
|
+
readonly ownership: RefusedOwnership;
|
|
58
|
+
constructor(sessionFile: string, ownership: RefusedOwnership);
|
|
59
|
+
}
|
|
60
|
+
export {};
|
|
61
|
+
//# sourceMappingURL=ownership.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may write a session file when sessions move between devices (sisyphuslabs/omo-desktop-app#3137,
|
|
3
|
+
* senpi#3630). The file itself records ownership as a sequence of transitions, folded in order:
|
|
4
|
+
* - writable (no ownership entry yet, a `takeover` release, `session_moved_in`, `session_reclaimed`);
|
|
5
|
+
* - `moving` after a `move` release: only that move may continue, with `session_moved` (it arrived),
|
|
6
|
+
* `session_reclaimed` (it was given back) or `session_moved_in` (an import of that same move);
|
|
7
|
+
* - `moved` after `session_moved`: nothing may reopen it. A new move id arriving by import replaces the file.
|
|
8
|
+
* A transition the current state does not allow is refused when written and ignored when read. Entries name the
|
|
9
|
+
* session they belong to (`session_id`); a copy or branch under a new id ignores the entries it inherited.
|
|
10
|
+
*/
|
|
11
|
+
/** `customType` of the release entry; shared with `release_session` (`modes/rpc/session-release.ts`). */
|
|
12
|
+
export const SESSION_RELEASED = "session_released";
|
|
13
|
+
export const SESSION_MOVED_IN = "session_moved_in";
|
|
14
|
+
export const SESSION_MOVED = "session_moved";
|
|
15
|
+
export const SESSION_RECLAIMED = "session_reclaimed";
|
|
16
|
+
const WRITABLE = { writable: true };
|
|
17
|
+
const NONE = { kind: "none" };
|
|
18
|
+
const REFUSED = { kind: "refused" };
|
|
19
|
+
const OWNERSHIP_TYPES = new Set([
|
|
20
|
+
SESSION_RELEASED,
|
|
21
|
+
SESSION_MOVED_IN,
|
|
22
|
+
SESSION_MOVED,
|
|
23
|
+
SESSION_RECLAIMED,
|
|
24
|
+
]);
|
|
25
|
+
export function isOwnershipEntryType(customType) {
|
|
26
|
+
return customType !== undefined && OWNERSHIP_TYPES.has(customType);
|
|
27
|
+
}
|
|
28
|
+
function field(data, key) {
|
|
29
|
+
if (typeof data !== "object" || data === null)
|
|
30
|
+
return undefined;
|
|
31
|
+
const value = data[key];
|
|
32
|
+
return typeof value === "string" ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
function requested(entry) {
|
|
35
|
+
const moveId = (field(entry.data, "move_id") ?? "");
|
|
36
|
+
const target = field(entry.data, "target") ?? "";
|
|
37
|
+
if (entry.customType === SESSION_MOVED)
|
|
38
|
+
return { writable: false, state: "moved", moveId, target };
|
|
39
|
+
if (entry.customType === SESSION_RELEASED && field(entry.data, "reason") === "move")
|
|
40
|
+
return { writable: false, state: "moving", moveId, target };
|
|
41
|
+
return WRITABLE;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The single transition rule. `sessionId` is the id of the session being written; an entry naming another session
|
|
45
|
+
* (inherited by a copy or branch) is not an ownership step for this one.
|
|
46
|
+
*/
|
|
47
|
+
export function ownershipStep(current, entry, sessionId) {
|
|
48
|
+
if (entry.type !== "custom" || !isOwnershipEntryType(entry.customType))
|
|
49
|
+
return NONE;
|
|
50
|
+
const owner = field(entry.data, "session_id");
|
|
51
|
+
if (owner !== undefined && sessionId !== undefined && owner !== sessionId)
|
|
52
|
+
return NONE;
|
|
53
|
+
const next = requested(entry);
|
|
54
|
+
if (current.writable)
|
|
55
|
+
return { kind: "apply", next };
|
|
56
|
+
if (current.state === "moved")
|
|
57
|
+
return REFUSED;
|
|
58
|
+
const sameMove = field(entry.data, "move_id") === current.moveId;
|
|
59
|
+
const continuesMove = entry.customType === SESSION_MOVED ||
|
|
60
|
+
entry.customType === SESSION_RECLAIMED ||
|
|
61
|
+
entry.customType === SESSION_MOVED_IN;
|
|
62
|
+
return sameMove && continuesMove ? { kind: "apply", next } : REFUSED;
|
|
63
|
+
}
|
|
64
|
+
export function headerSessionId(entries) {
|
|
65
|
+
const header = entries.find((entry) => entry.type === "session");
|
|
66
|
+
return typeof header?.id === "string" ? header.id : undefined;
|
|
67
|
+
}
|
|
68
|
+
export function sessionOwnership(entries) {
|
|
69
|
+
const sessionId = headerSessionId(entries);
|
|
70
|
+
let verdict = WRITABLE;
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const step = ownershipStep(verdict, entry, sessionId);
|
|
73
|
+
if (step.kind === "apply")
|
|
74
|
+
verdict = step.next;
|
|
75
|
+
}
|
|
76
|
+
return verdict;
|
|
77
|
+
}
|
|
78
|
+
export class SessionMovedAwayError extends Error {
|
|
79
|
+
constructor(sessionFile, ownership) {
|
|
80
|
+
const where = ownership.target === "" ? "another device" : ownership.target;
|
|
81
|
+
super(ownership.state === "moving"
|
|
82
|
+
? `This session is being moved to ${where}; it is read-only here. Wait for the move to finish, or make a copy to keep working here. (${sessionFile})`
|
|
83
|
+
: `This session was moved to ${where}; it is read-only here. Open it on ${where}, or make a copy here. (${sessionFile})`);
|
|
84
|
+
this.code = "session_moved_away";
|
|
85
|
+
this.name = "SessionMovedAwayError";
|
|
86
|
+
this.ownership = ownership;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=ownership.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MoveId } from "./ownership.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Puts input a move took out of a session back into its pending-input journal, as follow-ups in their original
|
|
4
|
+
* order. Each gets the id `move:<moveId>:<index>`, so running this twice admits each input once: the second pass is
|
|
5
|
+
* answered from the journal (`replayed`). The session restores them to its queue when it is next opened.
|
|
6
|
+
*/
|
|
7
|
+
export declare function requeueInput(sessionFile: string, moveId: MoveId, texts: readonly string[]): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=requeue.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SessionPendingInputs } from "../pending-input-session.js";
|
|
2
|
+
import { SessionManager } from "../session-manager.js";
|
|
3
|
+
/**
|
|
4
|
+
* Puts input a move took out of a session back into its pending-input journal, as follow-ups in their original
|
|
5
|
+
* order. Each gets the id `move:<moveId>:<index>`, so running this twice admits each input once: the second pass is
|
|
6
|
+
* answered from the journal (`replayed`). The session restores them to its queue when it is next opened.
|
|
7
|
+
*/
|
|
8
|
+
export async function requeueInput(sessionFile, moveId, texts) {
|
|
9
|
+
if (texts.length === 0)
|
|
10
|
+
return;
|
|
11
|
+
const sessionManager = SessionManager.open(sessionFile);
|
|
12
|
+
let next = 0;
|
|
13
|
+
const inputs = new SessionPendingInputs({ sessionManager, nextQueuedId: () => `move-${moveId}-${next++}` });
|
|
14
|
+
for (const [index, text] of texts.entries()) {
|
|
15
|
+
await inputs.admit({ clientMessageId: `move:${moveId}:${index}`, text, mode: "followUp" }, () => undefined);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=requeue.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface EntryLike {
|
|
2
|
+
readonly type: string;
|
|
3
|
+
readonly customType?: string;
|
|
4
|
+
readonly data?: unknown;
|
|
5
|
+
readonly id?: unknown;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The session-manager write funnel asks this before every persisted write. It folds only the entries appended since
|
|
9
|
+
* the last call (the funnel appends in place), and starts over when the entry array is replaced by a load or branch.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SessionWriteGuard {
|
|
12
|
+
private source;
|
|
13
|
+
private seen;
|
|
14
|
+
private sessionId;
|
|
15
|
+
private verdict;
|
|
16
|
+
assertWritable(entries: readonly EntryLike[], entry: EntryLike, sessionFile: string): void;
|
|
17
|
+
assertRewritable(entries: readonly EntryLike[], sessionFile: string): void;
|
|
18
|
+
isWritable(entries: readonly EntryLike[]): boolean;
|
|
19
|
+
private current;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=write-guard.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { headerSessionId, ownershipStep, SessionMovedAwayError } from "./ownership.js";
|
|
2
|
+
/**
|
|
3
|
+
* The session-manager write funnel asks this before every persisted write. It folds only the entries appended since
|
|
4
|
+
* the last call (the funnel appends in place), and starts over when the entry array is replaced by a load or branch.
|
|
5
|
+
*/
|
|
6
|
+
export class SessionWriteGuard {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.seen = 0;
|
|
9
|
+
this.verdict = { writable: true };
|
|
10
|
+
}
|
|
11
|
+
assertWritable(entries, entry, sessionFile) {
|
|
12
|
+
const ownership = this.current(entries);
|
|
13
|
+
if (ownership.writable)
|
|
14
|
+
return;
|
|
15
|
+
if (ownershipStep(ownership, entry, this.sessionId).kind !== "apply")
|
|
16
|
+
throw new SessionMovedAwayError(sessionFile, ownership);
|
|
17
|
+
}
|
|
18
|
+
assertRewritable(entries, sessionFile) {
|
|
19
|
+
const ownership = this.current(entries);
|
|
20
|
+
if (!ownership.writable)
|
|
21
|
+
throw new SessionMovedAwayError(sessionFile, ownership);
|
|
22
|
+
}
|
|
23
|
+
isWritable(entries) {
|
|
24
|
+
return this.current(entries).writable;
|
|
25
|
+
}
|
|
26
|
+
current(entries) {
|
|
27
|
+
if (entries !== this.source || entries.length < this.seen) {
|
|
28
|
+
this.source = entries;
|
|
29
|
+
this.seen = 0;
|
|
30
|
+
this.sessionId = headerSessionId(entries);
|
|
31
|
+
this.verdict = { writable: true };
|
|
32
|
+
}
|
|
33
|
+
for (; this.seen < entries.length; this.seen += 1) {
|
|
34
|
+
const entry = entries[this.seen];
|
|
35
|
+
if (entry === undefined)
|
|
36
|
+
continue;
|
|
37
|
+
const step = ownershipStep(this.verdict, entry, this.sessionId);
|
|
38
|
+
if (step.kind === "apply")
|
|
39
|
+
this.verdict = step.next;
|
|
40
|
+
}
|
|
41
|
+
return this.verdict;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=write-guard.js.map
|
|
@@ -21,6 +21,13 @@ export declare const DEFAULT_SESSION_SHUTDOWN_HANDLER_WARN_MS = 2000;
|
|
|
21
21
|
* hung handler still must not hold quit/reload/new/resume hostage.
|
|
22
22
|
*/
|
|
23
23
|
export declare const DEFAULT_SESSION_SHUTDOWN_HANDLER_TIMEOUT_MS = 10000;
|
|
24
|
+
/** Warn threshold for one handler of an agent lifecycle event the session awaits before persisting (senpi#3650). */
|
|
25
|
+
export declare const DEFAULT_EXTENSION_EVENT_HANDLER_WARN_MS = 60000;
|
|
26
|
+
/**
|
|
27
|
+
* Hard cap for one such handler. Above the 15 min summarization budget a blocking compaction in a `message_end`
|
|
28
|
+
* handler may legitimately use; past it the host stops waiting so the turn is persisted and queued input delivered.
|
|
29
|
+
*/
|
|
30
|
+
export declare const DEFAULT_EXTENSION_EVENT_HANDLER_TIMEOUT_MS = 1200000;
|
|
24
31
|
export type TuiMode = RendererTuiMode;
|
|
25
32
|
export type FullscreenExitOutput = "transcript" | "resume-hint";
|
|
26
33
|
/**
|
|
@@ -133,6 +140,8 @@ export interface Settings {
|
|
|
133
140
|
websocketConnectTimeoutMs?: number;
|
|
134
141
|
sessionShutdownHandlerWarnMs?: number;
|
|
135
142
|
sessionShutdownHandlerTimeoutMs?: number;
|
|
143
|
+
extensionEventHandlerWarnMs?: number;
|
|
144
|
+
extensionEventHandlerTimeoutMs?: number;
|
|
136
145
|
tuiMode?: TuiMode;
|
|
137
146
|
fullscreenExitOutput?: FullscreenExitOutput;
|
|
138
147
|
fullscreenScrollbar?: ScrollViewScrollbar;
|
|
@@ -422,6 +431,10 @@ export declare class SettingsManager {
|
|
|
422
431
|
*/
|
|
423
432
|
getSessionShutdownHandlerTimeoutMs(): number;
|
|
424
433
|
setSessionShutdownHandlerTimeoutMs(timeoutMs: number): void;
|
|
434
|
+
/** How long one agent lifecycle event handler may run before the host reports it as stalled. 0 disables. */
|
|
435
|
+
getExtensionEventHandlerWarnMs(): number;
|
|
436
|
+
/** Hard cap on one agent lifecycle event handler; past it the session's event queue moves on. 0 disables. */
|
|
437
|
+
getExtensionEventHandlerTimeoutMs(): number;
|
|
425
438
|
getProviderRetrySettings(): {
|
|
426
439
|
timeoutMs?: number;
|
|
427
440
|
maxRetries?: number;
|
|
@@ -36,6 +36,13 @@ export const DEFAULT_SESSION_SHUTDOWN_HANDLER_WARN_MS = 2_000;
|
|
|
36
36
|
* hung handler still must not hold quit/reload/new/resume hostage.
|
|
37
37
|
*/
|
|
38
38
|
export const DEFAULT_SESSION_SHUTDOWN_HANDLER_TIMEOUT_MS = 10_000;
|
|
39
|
+
/** Warn threshold for one handler of an agent lifecycle event the session awaits before persisting (senpi#3650). */
|
|
40
|
+
export const DEFAULT_EXTENSION_EVENT_HANDLER_WARN_MS = 60_000;
|
|
41
|
+
/**
|
|
42
|
+
* Hard cap for one such handler. Above the 15 min summarization budget a blocking compaction in a `message_end`
|
|
43
|
+
* handler may legitimately use; past it the host stops waiting so the turn is persisted and queued input delivered.
|
|
44
|
+
*/
|
|
45
|
+
export const DEFAULT_EXTENSION_EVENT_HANDLER_TIMEOUT_MS = 1_200_000;
|
|
39
46
|
const THINKING_LEVEL_VALUES = new Set([
|
|
40
47
|
"off",
|
|
41
48
|
"minimal",
|
|
@@ -1359,6 +1366,16 @@ export class SettingsManager {
|
|
|
1359
1366
|
this.markModified("sessionShutdownHandlerTimeoutMs");
|
|
1360
1367
|
this.save();
|
|
1361
1368
|
}
|
|
1369
|
+
/** How long one agent lifecycle event handler may run before the host reports it as stalled. 0 disables. */
|
|
1370
|
+
getExtensionEventHandlerWarnMs() {
|
|
1371
|
+
return (parseTimeoutSetting(this.settings.extensionEventHandlerWarnMs, "extensionEventHandlerWarnMs") ??
|
|
1372
|
+
DEFAULT_EXTENSION_EVENT_HANDLER_WARN_MS);
|
|
1373
|
+
}
|
|
1374
|
+
/** Hard cap on one agent lifecycle event handler; past it the session's event queue moves on. 0 disables. */
|
|
1375
|
+
getExtensionEventHandlerTimeoutMs() {
|
|
1376
|
+
return (parseTimeoutSetting(this.settings.extensionEventHandlerTimeoutMs, "extensionEventHandlerTimeoutMs") ??
|
|
1377
|
+
DEFAULT_EXTENSION_EVENT_HANDLER_TIMEOUT_MS);
|
|
1378
|
+
}
|
|
1362
1379
|
getProviderRetrySettings() {
|
|
1363
1380
|
return {
|
|
1364
1381
|
timeoutMs: this.settings.retry?.provider?.timeoutMs,
|
|
@@ -53,6 +53,8 @@ export interface DeadlineSessionOwner {
|
|
|
53
53
|
_recordDeadlineStop(deadline: TurnDeadline): void;
|
|
54
54
|
/** End the turn through the normal system abort (tools see the abort signal); resolves once the session is idle. */
|
|
55
55
|
abortTurnForDeadline(): Promise<void>;
|
|
56
|
+
/** Stop waiting on extension handlers that still hold the turn; returns how many were released (senpi#3650). */
|
|
57
|
+
releaseHeldExtensionHandlers(): number;
|
|
56
58
|
/** True while a provider retry is in flight; the engine is actively recovering the turn. */
|
|
57
59
|
readonly isRetryInFlight: boolean;
|
|
58
60
|
readonly isIdle: boolean;
|
|
@@ -153,18 +153,25 @@ export class AgentDeadlineTracker {
|
|
|
153
153
|
}
|
|
154
154
|
// The abort signal reaches every running tool (the bash tool kills its process tree on it), and the
|
|
155
155
|
// normal settle path emits agent_idle once the run ends. A turn still running after the grace is held by
|
|
156
|
-
// something no abort can end
|
|
156
|
+
// something no abort can end, usually an extension handler that never returns: the host stops waiting on
|
|
157
|
+
// those handlers and gives the settle one more grace. Only a turn still running after that is reported.
|
|
157
158
|
let abortError;
|
|
159
|
+
const abortStartedAt = this.now();
|
|
158
160
|
const aborting = this.session.abortTurnForDeadline().catch((error) => {
|
|
159
161
|
abortError = error instanceof Error ? error.message : String(error);
|
|
160
162
|
});
|
|
161
|
-
|
|
163
|
+
let settled = await within(aborting, TURN_DEADLINE_GRACE_MS);
|
|
164
|
+
if (!settled) {
|
|
165
|
+
// The abort path may already have released the waits (its own grace); either way the release gets a grace.
|
|
166
|
+
this.session.releaseHeldExtensionHandlers();
|
|
167
|
+
settled = await within(aborting, TURN_DEADLINE_GRACE_MS);
|
|
168
|
+
}
|
|
162
169
|
if (abortError !== undefined)
|
|
163
170
|
this.session._warnDeadline("turn_deadline_abort_failed", abortError);
|
|
164
171
|
if (settled && abortError === undefined)
|
|
165
172
|
return;
|
|
166
173
|
if (!this.session.isIdle)
|
|
167
|
-
this.session._warnDeadline("turn_deadline_unsettled", `turn still running ${
|
|
174
|
+
this.session._warnDeadline("turn_deadline_unsettled", `turn still running ${this.now() - abortStartedAt} ms after the ${kind} deadline abort`);
|
|
168
175
|
}
|
|
169
176
|
}
|
|
170
177
|
export function buildDeadlineStopNotice(deadline) {
|
|
@@ -25,6 +25,13 @@ export declare function renderQuestionLine(question: Question): string;
|
|
|
25
25
|
export declare function renderOptionsLine(question: Question, remaining: number): string;
|
|
26
26
|
/** Hint naming every way into the pending question; the shortcut segment follows the effective binding. */
|
|
27
27
|
export declare function renderAnswerHint(): string;
|
|
28
|
+
/**
|
|
29
|
+
* The modifier that makes a drag select text while the app owns the mouse: Option on iTerm2 and
|
|
30
|
+
* Terminal.app, Option on macOS VS Code only with `terminal.integrated.macOptionClickForcesSelection`,
|
|
31
|
+
* Shift elsewhere (Windows Terminal, Ghostty, WezTerm, kitty, VS Code on Windows/Linux). Inside tmux
|
|
32
|
+
* the outer terminal decides and cannot be detected.
|
|
33
|
+
*/
|
|
34
|
+
export declare function selectionBypassHint(termProgram?: string, platform?: NodeJS.Platform): string;
|
|
28
35
|
export interface AskUserAsyncWidgetOptions {
|
|
29
36
|
request: QuestionRequest;
|
|
30
37
|
draft: QuestionDraft;
|
|
@@ -87,6 +87,21 @@ export function renderAnswerHint() {
|
|
|
87
87
|
theme.fg("muted", "or just type your reply"),
|
|
88
88
|
].join(theme.fg("muted", " · "));
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The modifier that makes a drag select text while the app owns the mouse: Option on iTerm2 and
|
|
92
|
+
* Terminal.app, Option on macOS VS Code only with `terminal.integrated.macOptionClickForcesSelection`,
|
|
93
|
+
* Shift elsewhere (Windows Terminal, Ghostty, WezTerm, kitty, VS Code on Windows/Linux). Inside tmux
|
|
94
|
+
* the outer terminal decides and cannot be detected.
|
|
95
|
+
*/
|
|
96
|
+
export function selectionBypassHint(termProgram = process.env.TERM_PROGRAM ?? "", platform = process.platform) {
|
|
97
|
+
if (termProgram === "iTerm.app" || termProgram === "Apple_Terminal")
|
|
98
|
+
return "option+drag";
|
|
99
|
+
if (termProgram === "vscode" && platform === "darwin")
|
|
100
|
+
return "option+drag (with terminal.integrated.macOptionClickForcesSelection)";
|
|
101
|
+
if (termProgram === "tmux")
|
|
102
|
+
return "shift+drag (option+drag on iTerm2/Terminal.app)";
|
|
103
|
+
return "shift+drag";
|
|
104
|
+
}
|
|
90
105
|
export class AskUserAsyncWidget {
|
|
91
106
|
constructor(options) {
|
|
92
107
|
this.countdownLabel = "";
|
|
@@ -157,12 +172,11 @@ export class AskUserAsyncWidget {
|
|
|
157
172
|
}
|
|
158
173
|
if (this.pendingCount > 1)
|
|
159
174
|
addAction(theme.fg("muted", `▸ +${this.pendingCount - 1} more · ${keyText("app.question.next")} next question`), "next");
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
lines.push(...new Text(theme.fg("muted",
|
|
165
|
-
}
|
|
175
|
+
const capture = this.options.mouseCaptureActive ? (this.options.tui?.mouseCaptureState() ?? "armed") : "off";
|
|
176
|
+
if (capture === "armed")
|
|
177
|
+
lines.push(...new Text(theme.fg("muted", `click an option · ${selectionBypassHint()} to select text`), 0, 0).render(width));
|
|
178
|
+
else if (capture === "yielded")
|
|
179
|
+
lines.push(...new Text(theme.fg("muted", "wheel scrolls · press any key to click options again"), 0, 0).render(width));
|
|
166
180
|
lines.push(...new Text(renderAnswerHint(), 0, 0).render(width));
|
|
167
181
|
return lines;
|
|
168
182
|
}
|
|
@@ -836,6 +836,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
836
836
|
case "clear_queue": {
|
|
837
837
|
ClientAdmissions.clearQueuedFor(session);
|
|
838
838
|
const cleared = session.clearQueue({ abortWillFollow: command.abortWillFollow ?? false });
|
|
839
|
+
await session.whenQueueClearRecorded();
|
|
839
840
|
return success(id, "clear_queue", {
|
|
840
841
|
steering: cleared.steering,
|
|
841
842
|
followUp: cleared.followUp,
|
|
@@ -888,6 +889,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
888
889
|
return success(id, "get_state", buildRpcSessionState(session, lastAbortSource));
|
|
889
890
|
case "get_effect_status":
|
|
890
891
|
return runGetEffectStatus(effectContext(), command);
|
|
892
|
+
case "get_pending_inputs":
|
|
893
|
+
return success(id, "get_pending_inputs", await session.getPendingInputs(command.knownClientMessageIds));
|
|
891
894
|
// =================================================================
|
|
892
895
|
// Model
|
|
893
896
|
// =================================================================
|
|
@@ -150,10 +150,9 @@ export async function runQueuedInputChange(ctx, command) {
|
|
|
150
150
|
});
|
|
151
151
|
const clientMessageId = ctx.session.getQueuedInputs().find((input) => input.queuedId === queuedId)?.clientMessageId;
|
|
152
152
|
const expected = command.expectedRevision === undefined ? undefined : { expectedRevision: command.expectedRevision };
|
|
153
|
-
const status = command.type === "dequeue_message"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// Read in the same synchronous step as the change: the revision the answer reports is the one it made or met.
|
|
153
|
+
const status = await ctx.session.changeQueuedInput(queuedId, command.type === "dequeue_message" ? undefined : command.text, { ...expected, ...(effectId === undefined ? {} : { effectId }) });
|
|
154
|
+
// Read right after the change, before anything else on this command: the revision the answer reports is the one
|
|
155
|
+
// it made or met.
|
|
157
156
|
const current = ctx.session.getQueuedInputs().find((input) => input.queuedId === queuedId);
|
|
158
157
|
// A durable input is restored from its admission record on reopen: that record must carry the edit too.
|
|
159
158
|
if (status === "edited" && clientMessageId !== undefined && command.type === "edit_queued_message")
|
|
@@ -86,6 +86,11 @@ export type RpcEffectStatusAnswer = Extract<RpcResponse, {
|
|
|
86
86
|
command: "get_effect_status";
|
|
87
87
|
success: true;
|
|
88
88
|
}>["data"];
|
|
89
|
+
/** `get_pending_inputs`: pending inputs in durable custody, and an outcome for each known id. */
|
|
90
|
+
export type RpcPendingInputsAnswer = Extract<RpcResponse, {
|
|
91
|
+
command: "get_pending_inputs";
|
|
92
|
+
success: true;
|
|
93
|
+
}>["data"];
|
|
89
94
|
export declare class RpcClientOpenInFlightError extends Error {
|
|
90
95
|
readonly code: "open_session_in_flight";
|
|
91
96
|
constructor();
|
|
@@ -251,6 +256,11 @@ export declare class RpcClient {
|
|
|
251
256
|
getEffectStatus(effectId: string, options?: {
|
|
252
257
|
readonly sessionPath?: string;
|
|
253
258
|
}): Promise<RpcEffectStatusAnswer>;
|
|
259
|
+
/**
|
|
260
|
+
* The pending inputs in durable custody (queued, sending, failed) and an explicit outcome for each id in
|
|
261
|
+
* `knownClientMessageIds` (at most 64), consistent through `throughCursor`.
|
|
262
|
+
*/
|
|
263
|
+
getPendingInputs(knownClientMessageIds?: readonly string[]): Promise<RpcPendingInputsAnswer>;
|
|
254
264
|
/** Abort with host refusals and transport loss surfaced to machine callers. */
|
|
255
265
|
abortStrict(): Promise<void>;
|
|
256
266
|
abortCompaction(): Promise<void>;
|
|
@@ -480,6 +480,17 @@ export class RpcClient {
|
|
|
480
480
|
};
|
|
481
481
|
return this.getData(await this.send(command, options.sessionPath === undefined));
|
|
482
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* The pending inputs in durable custody (queued, sending, failed) and an explicit outcome for each id in
|
|
485
|
+
* `knownClientMessageIds` (at most 64), consistent through `throughCursor`.
|
|
486
|
+
*/
|
|
487
|
+
async getPendingInputs(knownClientMessageIds) {
|
|
488
|
+
const command = {
|
|
489
|
+
type: "get_pending_inputs",
|
|
490
|
+
...(knownClientMessageIds === undefined ? {} : { knownClientMessageIds: [...knownClientMessageIds] }),
|
|
491
|
+
};
|
|
492
|
+
return this.getData(await this.send(command));
|
|
493
|
+
}
|
|
483
494
|
/** Abort with host refusals and transport loss surfaced to machine callers. */
|
|
484
495
|
async abortStrict() {
|
|
485
496
|
this.getData(await this.send({ type: "abort" }));
|