@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/bundle/chunks/chunk-74YFRVGQ.js +2 -0
  3. package/dist/bundle/chunks/{chunk-KRAABTZY.js → chunk-AVALXFDW.js} +2 -2
  4. package/dist/bundle/chunks/{chunk-V5JFWWGJ.js → chunk-FMYLLJBQ.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-VY43SSYL.js → chunk-HJPX7PBL.js} +30 -27
  6. package/dist/bundle/chunks/{chunk-QQUHMCOM.js → chunk-I5ZJEXTM.js} +1 -1
  7. package/dist/bundle/chunks/chunk-K2EVQGGT.js +31 -0
  8. package/dist/bundle/chunks/chunk-WL4Y6XN6.js +18 -0
  9. package/dist/bundle/chunks/{chunk-Q7RD3NOC.js → chunk-X5VCHHXR.js} +1 -1
  10. package/dist/bundle/chunks/{cli-main-JDL52ADK.js → cli-main-JVPFJGP5.js} +1 -1
  11. package/dist/bundle/chunks/google-generative-ai-AI6PAUPA.js +2 -0
  12. package/dist/bundle/chunks/google-vertex-USNRIFF7.js +2 -0
  13. package/dist/bundle/chunks/google-vertex-adc-JZURL4XM.js +2 -0
  14. package/dist/bundle/chunks/{interactive-mode-7IM7YJYH.js → interactive-mode-BROZRY34.js} +1 -1
  15. package/dist/bundle/chunks/{multi-session-host-3MEEUMYU.js → multi-session-host-SMQR5SCJ.js} +2 -2
  16. package/dist/bundle/chunks/{openai-codex-responses-T26WW2RE.js → openai-codex-responses-NV27Q6VV.js} +1 -1
  17. package/dist/bundle/chunks/{package-manager-cli-UCHXVBQ2.js → package-manager-cli-2QFJY2AS.js} +1 -1
  18. package/dist/bundle/chunks/{rpc-mode-D4BJKWFL.js → rpc-mode-Z45T3NZO.js} +1 -1
  19. package/dist/bundle/chunks/{session-control-endpoint-NBSRSAJS.js → session-control-endpoint-H7FFWF2A.js} +1 -1
  20. package/dist/bundle/chunks/{session-picker-6TSBGWOA.js → session-picker-2O2BNBVE.js} +1 -1
  21. package/dist/bundle/chunks/session-worker.js +36 -33
  22. package/dist/bundle/cli.js +2 -2
  23. package/dist/bundle/index.js +1 -1
  24. package/dist/bundle/rpc-entry.js +1 -1
  25. package/dist/bundle/runtime-manifest.json +1 -1
  26. package/dist/core/agent-session.d.ts +16 -0
  27. package/dist/core/agent-session.js +46 -2
  28. package/dist/core/dynamic-prompt/intent-gate.d.ts +3 -0
  29. package/dist/core/dynamic-prompt/intent-gate.js +4 -3
  30. package/dist/core/dynamic-prompt/policies.d.ts +4 -1
  31. package/dist/core/dynamic-prompt/policies.js +3 -2
  32. package/dist/core/dynamic-prompt/working-task.d.ts +4 -1
  33. package/dist/core/dynamic-prompt/working-task.js +3 -2
  34. package/dist/core/dynamic-prompt/workstation.d.ts +4 -3
  35. package/dist/core/dynamic-prompt/workstation.js +1 -0
  36. package/dist/core/extensions/builtin/goal/index.js +18 -8
  37. package/dist/core/extensions/builtin/goal/monitor-continuation.js +7 -0
  38. package/dist/core/extensions/builtin/goal/reload-reengagement.js +5 -0
  39. package/dist/core/extensions/builtin/index.js +3 -0
  40. package/dist/core/extensions/builtin/prompt-preset/contract.js +6 -1
  41. package/dist/core/extensions/builtin/prompt-preset/gemini-4-argon.d.ts +7 -0
  42. package/dist/core/extensions/builtin/prompt-preset/gemini-4-argon.js +83 -0
  43. package/dist/core/extensions/builtin/prompt-preset/matchers.d.ts +2 -0
  44. package/dist/core/extensions/builtin/prompt-preset/matchers.js +12 -0
  45. package/dist/core/extensions/builtin/prompt-preset/presets.js +3 -0
  46. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
  47. package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
  48. package/dist/core/extensions/builtin/vertex-model-garden/auth.d.ts +16 -0
  49. package/dist/core/extensions/builtin/vertex-model-garden/auth.js +19 -0
  50. package/dist/core/extensions/builtin/vertex-model-garden/index.d.ts +6 -0
  51. package/dist/core/extensions/builtin/vertex-model-garden/index.js +27 -0
  52. package/dist/core/extensions/builtin/vertex-model-garden/models.d.ts +3 -0
  53. package/dist/core/extensions/builtin/vertex-model-garden/models.js +34 -0
  54. package/dist/core/extensions/builtin/vertex-model-garden/transport.d.ts +14 -0
  55. package/dist/core/extensions/builtin/vertex-model-garden/transport.js +35 -0
  56. package/dist/core/extensions/types.d.ts +32 -1
  57. package/dist/core/provider-api-key-auth.d.ts +9 -1
  58. package/dist/core/provider-api-key-auth.js +34 -0
  59. package/dist/core/provider-composer.d.ts +23 -0
  60. package/dist/core/settings-manager.d.ts +4 -0
  61. package/dist/core/settings-manager.js +6 -0
  62. package/dist/core/settings-shapes.d.ts +4 -0
  63. package/dist/core/turn-deadline.d.ts +92 -0
  64. package/dist/core/turn-deadline.js +179 -0
  65. package/dist/modes/rpc/client-admission-record.d.ts +1 -0
  66. package/dist/modes/rpc/client-admissions.d.ts +6 -3
  67. package/dist/modes/rpc/client-admissions.js +9 -4
  68. package/dist/modes/rpc/connection-handler.js +8 -3
  69. package/dist/modes/rpc/effect-commands.d.ts +1 -1
  70. package/dist/modes/rpc/effect-commands.js +2 -2
  71. package/dist/modes/rpc/effect-outcome-copy.d.ts +4 -4
  72. package/dist/modes/rpc/effect-outcome-copy.js +4 -4
  73. package/dist/modes/rpc/effect-record.d.ts +2 -0
  74. package/dist/modes/rpc/effect-record.js +2 -0
  75. package/dist/modes/rpc/effect-status.js +1 -1
  76. package/dist/modes/rpc/session-unload.js +4 -1
  77. package/docs/rpc.md +4 -1
  78. package/docs/settings.md +3 -1
  79. package/package.json +6 -6
  80. package/dist/bundle/chunks/chunk-7ASXJ4AD.js +0 -47
  81. package/dist/bundle/chunks/google-generative-ai-YNIOL5FT.js +0 -2
  82. 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
