@code-yeongyu/senpi 2026.10.16-2 → 2026.10.16-4

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 (154) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/bundle/chunks/{chunk-QQUHMCOM.js → chunk-2NO5ZOYE.js} +6 -6
  3. package/dist/bundle/chunks/chunk-74YFRVGQ.js +2 -0
  4. package/dist/bundle/chunks/{chunk-4R4PRTF5.js → chunk-CJRE5LLR.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-Q7RD3NOC.js → chunk-GY5O7NIQ.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-KRAABTZY.js → chunk-JD4CV3GH.js} +2 -2
  7. package/dist/bundle/chunks/chunk-K2EVQGGT.js +31 -0
  8. package/dist/bundle/chunks/{chunk-SSEGIXRL.js → chunk-KUQ3K33L.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-V5JFWWGJ.js → chunk-KWC3MCLP.js} +1 -1
  10. package/dist/bundle/chunks/chunk-MLL7AGVM.js +3 -0
  11. package/dist/bundle/chunks/chunk-PAJKEG2L.js +8 -0
  12. package/dist/bundle/chunks/{chunk-DPRV3FR7.js → chunk-PWHAOWNH.js} +1 -1
  13. package/dist/bundle/chunks/chunk-WL4Y6XN6.js +18 -0
  14. package/dist/bundle/chunks/{chunk-VY43SSYL.js → chunk-XCRGEMY2.js} +144 -134
  15. package/dist/bundle/chunks/{cli-main-JDL52ADK.js → cli-main-I6JF2GPS.js} +1 -1
  16. package/dist/bundle/chunks/google-generative-ai-AI6PAUPA.js +2 -0
  17. package/dist/bundle/chunks/google-vertex-USNRIFF7.js +2 -0
  18. package/dist/bundle/chunks/google-vertex-adc-JZURL4XM.js +2 -0
  19. package/dist/bundle/chunks/{help-fast-path-SVCMHCYG.js → help-fast-path-APSHUDY6.js} +1 -1
  20. package/dist/bundle/chunks/{host-session-command-46PTZGIB.js → host-session-command-BUJUJHDA.js} +1 -1
  21. package/dist/bundle/chunks/{interactive-mode-7IM7YJYH.js → interactive-mode-4Q3TIXOZ.js} +1 -1
  22. package/dist/bundle/chunks/{multi-session-host-3MEEUMYU.js → multi-session-host-XEZB2XVR.js} +6 -5
  23. package/dist/bundle/chunks/{openai-codex-responses-T26WW2RE.js → openai-codex-responses-NV27Q6VV.js} +1 -1
  24. package/dist/bundle/chunks/{package-manager-cli-UCHXVBQ2.js → package-manager-cli-YWZHOZOY.js} +1 -1
  25. package/dist/bundle/chunks/{rotation-stream-QX7YSKDU.js → rotation-stream-HY3JPQCX.js} +1 -1
  26. package/dist/bundle/chunks/{rpc-mode-D4BJKWFL.js → rpc-mode-SLWXVXXE.js} +1 -1
  27. package/dist/bundle/chunks/{schedule-command-2RHSWBQO.js → schedule-command-JRWZGEDU.js} +1 -1
  28. package/dist/bundle/chunks/{session-control-endpoint-NBSRSAJS.js → session-control-endpoint-E2ZSEWXW.js} +1 -1
  29. package/dist/bundle/chunks/session-picker-5IZY7E3G.js +2 -0
  30. package/dist/bundle/chunks/session-worker.js +266 -255
  31. package/dist/bundle/cli.js +2 -2
  32. package/dist/bundle/index.js +1 -1
  33. package/dist/bundle/rpc-entry.js +1 -1
  34. package/dist/bundle/runtime-manifest.json +1 -1
  35. package/dist/core/agent-session-runtime.d.ts +15 -0
  36. package/dist/core/agent-session-runtime.js +53 -2
  37. package/dist/core/agent-session.d.ts +28 -0
  38. package/dist/core/agent-session.js +88 -3
  39. package/dist/core/context-files.d.ts +21 -0
  40. package/dist/core/context-files.js +175 -0
  41. package/dist/core/diagnostics.d.ts +1 -0
  42. package/dist/core/dynamic-prompt/intent-gate.d.ts +3 -0
  43. package/dist/core/dynamic-prompt/intent-gate.js +4 -3
  44. package/dist/core/dynamic-prompt/policies.d.ts +4 -1
  45. package/dist/core/dynamic-prompt/policies.js +3 -2
  46. package/dist/core/dynamic-prompt/working-task.d.ts +4 -1
  47. package/dist/core/dynamic-prompt/working-task.js +3 -2
  48. package/dist/core/dynamic-prompt/workstation.d.ts +4 -3
  49. package/dist/core/dynamic-prompt/workstation.js +1 -0
  50. package/dist/core/extensions/builtin/anthropic-subscription/account-command.js +26 -3
  51. package/dist/core/extensions/builtin/anthropic-subscription/account-management.d.ts +5 -0
  52. package/dist/core/extensions/builtin/anthropic-subscription/account-management.js +40 -0
  53. package/dist/core/extensions/builtin/anthropic-subscription/accounts.d.ts +3 -0
  54. package/dist/core/extensions/builtin/anthropic-subscription/accounts.js +1 -1
  55. package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +1 -1
  56. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +1 -0
  57. package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +10 -5
  58. package/dist/core/extensions/builtin/anthropic-subscription/failover.js +158 -17
  59. package/dist/core/extensions/builtin/anthropic-subscription/usage-limit-block.d.ts +13 -0
  60. package/dist/core/extensions/builtin/anthropic-subscription/usage-limit-block.js +89 -0
  61. package/dist/core/extensions/builtin/goal/index.js +18 -8
  62. package/dist/core/extensions/builtin/goal/monitor-continuation.js +7 -0
  63. package/dist/core/extensions/builtin/goal/reload-reengagement.js +5 -0
  64. package/dist/core/extensions/builtin/index.js +3 -0
  65. package/dist/core/extensions/builtin/permission-system/always-scope.d.ts +34 -0
  66. package/dist/core/extensions/builtin/permission-system/always-scope.js +77 -0
  67. package/dist/core/extensions/builtin/permission-system/index.js +3 -1
  68. package/dist/core/extensions/builtin/permission-system/parsers.d.ts +1 -1
  69. package/dist/core/extensions/builtin/permission-system/parsers.js +24 -3
  70. package/dist/core/extensions/builtin/permission-system/prompt.js +5 -1
  71. package/dist/core/extensions/builtin/permission-system/service.js +1 -1
  72. package/dist/core/extensions/builtin/permission-system/types.d.ts +7 -0
  73. package/dist/core/extensions/builtin/prompt-preset/contract.js +6 -1
  74. package/dist/core/extensions/builtin/prompt-preset/gemini-4-argon.d.ts +7 -0
  75. package/dist/core/extensions/builtin/prompt-preset/gemini-4-argon.js +83 -0
  76. package/dist/core/extensions/builtin/prompt-preset/matchers.d.ts +2 -0
  77. package/dist/core/extensions/builtin/prompt-preset/matchers.js +12 -0
  78. package/dist/core/extensions/builtin/prompt-preset/presets.js +3 -0
  79. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
  80. package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
  81. package/dist/core/extensions/builtin/vertex-model-garden/auth.d.ts +16 -0
  82. package/dist/core/extensions/builtin/vertex-model-garden/auth.js +19 -0
  83. package/dist/core/extensions/builtin/vertex-model-garden/index.d.ts +6 -0
  84. package/dist/core/extensions/builtin/vertex-model-garden/index.js +27 -0
  85. package/dist/core/extensions/builtin/vertex-model-garden/models.d.ts +3 -0
  86. package/dist/core/extensions/builtin/vertex-model-garden/models.js +34 -0
  87. package/dist/core/extensions/builtin/vertex-model-garden/transport.d.ts +14 -0
  88. package/dist/core/extensions/builtin/vertex-model-garden/transport.js +35 -0
  89. package/dist/core/extensions/types.d.ts +32 -1
  90. package/dist/core/package-manager.js +4 -32
  91. package/dist/core/project-trust.d.ts +8 -0
  92. package/dist/core/project-trust.js +12 -0
  93. package/dist/core/provider-api-key-auth.d.ts +9 -1
  94. package/dist/core/provider-api-key-auth.js +34 -0
  95. package/dist/core/provider-composer.d.ts +23 -0
  96. package/dist/core/resource-loader.d.ts +5 -7
  97. package/dist/core/resource-loader.js +11 -84
  98. package/dist/core/retry-fallback/circuit-probes.d.ts +1 -0
  99. package/dist/core/retry-fallback/circuit-probes.js +3 -0
  100. package/dist/core/retry-fallback/circuit.d.ts +5 -0
  101. package/dist/core/retry-fallback/circuit.js +27 -4
  102. package/dist/core/retry-fallback/controller.d.ts +9 -4
  103. package/dist/core/retry-fallback/controller.js +21 -4
  104. package/dist/core/settings-manager.d.ts +19 -0
  105. package/dist/core/settings-manager.js +27 -1
  106. package/dist/core/settings-shapes.d.ts +4 -0
  107. package/dist/core/skills-prompt.d.ts +18 -0
  108. package/dist/core/skills-prompt.js +131 -0
  109. package/dist/core/skills.d.ts +1 -9
  110. package/dist/core/skills.js +1 -59
  111. package/dist/core/trust-manager.d.ts +13 -4
  112. package/dist/core/trust-manager.js +52 -21
  113. package/dist/core/turn-deadline.d.ts +92 -0
  114. package/dist/core/turn-deadline.js +179 -0
  115. package/dist/main.js +23 -4
  116. package/dist/modes/interactive/interactive-mode.js +10 -3
  117. package/dist/modes/rpc/abort-grace.d.ts +9 -0
  118. package/dist/modes/rpc/abort-grace.js +11 -0
  119. package/dist/modes/rpc/client-admission-record.d.ts +1 -0
  120. package/dist/modes/rpc/client-admissions.d.ts +6 -3
  121. package/dist/modes/rpc/client-admissions.js +9 -4
  122. package/dist/modes/rpc/connection-handler.js +8 -3
  123. package/dist/modes/rpc/effect-commands.d.ts +1 -3
  124. package/dist/modes/rpc/effect-commands.js +4 -5
  125. package/dist/modes/rpc/effect-outcome-copy.d.ts +4 -4
  126. package/dist/modes/rpc/effect-outcome-copy.js +4 -4
  127. package/dist/modes/rpc/effect-record.d.ts +2 -0
  128. package/dist/modes/rpc/effect-record.js +2 -0
  129. package/dist/modes/rpc/effect-status.js +1 -1
  130. package/dist/modes/rpc/host-handover-parked.d.ts +6 -0
  131. package/dist/modes/rpc/host-handover-parked.js +1 -0
  132. package/dist/modes/rpc/host-handover-reopen.js +11 -2
  133. package/dist/modes/rpc/rpc-client.d.ts +28 -2
  134. package/dist/modes/rpc/rpc-client.js +24 -11
  135. package/dist/modes/rpc/rpc-request-deadline.d.ts +12 -0
  136. package/dist/modes/rpc/rpc-request-deadline.js +15 -0
  137. package/dist/modes/rpc/session-command-router.js +7 -4
  138. package/dist/modes/rpc/session-registry-attach.js +6 -1
  139. package/dist/modes/rpc/session-registry-claims.d.ts +22 -2
  140. package/dist/modes/rpc/session-registry-claims.js +68 -0
  141. package/dist/modes/rpc/session-registry-opening.d.ts +23 -0
  142. package/dist/modes/rpc/session-registry-opening.js +57 -0
  143. package/dist/modes/rpc/session-registry.d.ts +1 -0
  144. package/dist/modes/rpc/session-registry.js +48 -60
  145. package/dist/modes/rpc/session-unload.js +4 -1
  146. package/docs/rpc.md +10 -3
  147. package/docs/settings.md +16 -1
  148. package/package.json +6 -6
  149. package/dist/bundle/chunks/chunk-6MEETY7N.js +0 -8
  150. package/dist/bundle/chunks/chunk-7ASXJ4AD.js +0 -47
  151. package/dist/bundle/chunks/chunk-TEGEPQIA.js +0 -3
  152. package/dist/bundle/chunks/google-generative-ai-YNIOL5FT.js +0 -2
  153. package/dist/bundle/chunks/google-vertex-CF6AYVXL.js +0 -2
  154. package/dist/bundle/chunks/session-picker-6TSBGWOA.js +0 -2
