@code-yeongyu/senpi 2026.10.16-2 → 2026.10.16-3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/bundle/chunks/chunk-74YFRVGQ.js +2 -0
- package/dist/bundle/chunks/{chunk-KRAABTZY.js → chunk-AVALXFDW.js} +2 -2
- package/dist/bundle/chunks/{chunk-V5JFWWGJ.js → chunk-FMYLLJBQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-VY43SSYL.js → chunk-HJPX7PBL.js} +30 -27
- package/dist/bundle/chunks/{chunk-QQUHMCOM.js → chunk-I5ZJEXTM.js} +1 -1
- package/dist/bundle/chunks/chunk-K2EVQGGT.js +31 -0
- package/dist/bundle/chunks/chunk-WL4Y6XN6.js +18 -0
- package/dist/bundle/chunks/{chunk-Q7RD3NOC.js → chunk-X5VCHHXR.js} +1 -1
- package/dist/bundle/chunks/{cli-main-JDL52ADK.js → cli-main-JVPFJGP5.js} +1 -1
- package/dist/bundle/chunks/google-generative-ai-AI6PAUPA.js +2 -0
- package/dist/bundle/chunks/google-vertex-USNRIFF7.js +2 -0
- package/dist/bundle/chunks/google-vertex-adc-JZURL4XM.js +2 -0
- package/dist/bundle/chunks/{interactive-mode-7IM7YJYH.js → interactive-mode-BROZRY34.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-3MEEUMYU.js → multi-session-host-SMQR5SCJ.js} +2 -2
- package/dist/bundle/chunks/{openai-codex-responses-T26WW2RE.js → openai-codex-responses-NV27Q6VV.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-UCHXVBQ2.js → package-manager-cli-2QFJY2AS.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-D4BJKWFL.js → rpc-mode-Z45T3NZO.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-NBSRSAJS.js → session-control-endpoint-H7FFWF2A.js} +1 -1
- package/dist/bundle/chunks/{session-picker-6TSBGWOA.js → session-picker-2O2BNBVE.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +36 -33
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session.d.ts +16 -0
- package/dist/core/agent-session.js +46 -2
- package/dist/core/dynamic-prompt/intent-gate.d.ts +3 -0
- package/dist/core/dynamic-prompt/intent-gate.js +4 -3
- package/dist/core/dynamic-prompt/policies.d.ts +4 -1
- package/dist/core/dynamic-prompt/policies.js +3 -2
- package/dist/core/dynamic-prompt/working-task.d.ts +4 -1
- package/dist/core/dynamic-prompt/working-task.js +3 -2
- package/dist/core/dynamic-prompt/workstation.d.ts +4 -3
- package/dist/core/dynamic-prompt/workstation.js +1 -0
- package/dist/core/extensions/builtin/goal/index.js +18 -8
- package/dist/core/extensions/builtin/goal/monitor-continuation.js +7 -0
- package/dist/core/extensions/builtin/goal/reload-reengagement.js +5 -0
- package/dist/core/extensions/builtin/index.js +3 -0
- package/dist/core/extensions/builtin/prompt-preset/contract.js +6 -1
- package/dist/core/extensions/builtin/prompt-preset/gemini-4-argon.d.ts +7 -0
- package/dist/core/extensions/builtin/prompt-preset/gemini-4-argon.js +83 -0
- package/dist/core/extensions/builtin/prompt-preset/matchers.d.ts +2 -0
- package/dist/core/extensions/builtin/prompt-preset/matchers.js +12 -0
- package/dist/core/extensions/builtin/prompt-preset/presets.js +3 -0
- package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
- package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
- package/dist/core/extensions/builtin/vertex-model-garden/auth.d.ts +16 -0
- package/dist/core/extensions/builtin/vertex-model-garden/auth.js +19 -0
- package/dist/core/extensions/builtin/vertex-model-garden/index.d.ts +6 -0
- package/dist/core/extensions/builtin/vertex-model-garden/index.js +27 -0
- package/dist/core/extensions/builtin/vertex-model-garden/models.d.ts +3 -0
- package/dist/core/extensions/builtin/vertex-model-garden/models.js +34 -0
- package/dist/core/extensions/builtin/vertex-model-garden/transport.d.ts +14 -0
- package/dist/core/extensions/builtin/vertex-model-garden/transport.js +35 -0
- package/dist/core/extensions/types.d.ts +32 -1
- package/dist/core/provider-api-key-auth.d.ts +9 -1
- package/dist/core/provider-api-key-auth.js +34 -0
- package/dist/core/provider-composer.d.ts +23 -0
- package/dist/core/settings-manager.d.ts +4 -0
- package/dist/core/settings-manager.js +6 -0
- package/dist/core/settings-shapes.d.ts +4 -0
- package/dist/core/turn-deadline.d.ts +92 -0
- package/dist/core/turn-deadline.js +179 -0
- package/dist/modes/rpc/client-admission-record.d.ts +1 -0
- package/dist/modes/rpc/client-admissions.d.ts +6 -3
- package/dist/modes/rpc/client-admissions.js +9 -4
- package/dist/modes/rpc/connection-handler.js +8 -3
- package/dist/modes/rpc/effect-commands.d.ts +1 -1
- package/dist/modes/rpc/effect-commands.js +2 -2
- package/dist/modes/rpc/effect-outcome-copy.d.ts +4 -4
- package/dist/modes/rpc/effect-outcome-copy.js +4 -4
- package/dist/modes/rpc/effect-record.d.ts +2 -0
- package/dist/modes/rpc/effect-record.js +2 -0
- package/dist/modes/rpc/effect-status.js +1 -1
- package/dist/modes/rpc/session-unload.js +4 -1
- package/docs/rpc.md +4 -1
- package/docs/settings.md +3 -1
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-7ASXJ4AD.js +0 -47
- package/dist/bundle/chunks/google-generative-ai-YNIOL5FT.js +0 -2
- package/dist/bundle/chunks/google-vertex-CF6AYVXL.js +0 -2
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export declare const TURN_DEADLINE_ENTRY_TYPE = "deadline-stop";
|
|
2
|
+
export interface TurnDeadlineSettings {
|
|
3
|
+
readonly inactivityMs: number;
|
|
4
|
+
readonly maxRuntimeMs: number;
|
|
5
|
+
}
|
|
6
|
+
/** Milliseconds the abort is given to terminalize before the lease forces settlement. */
|
|
7
|
+
export declare const TURN_DEADLINE_GRACE_MS = 10000;
|
|
8
|
+
export declare const DEFAULT_TURN_INACTIVITY_MS = 3600000;
|
|
9
|
+
export declare const MIN_TURN_INACTIVITY_MS = 60000;
|
|
10
|
+
export declare const DEFAULT_TURN_MAX_RUNTIME_MS = 21600000;
|
|
11
|
+
export type TurnDeadlineKind = "inactivity" | "max_runtime";
|
|
12
|
+
export interface TurnDeadline {
|
|
13
|
+
readonly kind: TurnDeadlineKind;
|
|
14
|
+
readonly limitMs: number;
|
|
15
|
+
readonly elapsedMs: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The recorded form of a deadline stop, durable on the session branch so a config reload
|
|
19
|
+
* or session reopen keeps suppressing goal continuations until admitted user input.
|
|
20
|
+
*/
|
|
21
|
+
export interface DeadlineStopData {
|
|
22
|
+
readonly kind: TurnDeadlineKind;
|
|
23
|
+
readonly limitMs: number;
|
|
24
|
+
readonly elapsedMs: number;
|
|
25
|
+
readonly at: number;
|
|
26
|
+
}
|
|
27
|
+
interface DeadlineStopEntry {
|
|
28
|
+
readonly type: string;
|
|
29
|
+
readonly customType?: string;
|
|
30
|
+
readonly data?: unknown;
|
|
31
|
+
readonly message?: {
|
|
32
|
+
readonly role?: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The latest deadline stop on the branch, or undefined when none is pending. A
|
|
37
|
+
* deadline-stopped session suppresses every automatic continuation until admitted user
|
|
38
|
+
* input starts a new turn; the entry is read fresh so the hold survives reload and reopen.
|
|
39
|
+
* Only a user message releases the hold: the aborted assistant message of the stopped
|
|
40
|
+
* turn is persisted after the stop entry and must not clear it.
|
|
41
|
+
*/
|
|
42
|
+
export declare function deadlineStopPendingSinceLastTurn(entries: readonly DeadlineStopEntry[]): DeadlineStopData | undefined;
|
|
43
|
+
export declare function resolveTurnDeadlineSettings(configured: {
|
|
44
|
+
readonly turn_inactivity_ms?: unknown;
|
|
45
|
+
readonly turn_max_runtime_ms?: unknown;
|
|
46
|
+
}): TurnDeadlineSettings;
|
|
47
|
+
/**
|
|
48
|
+
* The narrow AgentSession surface the tracker needs, declared here so the tracker lives
|
|
49
|
+
* with the rest of the deadline contract instead of inside agent-session.ts.
|
|
50
|
+
*/
|
|
51
|
+
export type TurnDeadlineWarning = "turn_deadline_record_failed" | "turn_deadline_abort_failed" | "turn_deadline_unsettled" | "turn_deadline_failed";
|
|
52
|
+
export interface DeadlineSessionOwner {
|
|
53
|
+
_recordDeadlineStop(deadline: TurnDeadline): void;
|
|
54
|
+
/** End the turn through the normal system abort (tools see the abort signal); resolves once the session is idle. */
|
|
55
|
+
abortTurnForDeadline(): Promise<void>;
|
|
56
|
+
/** True while a provider retry is in flight; the engine is actively recovering the turn. */
|
|
57
|
+
readonly isRetryInFlight: boolean;
|
|
58
|
+
readonly isIdle: boolean;
|
|
59
|
+
/** A deadline stop that could not complete (the entry write failed, or the turn outlived the grace). */
|
|
60
|
+
_warnDeadline(event: TurnDeadlineWarning, detail: string): void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Owns the inactivity lease and the hard max-runtime cap for one turn. Armed at prompt
|
|
64
|
+
* accept, renewed only by tool execution progress and admitted user input, never by text
|
|
65
|
+
* deltas. On expiry it records the durable stop entry and ends the run through the normal
|
|
66
|
+
* system-abort path. This is the single implementation of senpi#3227 for every surface.
|
|
67
|
+
*/
|
|
68
|
+
export declare class AgentDeadlineTracker {
|
|
69
|
+
private inactivityTimer;
|
|
70
|
+
private maxRuntimeTimer;
|
|
71
|
+
private deadlineValue;
|
|
72
|
+
private startedAtMs;
|
|
73
|
+
private readonly session;
|
|
74
|
+
private readonly settings;
|
|
75
|
+
private readonly now;
|
|
76
|
+
get current(): TurnDeadline | undefined;
|
|
77
|
+
constructor(session: DeadlineSessionOwner, settings: () => TurnDeadlineSettings, now?: () => number);
|
|
78
|
+
arm(): void;
|
|
79
|
+
renew(): void;
|
|
80
|
+
disarm(): void;
|
|
81
|
+
/**
|
|
82
|
+
* Admitted user input releases the deadline-stopped hold. Input steered into a live run counts as
|
|
83
|
+
* activity (the inactivity lease restarts) but never resets the max-runtime clock of that run.
|
|
84
|
+
*/
|
|
85
|
+
clearHold(): void;
|
|
86
|
+
private clearTimers;
|
|
87
|
+
private fire;
|
|
88
|
+
private expire;
|
|
89
|
+
}
|
|
90
|
+
export declare function buildDeadlineStopNotice(deadline: TurnDeadline): string;
|
|
91
|
+
export {};
|
|
92
|
+
//# sourceMappingURL=turn-deadline.d.ts.map
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// The session-level turn deadline contract: how long a turn may run with no tool
|
|
2
|
+
// progress, and the absolute cap on one turn's runtime. One implementation for every
|
|
3
|
+
// surface - TUI, RPC, and host sessions all run through AgentSession.
|
|
4
|
+
export const TURN_DEADLINE_ENTRY_TYPE = "deadline-stop";
|
|
5
|
+
/** Milliseconds the abort is given to terminalize before the lease forces settlement. */
|
|
6
|
+
export const TURN_DEADLINE_GRACE_MS = 10_000;
|
|
7
|
+
export const DEFAULT_TURN_INACTIVITY_MS = 3_600_000;
|
|
8
|
+
export const MIN_TURN_INACTIVITY_MS = 60_000;
|
|
9
|
+
export const DEFAULT_TURN_MAX_RUNTIME_MS = 21_600_000;
|
|
10
|
+
function isDeadlineStopData(data) {
|
|
11
|
+
if (data === null || typeof data !== "object")
|
|
12
|
+
return false;
|
|
13
|
+
const candidate = data;
|
|
14
|
+
return ((candidate.kind === "inactivity" || candidate.kind === "max_runtime") &&
|
|
15
|
+
typeof candidate.limitMs === "number" &&
|
|
16
|
+
typeof candidate.elapsedMs === "number");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The latest deadline stop on the branch, or undefined when none is pending. A
|
|
20
|
+
* deadline-stopped session suppresses every automatic continuation until admitted user
|
|
21
|
+
* input starts a new turn; the entry is read fresh so the hold survives reload and reopen.
|
|
22
|
+
* Only a user message releases the hold: the aborted assistant message of the stopped
|
|
23
|
+
* turn is persisted after the stop entry and must not clear it.
|
|
24
|
+
*/
|
|
25
|
+
export function deadlineStopPendingSinceLastTurn(entries) {
|
|
26
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
27
|
+
const entry = entries[index];
|
|
28
|
+
if (entry.type === "message" && entry.message?.role === "user")
|
|
29
|
+
return undefined;
|
|
30
|
+
if (entry.type === "custom" && entry.customType === TURN_DEADLINE_ENTRY_TYPE && isDeadlineStopData(entry.data)) {
|
|
31
|
+
return entry.data;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
export function resolveTurnDeadlineSettings(configured) {
|
|
37
|
+
const milliseconds = (value, fallback, minimum) => {
|
|
38
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
39
|
+
return fallback;
|
|
40
|
+
if (value === 0)
|
|
41
|
+
return 0;
|
|
42
|
+
return Math.max(minimum, Math.floor(value));
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
inactivityMs: milliseconds(configured.turn_inactivity_ms, DEFAULT_TURN_INACTIVITY_MS, MIN_TURN_INACTIVITY_MS),
|
|
46
|
+
maxRuntimeMs: milliseconds(configured.turn_max_runtime_ms, DEFAULT_TURN_MAX_RUNTIME_MS, 1),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Resolves true when `work` settles within `ms`, false when the time runs out first. */
|
|
50
|
+
async function within(work, ms) {
|
|
51
|
+
let timer;
|
|
52
|
+
const expired = new Promise((resolve) => {
|
|
53
|
+
timer = setTimeout(() => resolve(false), ms);
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
return await Promise.race([
|
|
57
|
+
work.then(() => true, () => false),
|
|
58
|
+
expired,
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Owns the inactivity lease and the hard max-runtime cap for one turn. Armed at prompt
|
|
67
|
+
* accept, renewed only by tool execution progress and admitted user input, never by text
|
|
68
|
+
* deltas. On expiry it records the durable stop entry and ends the run through the normal
|
|
69
|
+
* system-abort path. This is the single implementation of senpi#3227 for every surface.
|
|
70
|
+
*/
|
|
71
|
+
export class AgentDeadlineTracker {
|
|
72
|
+
get current() {
|
|
73
|
+
return this.deadlineValue;
|
|
74
|
+
}
|
|
75
|
+
constructor(session, settings, now = Date.now) {
|
|
76
|
+
this.startedAtMs = 0;
|
|
77
|
+
this.session = session;
|
|
78
|
+
this.settings = settings;
|
|
79
|
+
this.now = now;
|
|
80
|
+
}
|
|
81
|
+
arm() {
|
|
82
|
+
this.clearTimers();
|
|
83
|
+
this.deadlineValue = undefined;
|
|
84
|
+
this.startedAtMs = this.now();
|
|
85
|
+
const { inactivityMs, maxRuntimeMs } = this.settings();
|
|
86
|
+
if (inactivityMs > 0)
|
|
87
|
+
this.inactivityTimer = setTimeout(() => this.fire("inactivity"), inactivityMs);
|
|
88
|
+
if (maxRuntimeMs > 0)
|
|
89
|
+
this.maxRuntimeTimer = setTimeout(() => this.fire("max_runtime"), maxRuntimeMs);
|
|
90
|
+
}
|
|
91
|
+
renew() {
|
|
92
|
+
if (this.deadlineValue !== undefined || this.startedAtMs === 0)
|
|
93
|
+
return;
|
|
94
|
+
const { inactivityMs } = this.settings();
|
|
95
|
+
if (inactivityMs <= 0)
|
|
96
|
+
return;
|
|
97
|
+
if (this.inactivityTimer !== undefined)
|
|
98
|
+
clearTimeout(this.inactivityTimer);
|
|
99
|
+
this.inactivityTimer = setTimeout(() => this.fire("inactivity"), inactivityMs);
|
|
100
|
+
}
|
|
101
|
+
disarm() {
|
|
102
|
+
this.clearTimers();
|
|
103
|
+
this.startedAtMs = 0;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Admitted user input releases the deadline-stopped hold. Input steered into a live run counts as
|
|
107
|
+
* activity (the inactivity lease restarts) but never resets the max-runtime clock of that run.
|
|
108
|
+
*/
|
|
109
|
+
clearHold() {
|
|
110
|
+
// A stopped run that has not settled keeps its deadline (its agent_end reports it); the next arm resets it.
|
|
111
|
+
if (this.deadlineValue === undefined) {
|
|
112
|
+
this.renew();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Once the stopped run has settled, its deadline is history: input clears it.
|
|
116
|
+
if (this.session.isIdle)
|
|
117
|
+
this.deadlineValue = undefined;
|
|
118
|
+
}
|
|
119
|
+
clearTimers() {
|
|
120
|
+
if (this.inactivityTimer !== undefined)
|
|
121
|
+
clearTimeout(this.inactivityTimer);
|
|
122
|
+
if (this.maxRuntimeTimer !== undefined)
|
|
123
|
+
clearTimeout(this.maxRuntimeTimer);
|
|
124
|
+
this.inactivityTimer = undefined;
|
|
125
|
+
this.maxRuntimeTimer = undefined;
|
|
126
|
+
}
|
|
127
|
+
fire(kind) {
|
|
128
|
+
this.expire(kind).catch((error) => this.session._warnDeadline("turn_deadline_failed", error instanceof Error ? error.message : String(error)));
|
|
129
|
+
}
|
|
130
|
+
async expire(kind) {
|
|
131
|
+
if (this.deadlineValue !== undefined || this.startedAtMs === 0)
|
|
132
|
+
return;
|
|
133
|
+
const { inactivityMs, maxRuntimeMs } = this.settings();
|
|
134
|
+
const limitMs = kind === "inactivity" ? inactivityMs : maxRuntimeMs;
|
|
135
|
+
if (limitMs <= 0)
|
|
136
|
+
return;
|
|
137
|
+
// A provider retry in flight is the engine recovering the turn: it renews the inactivity lease.
|
|
138
|
+
// The max-runtime cap is absolute and applies through retries.
|
|
139
|
+
if (kind === "inactivity" && this.session.isRetryInFlight) {
|
|
140
|
+
this.inactivityTimer = undefined;
|
|
141
|
+
this.renew();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.clearTimers();
|
|
145
|
+
const deadline = { kind, limitMs, elapsedMs: this.now() - this.startedAtMs };
|
|
146
|
+
this.deadlineValue = deadline;
|
|
147
|
+
try {
|
|
148
|
+
this.session._recordDeadlineStop(deadline);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
// The stop still happens; only its durable hold entry is missing.
|
|
152
|
+
this.session._warnDeadline("turn_deadline_record_failed", error instanceof Error ? error.message : String(error));
|
|
153
|
+
}
|
|
154
|
+
// The abort signal reaches every running tool (the bash tool kills its process tree on it), and the
|
|
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 (an extension handler that never returns): it is reported, never faked idle.
|
|
157
|
+
let abortError;
|
|
158
|
+
const aborting = this.session.abortTurnForDeadline().catch((error) => {
|
|
159
|
+
abortError = error instanceof Error ? error.message : String(error);
|
|
160
|
+
});
|
|
161
|
+
const settled = await within(aborting, TURN_DEADLINE_GRACE_MS);
|
|
162
|
+
if (abortError !== undefined)
|
|
163
|
+
this.session._warnDeadline("turn_deadline_abort_failed", abortError);
|
|
164
|
+
if (settled && abortError === undefined)
|
|
165
|
+
return;
|
|
166
|
+
if (!this.session.isIdle)
|
|
167
|
+
this.session._warnDeadline("turn_deadline_unsettled", `turn still running ${TURN_DEADLINE_GRACE_MS} ms after the ${kind} deadline abort`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export function buildDeadlineStopNotice(deadline) {
|
|
171
|
+
const limit = deadline.limitMs < 60_000
|
|
172
|
+
? `${Math.max(1, Math.round(deadline.limitMs / 1000))} s`
|
|
173
|
+
: `${Math.round(deadline.limitMs / 60_000)} min`;
|
|
174
|
+
const reason = deadline.kind === "inactivity"
|
|
175
|
+
? `no tool progress for ${limit} (turn_inactivity_ms)`
|
|
176
|
+
: `turn hit its maximum runtime of ${limit} (turn_max_runtime_ms)`;
|
|
177
|
+
return `turn stopped: ${reason}. Action: send a message to continue, or raise the limit in settings`;
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=turn-deadline.js.map
|
|
@@ -69,6 +69,7 @@ export declare const clientAdmissionRecordSchema: z.ZodDiscriminatedUnion<[z.Zod
|
|
|
69
69
|
kind: z.ZodEnum<{
|
|
70
70
|
aborted: "aborted";
|
|
71
71
|
already_delivered: "already_delivered";
|
|
72
|
+
cleared: "cleared";
|
|
72
73
|
cleared_by_abort: "cleared_by_abort";
|
|
73
74
|
completed: "completed";
|
|
74
75
|
context_budget: "context_budget";
|
|
@@ -31,6 +31,8 @@ export declare class ClientAdmissions {
|
|
|
31
31
|
hasIdentities: boolean;
|
|
32
32
|
readonly ready: Promise<void>;
|
|
33
33
|
private constructor();
|
|
34
|
+
/** Mark queued input cleared on a session whose ledger exists; never builds one (that would restore queued input). */
|
|
35
|
+
static clearQueuedFor(session: AgentSession): void;
|
|
34
36
|
/**
|
|
35
37
|
* Whenever `runtime` replaces its session, give the outgoing session's still-queued inputs a
|
|
36
38
|
* `dropped_by_replace` receipt. Only a ledger that already exists is touched: building one now would
|
|
@@ -60,10 +62,11 @@ export declare class ClientAdmissions {
|
|
|
60
62
|
*/
|
|
61
63
|
forgetAbort(abort: ClientAbort): void;
|
|
62
64
|
/**
|
|
63
|
-
*
|
|
64
|
-
* `
|
|
65
|
+
* The queues are about to be drained (`clear_queue`, `abort` or `unload_session` with `clearQueue`, a session
|
|
66
|
+
* release is not one: its queued deliveries stay unwritten so the sender redelivers them): give every still-queued input an honest `cleared` receipt first, so the drain's `queue_update` cannot
|
|
67
|
+
* read it as delivered (senpi#3412).
|
|
65
68
|
*/
|
|
66
|
-
|
|
69
|
+
clearQueued(): void;
|
|
67
70
|
/**
|
|
68
71
|
* The session is being replaced (`new_session`, `switch_session`, `fork`) and its queue is about to be
|
|
69
72
|
* discarded with it: give every still-queued input an honest terminal receipt in its own transcript, so
|
|
@@ -84,6 +84,10 @@ export class ClientAdmissions {
|
|
|
84
84
|
// Publish this ledger in the WeakMap before restoration emits queue events.
|
|
85
85
|
this.ready = Promise.resolve().then(() => this.restore());
|
|
86
86
|
}
|
|
87
|
+
/** Mark queued input cleared on a session whose ledger exists; never builds one (that would restore queued input). */
|
|
88
|
+
static clearQueuedFor(session) {
|
|
89
|
+
sessions.get(session)?.clearQueued();
|
|
90
|
+
}
|
|
87
91
|
/**
|
|
88
92
|
* Whenever `runtime` replaces its session, give the outgoing session's still-queued inputs a
|
|
89
93
|
* `dropped_by_replace` receipt. Only a ledger that already exists is touched: building one now would
|
|
@@ -142,10 +146,11 @@ export class ClientAdmissions {
|
|
|
142
146
|
this.lastAbort = undefined;
|
|
143
147
|
}
|
|
144
148
|
/**
|
|
145
|
-
*
|
|
146
|
-
* `
|
|
149
|
+
* The queues are about to be drained (`clear_queue`, `abort` or `unload_session` with `clearQueue`, a session
|
|
150
|
+
* release is not one: its queued deliveries stay unwritten so the sender redelivers them): give every still-queued input an honest `cleared` receipt first, so the drain's `queue_update` cannot
|
|
151
|
+
* read it as delivered (senpi#3412).
|
|
147
152
|
*/
|
|
148
|
-
|
|
153
|
+
clearQueued() {
|
|
149
154
|
if (this.durableSessionId !== this.session.sessionManager.getSessionId())
|
|
150
155
|
return;
|
|
151
156
|
for (const record of [...this.records.values()]) {
|
|
@@ -155,7 +160,7 @@ export class ClientAdmissions {
|
|
|
155
160
|
...record,
|
|
156
161
|
state: "completed",
|
|
157
162
|
prepared: undefined,
|
|
158
|
-
outcome: EFFECT_OUTCOMES.
|
|
163
|
+
outcome: EFFECT_OUTCOMES.inputCleared,
|
|
159
164
|
});
|
|
160
165
|
}
|
|
161
166
|
}
|
|
@@ -651,10 +651,14 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
651
651
|
const abortSource = session.currentAbortSource;
|
|
652
652
|
if (abortSource !== undefined)
|
|
653
653
|
lastAbortSource = abortSource;
|
|
654
|
+
const deadline = session.currentDeadline;
|
|
654
655
|
const abortedBy = abortSource === undefined ? undefined : ClientAdmissions.forSession(session).abortedBy;
|
|
655
|
-
correlatedEvents.accept(
|
|
656
|
-
|
|
657
|
-
|
|
656
|
+
correlatedEvents.accept({
|
|
657
|
+
...event,
|
|
658
|
+
...(abortSource === undefined ? {} : { aborted: true, abortSource }),
|
|
659
|
+
...(abortedBy === undefined ? {} : { abortedBy }),
|
|
660
|
+
...(deadline === undefined ? {} : { deadline }),
|
|
661
|
+
});
|
|
658
662
|
return;
|
|
659
663
|
}
|
|
660
664
|
if (event.type === "session_abort") {
|
|
@@ -866,6 +870,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
866
870
|
case "edit_queued_message":
|
|
867
871
|
return runQueuedInputChange(effectContext(), command);
|
|
868
872
|
case "clear_queue": {
|
|
873
|
+
ClientAdmissions.clearQueuedFor(session);
|
|
869
874
|
const cleared = session.clearQueue({ abortWillFollow: command.abortWillFollow ?? false });
|
|
870
875
|
return success(id, "clear_queue", {
|
|
871
876
|
steering: cleared.steering,
|
|
@@ -21,7 +21,7 @@ export declare function isEffectBegin(value: EffectBegin | RpcResponse): value i
|
|
|
21
21
|
export declare const ABORT_DEFAULT_GRACE_MS = 10000;
|
|
22
22
|
export declare const ABORT_MAX_GRACE_MS = 60000;
|
|
23
23
|
/**
|
|
24
|
-
* `abort`: with `clearQueue` the queued inputs are drained (each gets a `
|
|
24
|
+
* `abort`: with `clearQueue` the queued inputs are drained (each gets a `cleared` receipt) before
|
|
25
25
|
* the abort; with `awaitIdle` the answer waits until the session is idle or `graceMs` passes. Without
|
|
26
26
|
* either, the abort is acknowledged on dispatch exactly as before.
|
|
27
27
|
*/
|
|
@@ -74,7 +74,7 @@ async function quiesce(session, aborting, graceMs) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
|
-
* `abort`: with `clearQueue` the queued inputs are drained (each gets a `
|
|
77
|
+
* `abort`: with `clearQueue` the queued inputs are drained (each gets a `cleared` receipt) before
|
|
78
78
|
* the abort; with `awaitIdle` the answer waits until the session is idle or `graceMs` passes. Without
|
|
79
79
|
* either, the abort is acknowledged on dispatch exactly as before.
|
|
80
80
|
*/
|
|
@@ -93,7 +93,7 @@ export async function runAbort(ctx, command, dispatch) {
|
|
|
93
93
|
const streaming = ctx.session.isStreaming;
|
|
94
94
|
let cleared = 0;
|
|
95
95
|
if (command.clearQueue === true) {
|
|
96
|
-
admissions.
|
|
96
|
+
admissions.clearQueued();
|
|
97
97
|
const drained = ctx.session.clearQueue({ abortWillFollow: true });
|
|
98
98
|
cleared = drained.steering.length + drained.followUp.length;
|
|
99
99
|
}
|
|
@@ -10,10 +10,10 @@ export declare const EFFECT_OUTCOMES: {
|
|
|
10
10
|
readonly detail: "the session was reopened while this input's turn was running, so that turn never finished";
|
|
11
11
|
readonly action: "read the transcript for what the turn produced, then send the input again with a new clientMessageId";
|
|
12
12
|
};
|
|
13
|
-
readonly
|
|
14
|
-
readonly kind: "
|
|
15
|
-
readonly reason: "
|
|
16
|
-
readonly detail: "
|
|
13
|
+
readonly inputCleared: {
|
|
14
|
+
readonly kind: "cleared";
|
|
15
|
+
readonly reason: "cleared";
|
|
16
|
+
readonly detail: "the queue was cleared (clear_queue, or abort or unload_session with clearQueue) before this queued input was delivered";
|
|
17
17
|
readonly action: "send the input again with a new clientMessageId if it is still needed";
|
|
18
18
|
};
|
|
19
19
|
readonly queuedInputAlreadyDelivered: {
|
|
@@ -9,10 +9,10 @@ export const EFFECT_OUTCOMES = {
|
|
|
9
9
|
detail: "the session was reopened while this input's turn was running, so that turn never finished",
|
|
10
10
|
action: "read the transcript for what the turn produced, then send the input again with a new clientMessageId",
|
|
11
11
|
},
|
|
12
|
-
|
|
13
|
-
kind: "
|
|
14
|
-
reason: "
|
|
15
|
-
detail: "
|
|
12
|
+
inputCleared: {
|
|
13
|
+
kind: "cleared",
|
|
14
|
+
reason: "cleared",
|
|
15
|
+
detail: "the queue was cleared (clear_queue, or abort or unload_session with clearQueue) before this queued input was delivered",
|
|
16
16
|
action: "send the input again with a new clientMessageId if it is still needed",
|
|
17
17
|
},
|
|
18
18
|
queuedInputAlreadyDelivered: {
|
|
@@ -14,6 +14,7 @@ export declare const effectOutcomeSchema: z.ZodObject<{
|
|
|
14
14
|
kind: z.ZodEnum<{
|
|
15
15
|
aborted: "aborted";
|
|
16
16
|
already_delivered: "already_delivered";
|
|
17
|
+
cleared: "cleared";
|
|
17
18
|
cleared_by_abort: "cleared_by_abort";
|
|
18
19
|
completed: "completed";
|
|
19
20
|
context_budget: "context_budget";
|
|
@@ -60,6 +61,7 @@ export declare const rpcEffectRecordSchema: z.ZodObject<{
|
|
|
60
61
|
kind: z.ZodEnum<{
|
|
61
62
|
aborted: "aborted";
|
|
62
63
|
already_delivered: "already_delivered";
|
|
64
|
+
cleared: "cleared";
|
|
63
65
|
cleared_by_abort: "cleared_by_abort";
|
|
64
66
|
completed: "completed";
|
|
65
67
|
context_budget: "context_budget";
|
|
@@ -21,6 +21,8 @@ export const effectOutcomeSchema = z.object({
|
|
|
21
21
|
"unsupported_thinking_level",
|
|
22
22
|
"superseded",
|
|
23
23
|
"context_budget",
|
|
24
|
+
"cleared",
|
|
25
|
+
// 2026.10.16-2 wrote an abort-drained input under this name; it must stay readable.
|
|
24
26
|
"cleared_by_abort",
|
|
25
27
|
"already_delivered",
|
|
26
28
|
"not_found",
|
|
@@ -10,7 +10,7 @@ export function projectAdmission(record, sessionId) {
|
|
|
10
10
|
effectId: record.clientMessageId,
|
|
11
11
|
kind: "input",
|
|
12
12
|
// A dropped or cleared input is stored as `completed` so older hosts never restore it; it never ran.
|
|
13
|
-
state: outcome?.kind === "dropped_by_replace" || outcome?.kind === "cleared_by_abort"
|
|
13
|
+
state: outcome?.kind === "dropped_by_replace" || outcome?.kind === "cleared" || outcome?.kind === "cleared_by_abort"
|
|
14
14
|
? "failed"
|
|
15
15
|
: ADMISSION_STATES[record.state],
|
|
16
16
|
disposition: record.disposition,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ClientAdmissions } from "./client-admissions.js";
|
|
1
2
|
import { beginEffect, effectAnswer, isEffectBegin } from "./effect-commands.js";
|
|
2
3
|
import { EffectLedger } from "./effect-ledger.js";
|
|
3
4
|
import { abortAndSettle } from "./session-release-interrupt.js";
|
|
@@ -49,8 +50,10 @@ async function claimAndUnload(port, command) {
|
|
|
49
50
|
const stillBlocked = blockers(port, command, ready.session, ready.durableSessionId);
|
|
50
51
|
if (stillBlocked.length > 0)
|
|
51
52
|
return fail(command, ready.session, stillBlocked[0]);
|
|
52
|
-
if (command.clearQueue === true)
|
|
53
|
+
if (command.clearQueue === true) {
|
|
54
|
+
ClientAdmissions.clearQueuedFor(ready.session);
|
|
53
55
|
ready.session.clearQueue();
|
|
56
|
+
}
|
|
54
57
|
const effect = command.effectId === undefined
|
|
55
58
|
? undefined
|
|
56
59
|
: EffectLedger.forSession(ready.session).transition(command.effectId, "done", { disposition: "unloaded" });
|
package/docs/rpc.md
CHANGED
|
@@ -2410,7 +2410,10 @@ Quiesce and queue options (both optional, combinable with `effectId`):
|
|
|
2410
2410
|
`{ reason: "invalid_grace_ms", action }`.
|
|
2411
2411
|
- `clearQueue: true` drains the steering and follow-up queues atomically before the abort, emits
|
|
2412
2412
|
`queue_update`, and gives each drained input with a `clientMessageId` a `failed` receipt with outcome
|
|
2413
|
-
`
|
|
2413
|
+
`cleared` (with `reason`, `detail` and `action`). 2026.10.16-2 wrote this outcome as `cleared_by_abort`;
|
|
2414
|
+
receipts stored under that name are still read and reported unchanged, so accept both. The answer's `cleared` counts the drained inputs.
|
|
2415
|
+
`clear_queue` and `unload_session { clearQueue: true }` close drained input with the same `cleared` outcome; a
|
|
2416
|
+
session release does not (its queued deliveries stay unwritten so the sender redelivers them).
|
|
2414
2417
|
It also empties the queue of an idle session.
|
|
2415
2418
|
|
|
2416
2419
|
```json
|
package/docs/settings.md
CHANGED
|
@@ -103,7 +103,7 @@ Permission rules are a confirmation policy, not a sandbox. Senpi, extensions, pa
|
|
|
103
103
|
| `defaultModel` | string | - | Startup model ID (saved with Ctrl+S in `/model`, or edited manually) |
|
|
104
104
|
| `recommendedModels` | string[] | `kimi-k3`, `gpt-6-astra`, `gpt-5.6-sol`, `claude-fable-5-1`, `claude-opus-5-5`, `glm-5.2` | Preferred default model ids in priority order. Built-in thinking levels are kimi-k3/`max`, GPT-6 Astra/`high`, GPT-5.6 Sol/`medium`, claude-fable-5-1/`high`, claude-opus-5-5/`max`, glm-5.2/`max`. Override the list or disable auto-switch with `--no-recommended-models` / `warnings.offRecommendedModel`. |
|
|
105
105
|
| `defaultThinkingLevel` | string | - | Startup thinking level (saved with Ctrl+S in `/thinking`, or edited manually): `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"` |
|
|
106
|
-
| `promptPreset` | string | `"auto"` | Force a system prompt preset: `"auto"`, `"kimi-k2-6"`, `"kimi-k2-7"`, `"kimi-k2-8"`, `"kimi-k3"`, `"glm-5.2"`, `"glm-5.3"`, `"grok-4.5"`, `"grok-4.6"`, `"grok-4.7"`, `"claude-fable-5"`, `"claude-fable-5-1"`, `"claude-opus-5-5"`, `"claude-opus-5"`, `"claude-sonnet-5-5"`, `"claude-haiku-5-5"`, `"claude-opus-4-5"`, `"claude-opus-4-6"`, `"claude-opus-4-7"`, `"claude-opus-4-8"`, `"deepseek-v4-flash"`, `"deepseek-v4-flash-0731"`, `"deepseek-v4-1-flash"`, `"deepseek-v4-pro"`, `"gpt-5"`, `"gpt-5.2"`, `"gpt-5.3-codex"`, `"gpt-5.4"`, `"gpt-5.5"`, `"gpt-5.6"`, or `"gpt-6-astra"` |
|
|
106
|
+
| `promptPreset` | string | `"auto"` | Force a system prompt preset: `"auto"`, `"kimi-k2-6"`, `"kimi-k2-7"`, `"kimi-k2-8"`, `"kimi-k3"`, `"glm-5.2"`, `"glm-5.3"`, `"grok-4.5"`, `"grok-4.6"`, `"grok-4.7"`, `"claude-fable-5"`, `"claude-fable-5-1"`, `"claude-opus-5-5"`, `"claude-opus-5"`, `"claude-sonnet-5-5"`, `"claude-haiku-5-5"`, `"claude-opus-4-5"`, `"claude-opus-4-6"`, `"claude-opus-4-7"`, `"claude-opus-4-8"`, `"deepseek-v4-flash"`, `"deepseek-v4-flash-0731"`, `"deepseek-v4-1-flash"`, `"deepseek-v4-pro"`, `"gemini-4-argon"`, `"gpt-5"`, `"gpt-5.2"`, `"gpt-5.3-codex"`, `"gpt-5.4"`, `"gpt-5.5"`, `"gpt-5.6"`, or `"gpt-6-astra"` |
|
|
107
107
|
| `hideThinkingBlock` | boolean | `false` | Hide thinking blocks in output |
|
|
108
108
|
| `showCacheMissNotices` | boolean | `false` | Show transcript notices for significant prompt-cache misses, compaction or branch-summary usage, and provider recovery diagnostics such as dropped Anthropic thinking blocks |
|
|
109
109
|
| `thinkingBudgets` | object | - | Custom token budgets per thinking level. Anthropic, Google, and Bedrock use these natively. OpenAI-compatible models use them when `compat.thinkingTokenBudgetField` (or `supportsThinkingTokenBudget`) is set. |
|
|
@@ -545,6 +545,8 @@ This is the default and has no setting. Tools may declare `exposure: "eval"` to
|
|
|
545
545
|
|---------|------|---------|-------------|
|
|
546
546
|
| `engineTurns.maxPerUserInput` | number | `150` | Turns the agent may start on its own (stream-rule nudges, goal continuations, other extension follow-ups) after one user message before it pauses; `0` turns the limit off |
|
|
547
547
|
| `engineTurns.maxToolFreePerMinute` | number | `12` | Turns started on its own within 60 s that called no tool before it pauses; `0` turns the limit off |
|
|
548
|
+
| `turn_inactivity_ms` | number | `3600000` | Milliseconds a turn may run with no tool progress before it is stopped as a deadline; renewed only by tool execution events and admitted user input, never by streamed text. Minimum `60000`; `0` disables |
|
|
549
|
+
| `turn_max_runtime_ms` | number | `21600000` | Hard cap on one turn's runtime from prompt accept; `0` disables |
|
|
548
550
|
|
|
549
551
|
When a limit is reached the session pauses with a notice instead of starting another turn; any message you send continues it, because only turns since your last message count.
|
|
550
552
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi",
|
|
3
|
-
"version": "2026.10.16-
|
|
3
|
+
"version": "2026.10.16-3",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@earendil-works/chord": "0.99.1",
|
|
63
|
-
"@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.16-
|
|
64
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.16-
|
|
65
|
-
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.16-
|
|
63
|
+
"@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.16-3",
|
|
64
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.16-3",
|
|
65
|
+
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.16-3",
|
|
66
66
|
"@silvia-odwyer/photon-node": "0.3.4",
|
|
67
67
|
"chalk": "6.0.0",
|
|
68
68
|
"cross-spawn": "7.0.6",
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
"yaml": "2.9.1",
|
|
82
82
|
"@anthropic-ai/claude-agent-sdk": "0.3.296",
|
|
83
83
|
"@anthropic-ai/sdk": "0.127.0",
|
|
84
|
-
"@code-yeongyu/senpi-codemode": "2026.10.16-
|
|
85
|
-
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.16-
|
|
84
|
+
"@code-yeongyu/senpi-codemode": "2026.10.16-3",
|
|
85
|
+
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.16-3",
|
|
86
86
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
87
87
|
"@mozilla/readability": "0.6.0",
|
|
88
88
|
"linkedom": "0.18.13",
|