- * `abort { clearQueue }` is about to drain the queues: give every still-queued input an honest
64
- * `cleared_by_abort` receipt first, so the drain's `queue_update` cannot read it as delivered.
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
- clearQueuedByAbort(): void;
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
- * `abort { clearQueue }` is about to drain the queues: give every still-queued input an honest
146
- * `cleared_by_abort` receipt first, so the drain's `queue_update` cannot read it as delivered.
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
- clearQueuedByAbort() {
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.inputClearedByAbort,
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(abortSource === undefined
656
- ? event
657
- : { ...event, aborted: true, abortSource, ...(abortedBy === undefined ? {} : { abortedBy }) });
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 `cleared_by_abort` receipt) before
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 `cleared_by_abort` receipt) before
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.clearQueuedByAbort();
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 inputClearedByAbort: {
14
- readonly kind: "cleared_by_abort";
15
- readonly reason: "cleared_by_abort";
16
- readonly detail: "an abort with clearQueue removed this queued input before it was delivered";
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
- inputClearedByAbort: {
13
- kind: "cleared_by_abort",
14
- reason: "cleared_by_abort",
15
- detail: "an abort with clearQueue removed this queued input before it was delivered",
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
- `cleared_by_abort` (with `reason`, `detail` and `action`). The answer's `cleared` counts the drained inputs.
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-2",
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-2",
64
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.16-2",
65
- "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.16-2",
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-2",
85
- "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.16-2",
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",