@@ -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
package/dist/main.js CHANGED
@@ -47,7 +47,7 @@ import { ModelRuntime } from "./core/model-runtime.js";
47
47
  import { markMovedSessions, movedHereSessionToContinue, withMovedSessions } from "./core/moved-sessions.js";
48
48
  import { restoreStdout, takeOverStdout } from "./core/output-guard.js";
49
49
  import { recordProcessLifetime } from "./core/process-crash-record.js";
50
- import { resolveProjectTrusted } from "./core/project-trust.js";
50
+ import { presetTrustsProject, resolveProjectTrusted } from "./core/project-trust.js";
51
51
  import { resolveResumeTarget } from "./core/resume-target.js";
52
52
  import { formatMissingSessionCwdPrompt, getMissingSessionCwdIssue, MissingSessionCwdError, } from "./core/session-cwd.js";
53
53
  import { sessionExtensionFlagValues } from "./core/session-extension-flags.js";
@@ -677,13 +677,26 @@ export function createCliRuntimeFactory(configuration, local = {}) {
677
677
  const projectTrustDiagnostics = [];
678
678
  const cachedProjectTrust = projectTrustByCwd.get(cwd);
679
679
  const hasTrustRequiringResources = hasTrustRequiringProjectResources(cwd);
680
- const shouldResolveProjectTrust = parsed.projectTrustOverride === undefined && cachedProjectTrust === undefined && hasTrustRequiringResources;
680
+ // A full-access session trusts its project without asking, for that session only: nothing is
681
+ // cached here or saved to the trust store, and a saved "do not trust" still wins (#2465).
682
+ const fullAccessTrust = presetTrustsProject(sessionExtensionFlagValues(parsed.unknownFlags, launchProfile).get("permission-preset"), startupSettingsManager.getGlobalSettings().permissionPreset) && trustStore.get(cwd) !== false;
683
+ const shouldResolveProjectTrust = parsed.projectTrustOverride === undefined &&
684
+ cachedProjectTrust === undefined &&
685
+ !fullAccessTrust &&
686
+ hasTrustRequiringResources;
681
687
  const projectTrusted = shouldResolveProjectTrust
682
688
  ? false
683
689
  : (cachedProjectTrust ??
684
690
  parsed.projectTrustOverride ??
685
- (!hasTrustRequiringResources || trustStore.get(cwd) === true));
686
- const runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
691
+ (fullAccessTrust || !hasTrustRequiringResources || trustStore.get(cwd) === true));
692
+ // Trusted only because of Full access (resources need trust and nothing else granted it): leaving
693
+ // Full access withdraws it (#3420 review T5).
694
+ const trustFromFullAccess = fullAccessTrust &&
695
+ hasTrustRequiringResources &&
696
+ cachedProjectTrust === undefined &&
697
+ parsed.projectTrustOverride === undefined &&
698
+ trustStore.get(cwd) !== true;
699
+ const runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted, trustFromFullAccess });
687
700
  // The opener's per-session fallback policy is an in-memory override: never saved, never shared.
688
701
  if (launchProfile?.retryFallback)
689
702
  applyRetryFallbackProfile(runtimeSettingsManager, launchProfile.retryFallback);
@@ -1194,6 +1207,12 @@ export async function main(args, options) {
1194
1207
  reportDiagnostics([{ type: "warning", message: formatThinkingClampWarning(startupThinkingClamp) }]);
1195
1208
  }
1196
1209
  if (appMode !== "interactive") {
1210
+ reportDiagnostics([...(resourceLoader.getAgentsFiles().diagnostics ?? []), ...resourceLoader.getSkills().diagnostics]
1211
+ .filter((diagnostic) => diagnostic.type === "warning" && diagnostic.category === "size-limit")
1212
+ .map((diagnostic) => ({
1213
+ type: "warning",
1214
+ message: diagnostic.path ? `${diagnostic.path}: ${diagnostic.message}` : diagnostic.message,
1215
+ })));
1197
1216
  reportDiagnostics(collectAuthDiagnostics(services.authStorage, "runtime creation"));
1198
1217
  }
1199
1218
  if (runtime.diagnostics.some((diagnostic) => diagnostic.type === "error")) {
@@ -1952,16 +1952,23 @@ export class InteractiveMode {
1952
1952
  }
1953
1953
  }
1954
1954
  if (showDiagnostics) {
1955
- const addDiagnosticNotice = (title, diagnostics) => {
1955
+ const addDiagnosticNotice = (title, diagnostics, why = "Conflicting or invalid loaded resources were found.") => {
1956
1956
  this.loadedResourcesContainer.addChild(buildNoticeBox({
1957
1957
  title,
1958
1958
  tone: "warning",
1959
- why: "Conflicting or invalid loaded resources were found.",
1959
+ why,
1960
1960
  extra: this.formatDiagnostics(diagnostics, sourceInfos),
1961
1961
  }, { expanded: this.toolOutputExpanded }, theme));
1962
1962
  this.loadedResourcesContainer.addChild(new Spacer(1));
1963
1963
  };
1964
- const skillDiagnostics = skillsResult.diagnostics;
1964
+ const contextDiagnostics = [
1965
+ ...(this.session.resourceLoader.getAgentsFiles().diagnostics ?? []),
1966
+ ...skillsResult.diagnostics.filter((diagnostic) => diagnostic.category === "size-limit"),
1967
+ ];
1968
+ if (contextDiagnostics.length > 0) {
1969
+ addDiagnosticNotice("Context size limits", contextDiagnostics, "Context files or skill catalogs were limited to keep requests bounded.");
1970
+ }
1971
+ const skillDiagnostics = skillsResult.diagnostics.filter((diagnostic) => diagnostic.category !== "size-limit");
1965
1972
  if (skillDiagnostics.length > 0) {
1966
1973
  addDiagnosticNotice("Skill conflicts", skillDiagnostics);
1967
1974
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The `abort { awaitIdle }` grace window, shared by the host that enforces it and the client that sizes its
3
+ * request deadline from it (docs/rpc.md `abort`). Kept free of host imports so the client can load it.
4
+ */
5
+ export declare const ABORT_DEFAULT_GRACE_MS = 10000;
6
+ export declare const ABORT_MAX_GRACE_MS = 60000;
7
+ /** A `graceMs` the host accepts: a whole number of milliseconds from 0 to {@link ABORT_MAX_GRACE_MS}. */
8
+ export declare function isAbortGraceInRange(graceMs: unknown): graceMs is number;
9
+ //# sourceMappingURL=abort-grace.d.ts.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The `abort { awaitIdle }` grace window, shared by the host that enforces it and the client that sizes its
3
+ * request deadline from it (docs/rpc.md `abort`). Kept free of host imports so the client can load it.
4
+ */
5
+ export const ABORT_DEFAULT_GRACE_MS = 10_000;
6
+ export const ABORT_MAX_GRACE_MS = 60_000;
7
+ /** A `graceMs` the host accepts: a whole number of milliseconds from 0 to {@link ABORT_MAX_GRACE_MS}. */
8
+ export function isAbortGraceInRange(graceMs) {
9
+ return typeof graceMs === "number" && Number.isInteger(graceMs) && graceMs >= 0 && graceMs <= ABORT_MAX_GRACE_MS;
10
+ }
11
+ //# sourceMappingURL=abort-grace.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,
@@ -18,10 +18,8 @@ export declare function beginEffect(ctx: EffectCommandContext, kind: EffectComma
18
18
  readonly effectId: string;
19
19
  }): Promise<EffectBegin | RpcResponse>;
20
20
  export declare function isEffectBegin(value: EffectBegin | RpcResponse): value is EffectBegin;
21
- export declare const ABORT_DEFAULT_GRACE_MS = 10000;
22
- export declare const ABORT_MAX_GRACE_MS = 60000;
23
21
  /**
24
- * `abort`: with `clearQueue` the queued inputs are drained (each gets a `cleared_by_abort` receipt) before
22
+ * `abort`: with `clearQueue` the queued inputs are drained (each gets a `cleared` receipt) before
25
23
  * the abort; with `awaitIdle` the answer waits until the session is idle or `graceMs` passes. Without
26
24
  * either, the abort is acknowledged on dispatch exactly as before.
27
25
  */
@@ -1,5 +1,6 @@
1
1
  import { ModelUsabilityBudgetError } from "../../core/extensions/builtin/compaction/model-usability-budget.js";
2
2
  import { ModelSwitchAuthError } from "../../core/model-switch-auth-error.js";
3
+ import { ABORT_DEFAULT_GRACE_MS, ABORT_MAX_GRACE_MS, isAbortGraceInRange } from "./abort-grace.js";
3
4
  import { ClientAdmissions } from "./client-admissions.js";
4
5
  import { EffectLedger } from "./effect-ledger.js";
5
6
  import { contextBudgetOutcome, EFFECT_OUTCOMES, modelSwitchErrorOutcome, providerUnavailableOutcome, unknownModelOutcome, unsupportedThinkingLevelOutcome, } from "./effect-outcome-copy.js";
@@ -45,13 +46,11 @@ function isResponse(value) {
45
46
  export function isEffectBegin(value) {
46
47
  return !isResponse(value);
47
48
  }
48
- export const ABORT_DEFAULT_GRACE_MS = 10_000;
49
- export const ABORT_MAX_GRACE_MS = 60_000;
50
49
  function invalidGrace(command) {
51
50
  const { graceMs } = command;
52
51
  if (graceMs === undefined)
53
52
  return undefined;
54
- if (Number.isInteger(graceMs) && graceMs >= 0 && graceMs <= ABORT_MAX_GRACE_MS)
53
+ if (isAbortGraceInRange(graceMs))
55
54
  return undefined;
56
55
  const reason = "invalid_grace_ms";
57
56
  const action = `send graceMs as a whole number of milliseconds from 0 to ${ABORT_MAX_GRACE_MS}`;
@@ -74,7 +73,7 @@ async function quiesce(session, aborting, graceMs) {
74
73
  }
75
74
  }
76
75
  /**
77
- * `abort`: with `clearQueue` the queued inputs are drained (each gets a `cleared_by_abort` receipt) before
76
+ * `abort`: with `clearQueue` the queued inputs are drained (each gets a `cleared` receipt) before
78
77
  * the abort; with `awaitIdle` the answer waits until the session is idle or `graceMs` passes. Without
79
78
  * either, the abort is acknowledged on dispatch exactly as before.
80
79
  */
@@ -93,7 +92,7 @@ export async function runAbort(ctx, command, dispatch) {
93
92
  const streaming = ctx.session.isStreaming;
94
93
  let cleared = 0;
95
94
  if (command.clearQueue === true) {
96
- admissions.clearQueuedByAbort();
95
+ admissions.clearQueued();
97
96
  const drained = ctx.session.clearQueue({ abortWillFollow: true });
98
97
  cleared = drained.steering.length + drained.followUp.length;
99
98
  }
@@ -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,
@@ -30,6 +30,12 @@ export interface HandoverParkedEntry {
30
30
  readonly sessionContext?: SessionContext;
31
31
  readonly autoTitle?: boolean;
32
32
  readonly retryFallback?: SessionRetryFallbackProfile;
33
+ /**
34
+ * The host instance that wrote the park. Its own reopen watch skips it: the park is for the NEXT
35
+ * generation, and a host reopening what it just idle-parked races the client's reopen (senpi#3485).
36
+ * Absent on entries an older generation wrote, which every reader treats as a predecessor's park.
37
+ */
38
+ readonly parkedBy?: string;
33
39
  }
34
40
  export declare function recordHandoverParkedSession(daemonDir: string, entry: HandoverParkedEntry): Promise<void>;
35
41
  export declare function readHandoverParkedSessions(daemonDir: string): Promise<readonly HandoverParkedEntry[]>;
@@ -35,6 +35,7 @@ function isEntry(value) {
35
35
  typeof record.retainOnDisconnect === "boolean" &&
36
36
  (record.durableSessionId === undefined || typeof record.durableSessionId === "string") &&
37
37
  (record.cwd === undefined || typeof record.cwd === "string") &&
38
+ (record.parkedBy === undefined || typeof record.parkedBy === "string") &&
38
39
  (record.thinkingLevel === undefined || typeof record.thinkingLevel === "string") &&
39
40
  (record.permissionPreset === undefined ||
40
41
  (typeof record.permissionPreset === "string" &&
@@ -46,12 +46,12 @@ export async function reopenHandoverParkedSessions(open, agentDir, socket, holde
46
46
  // it by path with the same durable id.
47
47
  if (holder?.shouldReopen !== undefined && !holder.shouldReopen())
48
48
  return;
49
- const parked = await readHandoverParkedSessions(daemonDir);
49
+ const parked = await inheritedParks(daemonDir, holder);
50
50
  for (const entry of parked) {
51
51
  sawAnyPark = true;
52
52
  await reopenOne(daemonDir, open, entry, reopenAttempts, heldPolls, holder);
53
53
  }
54
- const remaining = (await readHandoverParkedSessions(daemonDir)).length;
54
+ const remaining = (await inheritedParks(daemonDir, holder)).length;
55
55
  emptyStreak = remaining === 0 ? emptyStreak + 1 : 0;
56
56
  if (sawAnyPark && emptyStreak >= HANDOVER_PARKED_REOPEN_QUIET_STREAK)
57
57
  return;
@@ -60,6 +60,15 @@ export async function reopenHandoverParkedSessions(open, agentDir, socket, holde
60
60
  await new Promise((resolve) => setTimeout(resolve, HANDOVER_PARKED_REOPEN_POLL_MS));
61
61
  }
62
62
  }
63
+ /**
64
+ * The parks a PREVIOUS generation left for this one. A park this host wrote itself (its idle sweep
65
+ * parks retained sessions into the same record) is for its successor: reopening it here would put a
66
+ * second open of the path in flight against the client reopening it (senpi#3485).
67
+ */
68
+ async function inheritedParks(daemonDir, holder) {
69
+ const parked = await readHandoverParkedSessions(daemonDir);
70
+ return parked.filter((entry) => entry.parkedBy === undefined || entry.parkedBy !== holder?.hostInstance);
71
+ }
63
72
  async function reopenOne(daemonDir, open, entry, attempts, heldPolls, holder) {
64
73
  await dropHandoverParkedSessionIfFileGone(daemonDir, entry.sessionPath);
65
74
  if (!existsSync(entry.sessionPath)) {
@@ -22,7 +22,7 @@ export declare class RpcCommandError extends Error {
22
22
  }
23
23
  import type { SessionEntry, SessionTreeNode } from "../../core/session-manager.ts";
24
24
  import type { JsonAgentSessionEvent } from "../json-event.ts";
25
- import type { EditAssistantMessageResult, EditUserMessageResult, RpcAccountFailoverEvent, RpcAuthAccountsChangedEvent, RpcCommand, RpcEvalCell, RpcEvalCellSnapshot, RpcExtensionEvent, RpcExtensionUIProgress, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcOpenQueuedEvent, RpcProviderAccount, RpcSessionModelEntry, RpcSessionParkedEvent, RpcSessionReplacedEvent, RpcSessionState, RpcSlashCommand, RpcUnloadAnswer } from "./rpc-types.ts";
25
+ import type { EditAssistantMessageResult, EditUserMessageResult, RpcAccountFailoverEvent, RpcAuthAccountsChangedEvent, RpcCommand, RpcEvalCell, RpcEvalCellSnapshot, RpcExtensionEvent, RpcExtensionUIProgress, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcOpenQueuedEvent, RpcProviderAccount, RpcResponse, RpcSessionModelEntry, RpcSessionParkedEvent, RpcSessionReplacedEvent, RpcSessionState, RpcSlashCommand, RpcUnloadAnswer } from "./rpc-types.ts";
26
26
  export interface RpcClientOptions {
27
27
  /** Connect to an existing multi-session Unix socket instead of spawning a child. */
28
28
  socketPath?: string;
@@ -72,6 +72,20 @@ export declare class RpcTransportGoneError extends Error {
72
72
  constructor(message?: string);
73
73
  }
74
74
  export declare function isTransportGoneError(error: unknown): error is RpcTransportGoneError;
75
+ /** The options of a typed `abort` (see `docs/rpc.md`, `abort`). */
76
+ export type RpcAbortOptions = Omit<Extract<RpcCommand, {
77
+ type: "abort";
78
+ }>, "type" | "id" | "sessionId">;
79
+ /** The host's answer to a typed `abort`; an older host answers with no data. */
80
+ export type RpcAbortAnswer = Extract<RpcResponse, {
81
+ command: "abort";
82
+ success: true;
83
+ }>["data"];
84
+ /** `get_effect_status`: the receipt, or proof the whole transcript holds none. */
85
+ export type RpcEffectStatusAnswer = Extract<RpcResponse, {
86
+ command: "get_effect_status";
87
+ success: true;
88
+ }>["data"];
75
89
  export declare class RpcClientOpenInFlightError extends Error {
76
90
  readonly code: "open_session_in_flight";
77
91
  constructor();
@@ -222,9 +236,21 @@ export declare class RpcClient {
222
236
  enqueueOrder?: number;
223
237
  }): Promise<QueuedInputDisposition>;
224
238
  /**
225
- * Abort current operation.
239
+ * Abort current operation. With no options this is the fire-and-forget abort it always was (transport
240
+ * loss resolves). With options it is the typed abort: `effectId` records a receipt, `awaitIdle` answers
241
+ * once the turn stopped (or `graceMs` passed), `clearQueue` drains queued input first; the host's
242
+ * answer (`effectId`, `state`, `effect`, `stopped`, `idle`, `cleared`) is returned.
226
243
  */
227
244
  abort(): Promise<void>;
245
+ abort(options: RpcAbortOptions): Promise<RpcAbortAnswer>;
246
+ abort(options?: RpcAbortOptions): Promise<RpcAbortAnswer | undefined>;
247
+ /**
248
+ * The newest receipt of one effect (`get_effect_status`, capability `effect_ledger`). With `sessionPath`
249
+ * the host reads that transcript from disk, so a session no host holds is answered without loading it.
250
+ */
251
+ getEffectStatus(effectId: string, options?: {
252
+ readonly sessionPath?: string;
253
+ }): Promise<RpcEffectStatusAnswer>;
228
254
  /** Abort with host refusals and transport loss surfaced to machine callers. */
229
255
  abortStrict(): Promise<void>;
230
256
  abortCompaction(): Promise<void>;