@code-yeongyu/senpi 2026.11.2-3 → 2026.11.3-1

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 (169) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/bundle/chunks/{anthropic-messages-WUPLDYD6.js → anthropic-messages-AFWQB3KN.js} +1 -1
  3. package/dist/bundle/chunks/{args-AOI4NEA6.js → args-O3WS4WCO.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-O4GRBX4R.js → chunk-3WJ2DE2G.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-LMIETUPG.js → chunk-4BF4FLT6.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-T5ZJ2Q4O.js → chunk-4SENPCEA.js} +1 -1
  7. package/dist/bundle/chunks/chunk-5DZD623T.js +448 -0
  8. package/dist/bundle/chunks/{chunk-TXOR5BQL.js → chunk-6BSMXZKQ.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-VRWJJMHO.js → chunk-7565XQAU.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-IWM5QRAE.js → chunk-7KLEFHSU.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-FO56LSUK.js → chunk-BNIZP4OS.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-L3V65XW7.js → chunk-CJWSQ6CK.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-QLTQ3UUI.js → chunk-DAPBNRN5.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-GIY7PGOH.js → chunk-DPBPO5OU.js} +1 -1
  15. package/dist/bundle/chunks/chunk-ESQBKTNG.js +2 -0
  16. package/dist/bundle/chunks/{chunk-N4TSAHRL.js → chunk-EVTTLPIY.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-NYXXKS6D.js → chunk-F4OHTL2K.js} +3 -2
  18. package/dist/bundle/chunks/{chunk-TVMW2SUP.js → chunk-FJ2UCBOA.js} +2 -2
  19. package/dist/bundle/chunks/{chunk-KVPRQ5KN.js → chunk-FLM6GZPT.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-P4AT7SBE.js → chunk-HBCNDC53.js} +1 -1
  21. package/dist/bundle/chunks/chunk-MOH5HOHX.js +2 -0
  22. package/dist/bundle/chunks/{chunk-7FUO655J.js → chunk-MOOFLKLI.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-IHOQR7TU.js → chunk-PX7FF5LE.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-QAM6AXA2.js → chunk-QTQ3ES2S.js} +12 -9
  25. package/dist/bundle/chunks/{chunk-FJSJUVQQ.js → chunk-TOOB6OLG.js} +3 -3
  26. package/dist/bundle/chunks/{chunk-662SCMTH.js → chunk-VPDQ6MOL.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-BW4YXS7X.js → chunk-WCRNJYHO.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-ZSVWKOAJ.js → chunk-ZNLCKKF6.js} +170 -226
  29. package/dist/bundle/chunks/{cli-main-534A3WSC.js → cli-main-XM5FWY3Z.js} +1 -1
  30. package/dist/bundle/chunks/{credential-accounts-OAK2E5O2.js → credential-accounts-YZYSKA7X.js} +1 -1
  31. package/dist/bundle/chunks/{health-write-EB4CPHXN.js → health-write-45CX64T2.js} +1 -1
  32. package/dist/bundle/chunks/{help-fast-path-U2PSNNWO.js → help-fast-path-3EDGRKQM.js} +1 -1
  33. package/dist/bundle/chunks/{host-command-WJIMRBGN.js → host-command-PZ4VYQIW.js} +1 -1
  34. package/dist/bundle/chunks/{host-session-command-PWVPQ2OV.js → host-session-command-OJFARE5Q.js} +1 -1
  35. package/dist/bundle/chunks/{interactive-mode-NPHFDPGH.js → interactive-mode-7JSJVVIS.js} +1 -1
  36. package/dist/bundle/chunks/{login-health-7G2DZVOV.js → login-health-YOPTYRTU.js} +1 -1
  37. package/dist/bundle/chunks/{main-I6FJAFUP.js → main-3YGJ2O4I.js} +1 -1
  38. package/dist/bundle/chunks/{multi-session-host-7E26R5JF.js → multi-session-host-N2RQ7JSZ.js} +4 -4
  39. package/dist/bundle/chunks/openai-chatgpt.js +1 -1
  40. package/dist/bundle/chunks/{openai-codex-responses-VNBMMIL3.js → openai-codex-responses-YTWLG4OL.js} +1 -1
  41. package/dist/bundle/chunks/{openai-completions-JLR32APM.js → openai-completions-TIFDYHZL.js} +1 -1
  42. package/dist/bundle/chunks/{openai-images-LEBNERSM.js → openai-images-OAEMY4RG.js} +1 -1
  43. package/dist/bundle/chunks/{openai-responses-JCDUNMHA.js → openai-responses-L35NIG5Y.js} +1 -1
  44. package/dist/bundle/chunks/{package-manager-cli-CBSDV36M.js → package-manager-cli-JPNA27E5.js} +1 -1
  45. package/dist/bundle/chunks/{role-LP6LVNCY.js → role-45Y7PQFC.js} +1 -1
  46. package/dist/bundle/chunks/{rotation-stream-ZDCNHIBE.js → rotation-stream-WXWJKJZN.js} +1 -1
  47. package/dist/bundle/chunks/{rpc-mode-GBBTN2WP.js → rpc-mode-Z7MN2ZWA.js} +1 -1
  48. package/dist/bundle/chunks/{run-attached-tui-PVLUHHJX.js → run-attached-tui-TI6MELEI.js} +1 -1
  49. package/dist/bundle/chunks/{schedule-command-W45NYTTK.js → schedule-command-OZ5ESMIT.js} +1 -1
  50. package/dist/bundle/chunks/{session-control-endpoint-LFLUNALC.js → session-control-endpoint-5YKBYJOO.js} +1 -1
  51. package/dist/bundle/chunks/{session-picker-OZUINLD7.js → session-picker-CMRV72VA.js} +1 -1
  52. package/dist/bundle/chunks/session-worker.js +324 -376
  53. package/dist/bundle/chunks/{state-store-SZARKD36.js → state-store-Q47AYMRI.js} +1 -1
  54. package/dist/bundle/cli.js +2 -2
  55. package/dist/bundle/index.js +1 -1
  56. package/dist/bundle/rpc-entry.js +1 -1
  57. package/dist/bundle/runtime-manifest.json +1 -1
  58. package/dist/core/agent-session.d.ts +8 -0
  59. package/dist/core/agent-session.js +68 -60
  60. package/dist/core/credential-pool/classify.d.ts +3 -2
  61. package/dist/core/credential-pool/classify.js +10 -3
  62. package/dist/core/dynamic-prompt/build.d.ts +2 -1
  63. package/dist/core/dynamic-prompt/build.js +1 -2
  64. package/dist/core/dynamic-prompt/handoff.d.ts +1 -1
  65. package/dist/core/dynamic-prompt/handoff.js +9 -9
  66. package/dist/core/dynamic-prompt/identity.js +1 -1
  67. package/dist/core/dynamic-prompt/intent-gate.d.ts +1 -1
  68. package/dist/core/dynamic-prompt/intent-gate.js +8 -8
  69. package/dist/core/dynamic-prompt/policies.d.ts +1 -1
  70. package/dist/core/dynamic-prompt/policies.js +5 -7
  71. package/dist/core/dynamic-prompt/style.js +5 -5
  72. package/dist/core/dynamic-prompt/tool-section.d.ts +5 -0
  73. package/dist/core/dynamic-prompt/tool-section.js +8 -6
  74. package/dist/core/dynamic-prompt/verification.d.ts +6 -6
  75. package/dist/core/dynamic-prompt/verification.js +12 -12
  76. package/dist/core/dynamic-prompt/working-task.d.ts +1 -1
  77. package/dist/core/dynamic-prompt/working-task.js +3 -3
  78. package/dist/core/dynamic-prompt/workstation.js +1 -1
  79. package/dist/core/extensions/builtin/anthropic-subscription/session-registry-pump.d.ts +13 -0
  80. package/dist/core/extensions/builtin/anthropic-subscription/session-registry-pump.js +64 -2
  81. package/dist/core/extensions/builtin/anthropic-subscription/session-stream.js +5 -1
  82. package/dist/core/extensions/builtin/anthropic-subscription/session-turn-attempt.d.ts +7 -0
  83. package/dist/core/extensions/builtin/anthropic-subscription/session-turn-attempt.js +28 -1
  84. package/dist/core/extensions/builtin/anthropic-subscription/session-turn-types.d.ts +2 -0
  85. package/dist/core/extensions/builtin/bash-timeout/index.d.ts +1 -1
  86. package/dist/core/extensions/builtin/bash-timeout/index.js +3 -28
  87. package/dist/core/extensions/builtin/bash-timeout/timeout.d.ts +0 -11
  88. package/dist/core/extensions/builtin/bash-timeout/timeout.js +0 -11
  89. package/dist/core/extensions/builtin/imagegen/index.d.ts +0 -1
  90. package/dist/core/extensions/builtin/imagegen/index.js +0 -11
  91. package/dist/core/extensions/builtin/prompt-preset/execution-tooling.d.ts +5 -5
  92. package/dist/core/extensions/builtin/prompt-preset/execution-tooling.js +5 -5
  93. package/dist/core/extensions/builtin/prompt-preset/glm-5.d.ts +1 -1
  94. package/dist/core/extensions/builtin/prompt-preset/glm-5.js +1 -1
  95. package/dist/core/extensions/builtin/terminal/extension.js +8 -1
  96. package/dist/core/extensions/builtin/terminal/notify.js +9 -3
  97. package/dist/core/extensions/builtin/terminal/prompt.d.ts +7 -1
  98. package/dist/core/extensions/builtin/terminal/prompt.js +27 -60
  99. package/dist/core/extensions/builtin/terminal/tools/bash.js +10 -2
  100. package/dist/core/extensions/builtin/todotools/normalize.d.ts +20 -3
  101. package/dist/core/extensions/builtin/todotools/normalize.js +251 -5
  102. package/dist/core/extensions/builtin/todotools/prompt.d.ts +2 -2
  103. package/dist/core/extensions/builtin/todotools/prompt.js +5 -5
  104. package/dist/core/extensions/builtin/todotools/tools/todo.js +9 -8
  105. package/dist/core/extensions/builtin/webfetch/webfetch/tool.js +0 -5
  106. package/dist/core/extensions/builtin/websearch/index.js +9 -4
  107. package/dist/core/extensions/builtin/websearch/websearch/tool.d.ts +2 -0
  108. package/dist/core/extensions/builtin/websearch/websearch/tool.js +3 -1
  109. package/dist/core/model-runtime.js +1 -1
  110. package/dist/core/provider-failure.d.ts +6 -0
  111. package/dist/core/provider-failure.js +6 -2
  112. package/dist/core/retry-fallback/exhaustion.d.ts +18 -0
  113. package/dist/core/retry-fallback/exhaustion.js +20 -0
  114. package/dist/core/system-prompt.js +1 -1
  115. package/dist/core/tools/bash.d.ts +3 -1
  116. package/dist/core/tools/bash.js +14 -4
  117. package/dist/core/tools/edit.d.ts +1 -1
  118. package/dist/core/tools/edit.js +1 -6
  119. package/dist/core/tools/grep/index.js +2 -3
  120. package/dist/core/tools/powershell.d.ts +1 -1
  121. package/dist/core/tools/powershell.js +1 -1
  122. package/dist/core/tools/read.d.ts +1 -1
  123. package/dist/core/tools/read.js +1 -1
  124. package/dist/core/tools/write.d.ts +1 -1
  125. package/dist/core/tools/write.js +1 -1
  126. package/dist/engine-trust/shell-trust.d.ts +36 -0
  127. package/dist/engine-trust/shell-trust.js +44 -0
  128. package/dist/engine-trust/transport-failure.d.ts +12 -0
  129. package/dist/engine-trust/transport-failure.js +48 -0
  130. package/dist/modes/interactive/interactive-mode.js +9 -3
  131. package/dist/modes/print-mode.js +2 -1
  132. package/dist/modes/rpc/compact-catalog-families.d.ts +5 -2
  133. package/dist/modes/rpc/compact-catalog-families.js +5 -2
  134. package/dist/modes/rpc/compact-catalog-registry.d.ts +9 -1
  135. package/dist/modes/rpc/compact-catalog-registry.js +9 -2
  136. package/dist/modes/rpc/compact-recovery-manifest-kinds.d.ts +17 -0
  137. package/dist/modes/rpc/compact-recovery-manifest-kinds.js +21 -0
  138. package/dist/modes/rpc/compact-recovery-manifest-shape.d.ts +7 -0
  139. package/dist/modes/rpc/compact-recovery-manifest.js +11 -3
  140. package/dist/modes/rpc/compact-socket-commands.js +6 -0
  141. package/dist/modes/rpc/connection-handler.js +21 -5
  142. package/dist/modes/rpc/connection-question-bridge.d.ts +3 -1
  143. package/dist/modes/rpc/connection-question-bridge.js +4 -1
  144. package/dist/modes/rpc/connection-roles.d.ts +8 -0
  145. package/dist/modes/rpc/connection-roles.js +18 -0
  146. package/dist/modes/rpc/method-classes-schema-session.d.ts +4 -0
  147. package/dist/modes/rpc/method-classes-schema-session.js +1 -0
  148. package/dist/modes/rpc/method-classes-schema.d.ts +4 -0
  149. package/dist/modes/rpc/protocol-identity.js +2 -0
  150. package/dist/modes/rpc/remote-origin.d.ts +31 -0
  151. package/dist/modes/rpc/remote-origin.js +58 -0
  152. package/dist/modes/rpc/rpc-protocol-info-types.d.ts +8 -0
  153. package/dist/modes/rpc/rpc-question-types.d.ts +7 -0
  154. package/dist/modes/rpc/rpc-question-types.js +0 -3
  155. package/dist/modes/rpc/rpc-session-command-types.d.ts +3 -0
  156. package/dist/modes/rpc/session-registry.js +12 -3
  157. package/dist/modes/rpc/session-worker-client.js +2 -0
  158. package/dist/modes/rpc/session-worker-protocol.d.ts +3 -0
  159. package/dist/modes/rpc/session-worker.js +2 -0
  160. package/docs/engine-socket/fixtures/admission-routes/c-baseline.json +3 -3
  161. package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
  162. package/docs/engine-socket/fixtures/admission-routes/i-reset-stream.json +3 -3
  163. package/docs/engine-socket/fixtures/recovery-manifest/index.json +32 -0
  164. package/docs/engine-socket/fixtures/recovery-manifest/one-partial-family.json +118 -0
  165. package/docs/rpc.md +93 -3
  166. package/package.json +6 -6
  167. package/dist/bundle/chunks/chunk-7UC4W2T2.js +0 -2
  168. package/dist/bundle/chunks/chunk-R23WXN4L.js +0 -448
  169. package/dist/bundle/chunks/chunk-W5NLCRB3.js +0 -2
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_INIT_PHASE } from "./todo-types.js";
1
2
  const CONFLICTING_SHAPES = 'conflicting shapes. Use {"op":"init","list":[...]} to replace the list or {"op":"append","phase":"<active phase>","items":[...]} to add tasks.';
2
3
  function isRecord(value) {
3
4
  return typeof value === "object" && value !== null;
@@ -66,12 +67,257 @@ function unwrapTodoList(value) {
66
67
  }
67
68
  return Array.isArray(current) && current.length > 0 ? current : value;
68
69
  }
69
- /** The todo tool's `prepareArguments`: returns the arguments with `list` unwrapped to its canonical shape. */
70
- export function prepareTodoArguments(args) {
71
- if (!isRecord(args) || Array.isArray(args) || !("list" in args))
70
+ /**
71
+ * Reserved string key under which `prepareTodoArguments` carries its correction notices to
72
+ * `execute`. `validateToolArguments` clones prepared arguments with `structuredClone`, which
73
+ * drops symbol keys, so the channel must be a string key; `execute` strips it before
74
+ * `normalizeTodoParams` and prepends the notices. The key is never persisted or rendered,
75
+ * but prepared arguments flow to the agent loop's `beforeToolCall`/`afterToolCall` hooks, so
76
+ * extension `tool_call`/`tool_result` hooks may observe it on the arguments they receive.
77
+ */
78
+ const TODO_PREPARED_CORRECTIONS_KEY = "__todoPreparedCorrections";
79
+ function isPhaseRecord(value) {
80
+ return isRecord(value) && typeof value.phase === "string" && Array.isArray(value.items);
81
+ }
82
+ function isContentRecord(value) {
83
+ return isRecord(value) && typeof value.content === "string";
84
+ }
85
+ function quoted(values) {
86
+ return values.map((value) => JSON.stringify(value)).join(", ");
87
+ }
88
+ /**
89
+ * Rewrites near-canonical `list` entries (senpi#5332) to the canonical shape: an entry whose
90
+ * `items` are all nested `{phase, items}` records is replaced by them, `items` entries that
91
+ * are records carrying a string `content` become that string, entries with missing or empty
92
+ * `items` and stray string entries are dropped, and entries with tasks but no usable `phase`
93
+ * name get sequential default names. An `items` array that mixes strings with phase records
94
+ * is ambiguous and returns `null`, as does nothing else here: the caller then leaves the
95
+ * whole call untouched so validation rejects it as before. The input array and its entries
96
+ * are never mutated; when nothing changes, the same array reference is returned.
97
+ */
98
+ function normalizeTodoListEntries(input, notices) {
99
+ let list = [...input];
100
+ let changed = false;
101
+ const promotedOuter = [];
102
+ for (let depth = 0; depth < 5; depth++) {
103
+ const next = [];
104
+ let promotedThisPass = false;
105
+ for (const entry of list) {
106
+ if (isPhaseRecord(entry) && entry.items.length > 0 && entry.items.every(isPhaseRecord)) {
107
+ next.push(...entry.items);
108
+ promotedOuter.push(entry.phase);
109
+ promotedThisPass = true;
110
+ }
111
+ else {
112
+ next.push(entry);
113
+ }
114
+ }
115
+ if (!promotedThisPass)
116
+ break;
117
+ list = next;
118
+ }
119
+ if (promotedOuter.length > 0) {
120
+ changed = true;
121
+ notices.push(`[auto-corrected] "list" ${promotedOuter.length === 1 ? "entry" : "entries"} ${quoted(promotedOuter)} contained only nested phases; promoted the nested phases to top level. Each "list" entry is exactly one phase: {"phase":"<name>","items":["<task>", ...]}`);
122
+ }
123
+ let unwrappedContent = 0;
124
+ for (let index = 0; index < list.length; index++) {
125
+ const entry = list[index];
126
+ if (!isPhaseRecord(entry))
127
+ continue;
128
+ const mapped = entry.items.map((item) => {
129
+ if (isContentRecord(item) && !isPhaseRecord(item)) {
130
+ unwrappedContent += 1;
131
+ return item.content;
132
+ }
133
+ return item;
134
+ });
135
+ // Strings and nested phase records in one items array cannot be told apart, so no
136
+ // correction may guess: leave the whole call to validation.
137
+ if (mapped.some((item) => typeof item === "string") && mapped.some(isPhaseRecord))
138
+ return null;
139
+ if (!mapped.every((item) => typeof item === "string"))
140
+ continue;
141
+ if (mapped.some((item, position) => item !== entry.items[position])) {
142
+ list[index] = { ...entry, items: mapped };
143
+ changed = true;
144
+ }
145
+ }
146
+ if (unwrappedContent > 0) {
147
+ notices.push(`[auto-corrected] unwrapped ${unwrappedContent} "items" ${unwrappedContent === 1 ? "entry" : "entries"} carrying a "content" field into their task strings. "items" holds only task strings.`);
148
+ }
149
+ const withoutEmptyPhases = list.filter((entry) => {
150
+ if (!isRecord(entry))
151
+ return true;
152
+ if (typeof entry.phase !== "string" && !("items" in entry))
153
+ return true;
154
+ return entry.items !== undefined && (!Array.isArray(entry.items) || entry.items.length > 0);
155
+ });
156
+ if (withoutEmptyPhases.length !== list.length) {
157
+ changed = true;
158
+ const dropped = list.length - withoutEmptyPhases.length;
159
+ notices.push(`[auto-corrected] dropped ${dropped} "list" ${dropped === 1 ? "entry" : "entries"} with no tasks. Every phase needs at least one task in "items".`);
160
+ list = withoutEmptyPhases;
161
+ }
162
+ const hasObjectEntries = list.some((entry) => isRecord(entry));
163
+ const strayStrings = list.filter((entry) => typeof entry === "string");
164
+ if (strayStrings.length > 0 && hasObjectEntries) {
165
+ changed = true;
166
+ notices.push(`[auto-corrected] dropped ${strayStrings.length} stray ${strayStrings.length === 1 ? "entry" : "entries"} from "list". "list" holds only phase objects: {"phase":"<name>","items":["<task>", ...]}`);
167
+ list = list.filter((entry) => typeof entry !== "string");
168
+ }
169
+ const usedNames = new Set();
170
+ for (const entry of list) {
171
+ if (isRecord(entry) && typeof entry.phase === "string" && entry.phase.trim() !== "")
172
+ usedNames.add(entry.phase);
173
+ }
174
+ const generatedNames = [];
175
+ for (let index = 0; index < list.length; index++) {
176
+ const entry = list[index];
177
+ if (!isRecord(entry) ||
178
+ !Array.isArray(entry.items) ||
179
+ entry.items.length === 0 ||
180
+ !entry.items.every((item) => typeof item === "string") ||
181
+ (typeof entry.phase === "string" && entry.phase.trim() !== "")) {
182
+ continue;
183
+ }
184
+ let counter = 1;
185
+ let name = DEFAULT_INIT_PHASE;
186
+ while (usedNames.has(name)) {
187
+ counter += 1;
188
+ name = `${DEFAULT_INIT_PHASE} ${counter}`;
189
+ }
190
+ usedNames.add(name);
191
+ generatedNames.push(name);
192
+ const { phase: _replaced, ...rest } = entry;
193
+ list[index] = { phase: name, ...rest };
194
+ changed = true;
195
+ }
196
+ if (generatedNames.length > 0) {
197
+ notices.push(`[auto-corrected] named ${generatedNames.length} "list" ${generatedNames.length === 1 ? "entry" : "entries"} without a phase name: ${quoted(generatedNames)}. Pass "phase" explicitly: {"phase":"<name>","items":["<task>", ...]}`);
198
+ }
199
+ return changed ? list : input;
200
+ }
201
+ /**
202
+ * The todo tool's `prepareArguments`: rewrites near-canonical shapes (senpi#5148 wrappers,
203
+ * senpi#5332 list/op corrections) to the canonical one before schema validation. Any call a
204
+ * rule does not correct is returned as the SAME reference, and a correction that would leave
205
+ * `list` empty is abandoned entirely so the malformed call still fails validation instead of
206
+ * clearing the todo list. A caller-sent `TODO_PREPARED_CORRECTIONS_KEY` is never trusted
207
+ * (senpi#5332): it is dropped on a copy before any rule runs, and when that removal is the
208
+ * only change the stripped copy is returned instead of the same reference.
209
+ */
210
+ export function prepareTodoArguments(input) {
211
+ if (!isRecord(input) || Array.isArray(input))
212
+ return input;
213
+ const { [TODO_PREPARED_CORRECTIONS_KEY]: _callerSent, ...stripped } = input;
214
+ const args = TODO_PREPARED_CORRECTIONS_KEY in input ? stripped : input;
215
+ const notices = [];
216
+ let draft;
217
+ const revise = (mutate) => {
218
+ draft ??= { ...args };
219
+ mutate(draft);
220
+ };
221
+ const rawOp = args.op;
222
+ let op = rawOp;
223
+ if (typeof rawOp === "string" && !isOperation(rawOp)) {
224
+ // Only stray leading/trailing characters that are neither letters nor digits are
225
+ // stripped ("view}", " done."); digits survive, so "rm 2" and " 2rm" stay invalid
226
+ // ops, and the middle must already be exactly a known operation ("re-move").
227
+ const stripped = rawOp.replace(/^[^a-z0-9]+|[^a-z0-9]+$/gi, "");
228
+ // A corrected rm/done/drop/start without a non-blank "task"/"phase" would act on every
229
+ // task (senpi#5332 review): leave such a call for validation to reject unchanged.
230
+ const carriesTarget = nonBlank(args.task) !== undefined || nonBlank(args.phase) !== undefined;
231
+ const needsTarget = stripped === "rm" || stripped === "done" || stripped === "drop" || stripped === "start";
232
+ if (isOperation(stripped) && (!needsTarget || carriesTarget)) {
233
+ op = stripped;
234
+ revise((draftArgs) => {
235
+ draftArgs.op = stripped;
236
+ });
237
+ notices.push(`[auto-corrected] "op" ${JSON.stringify(rawOp)} is not a known operation; used "${stripped}" after stripping stray leading or trailing non-alphanumeric characters. Pass "op" exactly: init, start, done, rm, drop, append, view.`);
238
+ }
239
+ }
240
+ // A `list` always means init, so its rules also run when op is missing. The top-level
241
+ // `items` rules do not: an op-less `items` call is only inferred as init against an empty
242
+ // list and as append with a phase, so promoting or unwrapping it here would bypass those
243
+ // guards (senpi#5332 review). Only an explicit "init" — after the op correction above —
244
+ // may adopt the `items` rules.
245
+ if (op === undefined || op === "init") {
246
+ if ("list" in args) {
247
+ let listValue = args.list;
248
+ if (typeof listValue === "string") {
249
+ try {
250
+ const parsed = JSON.parse(listValue);
251
+ if (Array.isArray(parsed) || isRecord(parsed)) {
252
+ listValue = parsed;
253
+ notices.push('[auto-corrected] "list" was sent as a JSON string; parsed it. Pass "list" as an array of phases.');
254
+ }
255
+ }
256
+ catch {
257
+ // Unparseable: leave the string for validation to reject.
258
+ }
259
+ }
260
+ let candidate = unwrapTodoList(listValue);
261
+ if (Array.isArray(candidate)) {
262
+ const normalized = normalizeTodoListEntries(candidate, notices);
263
+ if (normalized === null)
264
+ return args;
265
+ candidate = normalized;
266
+ }
267
+ // A correction must never widen a malformed call into a list clear.
268
+ if (Array.isArray(candidate) && candidate.length === 0)
269
+ return args;
270
+ if (candidate !== args.list) {
271
+ const adopted = candidate;
272
+ revise((draftArgs) => {
273
+ draftArgs.list = adopted;
274
+ });
275
+ }
276
+ }
277
+ }
278
+ if (op === "init" && args.list === undefined && Array.isArray(args.items) && args.items.length > 0) {
279
+ const items = args.items;
280
+ if (items.every(isPhaseRecord)) {
281
+ const normalized = normalizeTodoListEntries([...items], notices);
282
+ if (normalized === null || normalized.length === 0)
283
+ return args;
284
+ const adopted = normalized;
285
+ revise((draftArgs) => {
286
+ draftArgs.list = adopted;
287
+ delete draftArgs.items;
288
+ });
289
+ notices.push('[auto-corrected] "items" held phase records; moved them to "list". Pass phases under "list": {"op":"init","list":[{"phase":"<name>","items":["<task>", ...]}]}');
290
+ }
291
+ else {
292
+ const mapped = items.map((item) => (isContentRecord(item) && !isPhaseRecord(item) ? item.content : item));
293
+ if (mapped.some((item) => typeof item === "string") && mapped.some(isPhaseRecord))
294
+ return args;
295
+ const unwrapped = items.filter((item) => isContentRecord(item) && !isPhaseRecord(item)).length;
296
+ if (unwrapped > 0 && mapped.every((item) => typeof item === "string")) {
297
+ revise((draftArgs) => {
298
+ draftArgs.items = mapped;
299
+ });
300
+ notices.push(`[auto-corrected] unwrapped ${unwrapped} top-level "items" ${unwrapped === 1 ? "entry" : "entries"} carrying a "content" field into their task strings. "items" holds only task strings.`);
301
+ }
302
+ }
303
+ }
304
+ if (!draft)
72
305
  return args;
73
- const unwrapped = unwrapTodoList(args.list);
74
- return unwrapped === args.list ? args : { ...args, list: unwrapped };
306
+ return notices.length > 0 ? { ...draft, [TODO_PREPARED_CORRECTIONS_KEY]: [...notices] } : draft;
307
+ }
308
+ /**
309
+ * Splits the prepare-time correction notices off prepared arguments (see
310
+ * `TODO_PREPARED_CORRECTIONS_KEY`): the arguments without the reserved key, plus the notices
311
+ * to prepend to `normalizeTodoParams`'s own corrections.
312
+ */
313
+ export function splitPreparedCorrections(args) {
314
+ const { [TODO_PREPARED_CORRECTIONS_KEY]: notices, ...rest } = args;
315
+ return {
316
+ args: rest,
317
+ corrections: Array.isArray(notices)
318
+ ? notices.filter((notice) => typeof notice === "string")
319
+ : [],
320
+ };
75
321
  }
76
322
  function nonBlank(value) {
77
323
  return typeof value === "string" && value.trim() !== "" ? value : undefined;
@@ -1,3 +1,3 @@
1
- export declare const TODO_TOOL_DESCRIPTION = "**Tasks referenced by verbatim content string, NEVER an auto-generated ID: no \"task-1\"/\"task-N\" exists. Pass the content text in the task field.**\n\nOn each completion the earliest still-open task (in phase order) auto-promotes to in_progress.\nCompleting tasks out of phase order can move this pointer **back** to an earlier phase. This is expected; completed tasks are never reverted.\n\n## Operations\n\n| op | Required fields | Effect |\n|---|---|---|\n| init | list: [{phase, items: string[]}] | Initialize full list (replaces existing) |\n| init | items: string[] | Flattened single-phase init |\n| start | task | Mark in progress |\n| done | task or phase | Mark completed |\n| drop | task or phase | Mark abandoned |\n| rm | task or phase (optional) | Remove task or phase; omit both to clear |\n| append | phase?, items: string[] | Append tasks to phase; lazily creates phase |\n| view | - | Read-only: echo list |\n\nExample: {\"op\":\"init\",\"list\":[{\"phase\":\"Setup\",\"items\":[\"Survey code\",\"Write tests\"]}]}\n\n## Anatomy\n- **Task content**: 5\u201310 words; what, not how. Unique identifier.\n- **Phase name**: short noun phrase (e.g. Foundation, Auth, Verification). Unique identifier. NEVER prefix 1., A), Phase 1:.\n\n## Rules\n- Mark tasks done immediately after finishing, then re-check the list against the newest user message. Complete phases in order.\n- NEVER end a turn with a todo call as its only tool call - batch it with the real work: init with the first reads/edits, each done/start with the next action.\n- Blocked? append a task to the active phase, or drop.\n- Keep task/phase strings stable once introduced. done/start/drop take the task's EXACT text: copy it verbatim from the latest todo result; when it is lost, view echoes the list.\n\n## When to create a list\n- User explicitly requests one\n- User provides a set of tasks\n- New instructions arrive mid-task (reconcile before acting): keep what they don't conflict with, amend what they contradict, append what they add; replace only on explicit redirect\n\n<critical>\nUser hands you a multi-step plan (phased todo, numbered/bulleted checklist, or \"N bugs/items/tasks\"):\n- You MUST init the list with EVERY item as its own task before working.\n- Enumerate all; NEVER summarize into fewer tasks, sample \"the important ones\", drop items, or track the rest from memory.\n</critical>";
2
- export declare const TASK_MANAGEMENT_SECTION = "\n<Task_Management>\n## Todo Management\n\nA request with three or more distinct steps gets a phased todo before the first edit - investigation, implementation, verification, final report - in the todo tool; its description carries the operations, anatomy, and rules. Mark each item done the moment it finishes and reconcile the list against the newest user message before ending a turn.\n\n## Evidence\n- File edit: inspect the changed files and diagnostics.\n- Build command: require exit code 0.\n- Test run: require passing output, or state the pre-existing failure.\n</Task_Management>\n";
1
+ export declare const TODO_TOOL_DESCRIPTION = "**Tasks referenced by verbatim content string, NEVER an auto-generated ID: no \"task-1\"/\"task-N\" exists. Pass the content text in the task field.**\n\nOn each completion the earliest still-open task (in phase order) auto-promotes to in_progress.\nCompleting tasks out of phase order can move this pointer **back** to an earlier phase. This is expected; completed tasks are never reverted.\n\n## Operations\n\n| op | Required fields | Effect |\n|---|---|---|\n| init | list: [{phase, items: string[]}] | Initialize full list (replaces existing). Each list entry is exactly one phase; items holds only task strings |\n| init | items: string[] | Flattened single-phase init |\n| start | task | Mark in progress |\n| done | task or phase | Mark completed |\n| drop | task or phase | Mark abandoned |\n| rm | task or phase (optional) | Remove task or phase; omit both to clear |\n| append | phase?, items: string[] | Append tasks to phase; lazily creates phase |\n| view | - | Read-only: echo list |\n\nExample: {\"op\":\"init\",\"list\":[{\"phase\":\"Setup\",\"items\":[\"Survey code\",\"Write tests\"]}]}\n\n## Anatomy\n- **Task content**: 5\u201310 words; what, not how. Unique identifier.\n- **Phase name**: short noun phrase (e.g. Foundation, Auth, Verification). Unique identifier. NEVER prefix 1., A), Phase 1:.\n\n## Rules\n- Mark tasks done immediately after finishing, then re-check the list against the newest user message. Complete phases in order.\n- NEVER end a turn with a todo call as its only tool call - batch it with the real work: init with the first reads/edits, each done/start with the next action.\n- Blocked? append a task to the active phase, or drop.\n- Keep task/phase strings stable once introduced. done/start/drop take the task's EXACT text: copy it verbatim from the latest todo result; when it is lost, view echoes the list.\n\n## When to create a list\n- User explicitly requests one\n- User provides a set of tasks\n- New instructions arrive mid-task (reconcile before acting): keep what they don't conflict with, amend what they contradict, append what they add; replace only on explicit redirect\n\n<critical>\nUser hands you a multi-step plan (phased todo, numbered/bulleted checklist, or \"N bugs/items/tasks\"):\n- You MUST init the list with EVERY item as its own task before working.\n- Enumerate all; NEVER summarize into fewer tasks, sample \"the important ones\", drop items, or track the rest from memory.\n</critical>";
2
+ export declare const TASK_MANAGEMENT_SECTION = "\n<Task_Management>\n## Todo Management\n\nRequest with 3+ distinct steps -> phased todo in the todo tool before first edit: investigation, implementation, verification, final report.\n\n## Evidence\n- File edit: inspect changed files + diagnostics.\n- Build: exit code 0.\n- Test run: passing output, or state the pre-existing failure.\n</Task_Management>\n";
3
3
  //# sourceMappingURL=prompt.d.ts.map
@@ -11,7 +11,7 @@ Completing tasks out of phase order can move this pointer **back** to an earlier
11
11
 
12
12
  | op | Required fields | Effect |
13
13
  |---|---|---|
14
- | init | list: [{phase, items: string[]}] | Initialize full list (replaces existing) |
14
+ | init | list: [{phase, items: string[]}] | Initialize full list (replaces existing). Each list entry is exactly one phase; items holds only task strings |
15
15
  | init | items: string[] | Flattened single-phase init |
16
16
  | start | task | Mark in progress |
17
17
  | done | task or phase | Mark completed |
@@ -46,12 +46,12 @@ export const TASK_MANAGEMENT_SECTION = `
46
46
  <Task_Management>
47
47
  ## Todo Management
48
48
 
49
- A request with three or more distinct steps gets a phased todo before the first edit - investigation, implementation, verification, final report - in the todo tool; its description carries the operations, anatomy, and rules. Mark each item done the moment it finishes and reconcile the list against the newest user message before ending a turn.
49
+ Request with 3+ distinct steps -> phased todo in the todo tool before first edit: investigation, implementation, verification, final report.
50
50
 
51
51
  ## Evidence
52
- - File edit: inspect the changed files and diagnostics.
53
- - Build command: require exit code 0.
54
- - Test run: require passing output, or state the pre-existing failure.
52
+ - File edit: inspect changed files + diagnostics.
53
+ - Build: exit code 0.
54
+ - Test run: passing output, or state the pre-existing failure.
55
55
  </Task_Management>
56
56
  `;
57
57
  //# sourceMappingURL=prompt.js.map
@@ -6,7 +6,7 @@ import { Text } from "@earendil-works/pi-tui";
6
6
  import { Type } from "typebox";
7
7
  import { partialStrikethrough, strikeRevealCount } from "../../../../../modes/interactive/components/todo-strike.js";
8
8
  import { resolvePromptSurface } from "../../../../dynamic-prompt/build.js";
9
- import { normalizeTodoParams, prepareTodoArguments } from "../normalize.js";
9
+ import { normalizeTodoParams, prepareTodoArguments, splitPreparedCorrections } from "../normalize.js";
10
10
  import { TODO_TOOL_DESCRIPTION } from "../prompt.js";
11
11
  import { applyParams, captureListAsk, clonePhases, findTaskByContent, formatHandoffCue, formatSummary, getCompletionTransitions, getTodoMarker, handoffMomentOf, nextActionableTask, sanitizeTodoText, TODO_STATE_ENTRY_TYPE, } from "../state.js";
12
12
  const TodoOperationSchema = Type.Union([
@@ -212,24 +212,25 @@ export function registerTodoTool(pi, accessors) {
212
212
  label: "Todo",
213
213
  description: TODO_TOOL_DESCRIPTION,
214
214
  promptSnippet: "Track phased tasks with one op-based todo tool; reference tasks by their exact content.",
215
- promptGuidelines: [
216
- "Use one todo operation at a time, batched with the real work; never end a turn with a todo call as its only tool call.",
217
- "Reference tasks and phases by their exact content/name; use view when the text is uncertain.",
218
- "Mark work done immediately and use drop for tasks that are no longer needed.",
219
- ],
220
215
  parameters: TODO_PARAMS_SCHEMA,
221
216
  prepareArguments: (args) => prepareTodoArguments(args),
222
217
  executionMode: "sequential",
223
218
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
224
219
  const previousPhases = clonePhases(accessors.getCurrentPhases());
225
220
  const previousAsk = accessors.getCurrentAsk();
226
- const normalized = normalizeTodoParams(params, previousPhases);
221
+ // prepareTodoArguments carries its notices on the prepared args under a reserved key
222
+ // (#5332). The key is stripped here before normalizing and is never persisted or
223
+ // rendered, but the prepared arguments — key included — flow to the agent loop's
224
+ // beforeToolCall/afterToolCall hooks, so extension tool_call/tool_result hooks may
225
+ // observe it on the arguments they receive.
226
+ const prepared = splitPreparedCorrections(params);
227
+ const normalized = normalizeTodoParams(prepared.args, previousPhases);
227
228
  if (normalized.error || !normalized.entry) {
228
229
  const error = normalized.error ?? 'Missing "op". Example: {"op":"init","list":[{"phase":"Setup","items":["..."]}]}';
229
230
  throw new Error(`${error}\n\n${formatSummary(previousPhases, [], true, previousAsk)}`);
230
231
  }
231
232
  const entry = normalized.entry;
232
- const corrections = normalized.corrections;
233
+ const corrections = [...prepared.corrections, ...normalized.corrections];
233
234
  const readOnly = entry.op === "view";
234
235
  const errors = [];
235
236
  const applied = readOnly
@@ -19,11 +19,6 @@ export const webfetch = defineTool({
19
19
  description: "Fetches content from a URL and returns it as markdown, plain text, or HTML. " +
20
20
  "Network use is bounded by timeout and response size limits.",
21
21
  promptSnippet: "webfetch: retrieve URL content as markdown, text, or html",
22
- promptGuidelines: [
23
- "Use webfetch when a specific URL must be retrieved.",
24
- "Prefer markdown format unless raw HTML or plain text is explicitly needed.",
25
- "The tool is read-only and does not modify files.",
26
- ],
27
22
  parameters: Params,
28
23
  async execute(_toolCallId, params, signal, onUpdate, _ctx) {
29
24
  const format = parseWebfetchFormat(params.format);
@@ -8,7 +8,7 @@ import { loadWebsearchConfig } from "./websearch/config.js";
8
8
  import { attemptRouteLabel } from "./websearch/route-attempts.js";
9
9
  import { providerEntryLabel } from "./websearch/search.js";
10
10
  import { resolveNativeSearchModel } from "./websearch/search-model.js";
11
- import { createWebSearchTool } from "./websearch/tool.js";
11
+ import { createWebSearchTool, WEB_SEARCH_CITE_GUIDELINE } from "./websearch/tool.js";
12
12
  const STATUS_KEY = "pi-websearch";
13
13
  const WIDGET_KEY = "pi-websearch";
14
14
  const NATIVE_BYPASS_MESSAGE = "Native provider web search is handled by the built-in provider extension.";
@@ -127,13 +127,18 @@ export default function (pi) {
127
127
  }
128
128
  if (!hasServerSearch(ctx.model))
129
129
  return undefined;
130
- const systemPrompt = event.systemPrompt
130
+ const failed = hasServerSearchFailed(ctx.sessionManager, ctx.model);
131
+ const withoutSections = event.systemPrompt
131
132
  .replace(ANTHROPIC_WEB_SEARCH_SECTION, "")
132
133
  .replace(OPENAI_WEB_SEARCH_SECTION, "");
134
+ // Server search: the section below carries the cite rule, so the tool's guideline bullet goes.
135
+ const systemPrompt = failed
136
+ ? withoutSections
137
+ : withoutSections.replace(`- ${WEB_SEARCH_CITE_GUIDELINE}\n`, "");
133
138
  return {
134
- systemPrompt: `${systemPrompt}\n## Web Search\n${hasServerSearchFailed(ctx.sessionManager, ctx.model)
139
+ systemPrompt: `${systemPrompt}\n## Web Search\n${failed
135
140
  ? "Use the standalone web_search tool when current information is needed. Native search failed for this session model."
136
- : "Use the provider's in-turn server web search when current information is needed."}`,
141
+ : "web_search = provider server-side search. Use when current info needed; cite returned URLs in final answer."}`,
137
142
  };
138
143
  }, { previewSafe: true });
139
144
  pi.on("before_provider_request", (event, ctx) => {
@@ -12,6 +12,8 @@ type WebSearchTool = ReturnType<typeof defineTool<typeof Params, SearchRenderDet
12
12
  export interface WebSearchToolOptions {
13
13
  onSearchComplete?: (details: SearchDetails) => void;
14
14
  }
15
+ /** The one Tool Guidelines bullet; the server-search `## Web Search` section states it instead (senpi#5335). */
16
+ export declare const WEB_SEARCH_CITE_GUIDELINE = "web_search used -> cite returned URLs in final answer.";
15
17
  export declare function createWebSearchTool(getConfig: ConfigProvider, options?: WebSearchToolOptions): WebSearchTool;
16
18
  export declare const web_search: import("../../../types.ts").ToolDefinition<Type.TObject<{
17
19
  query: Type.TString;
@@ -40,6 +40,8 @@ function formatSearchProgressText(details) {
40
40
  function searchErrorDetails(query, error, reason) {
41
41
  return { phase: "error", query, error, ...(reason ? { reason } : {}) };
42
42
  }
43
+ /** The one Tool Guidelines bullet; the server-search `## Web Search` section states it instead (senpi#5335). */
44
+ export const WEB_SEARCH_CITE_GUIDELINE = "web_search used -> cite returned URLs in final answer.";
43
45
  export function createWebSearchTool(getConfig, options = {}) {
44
46
  let routingState;
45
47
  let routingKey = "";
@@ -49,7 +51,7 @@ export function createWebSearchTool(getConfig, options = {}) {
49
51
  label: "Web Search",
50
52
  description: "Search the web for current information and return source URLs for citation.",
51
53
  promptSnippet: "Search the web for current information, documentation, news, or external facts.",
52
- promptGuidelines: ["After using web_search, cite relevant returned URLs in the final answer."],
54
+ promptGuidelines: [WEB_SEARCH_CITE_GUIDELINE],
53
55
  parameters: Params,
54
56
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
55
57
  if (params.allowed_domains?.length && params.blocked_domains?.length) {
@@ -946,7 +946,7 @@ export class ModelRuntime {
946
946
  return this.enqueueCredentialOperation(providerId, signal, async () => {
947
947
  const store = this.credentials.backingStore;
948
948
  const before = await store.read(providerId, { signal });
949
- const credential = await this.models.login(providerId, type, { ...interaction, signal }, options);
949
+ const credential = await this.models.login(providerId, type, { ...interaction, signal }, { ...options, appName: APP_NAME });
950
950
  if (this.poolStatePath !== undefined) {
951
951
  const { retireLoginSlotHealth } = await import("./credential-pool/login-health.js");
952
952
  await retireLoginSlotHealth(this.poolStatePath, providerId, before, await store.read(providerId, { signal }));
@@ -1,5 +1,6 @@
1
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
2
  import { type Api, type AssistantMessage, type Model, type ProviderDiagnostic, type ProviderDiagnosticCategory } from "@earendil-works/pi-ai";
3
+ import { type TransportTrustFailure } from "../engine-trust/transport-failure.ts";
3
4
  import { type FallbackModelLookup } from "./retry-fallback/chains.ts";
4
5
  /**
5
6
  * `request_refused`: an `auth` failure on a credential that served a response moments before, so the
@@ -33,6 +34,11 @@ export interface ProviderFailure {
33
34
  readonly resetsAt?: number;
34
35
  readonly reason: string;
35
36
  readonly action: string;
37
+ /**
38
+ * Present when the request failed TLS chain verification while the engine runs on a fallback trust
39
+ * generation: the fallback reason and its repair action id (senpi#5244).
40
+ */
41
+ readonly trust?: TransportTrustFailure;
36
42
  }
37
43
  /** An `auth` failure on a credential that served a response moments before: the request was refused, not the key. */
38
44
  export declare function isRequestLevelRefusal(message: AssistantMessage): boolean;
@@ -1,4 +1,5 @@
1
1
  import { PROVIDER_NOT_CONFIGURED_PREFIX, sanitizeProviderDiagnostic, stripTurnRetrySuppressionPrefix, } from "@earendil-works/pi-ai";
2
+ import { transportTrustFailure } from "../engine-trust/transport-failure.js";
2
3
  import { parseFallbackSelector } from "./retry-fallback/chains.js";
3
4
  const ACTIONS = {
4
5
  auth: { reason: "the provider rejected the credentials", action: "re-authenticate the provider" },
@@ -82,11 +83,14 @@ export function providerFailureOf(messages, selected) {
82
83
  const last = messages.findLast((message) => message.role === "assistant");
83
84
  if (last === undefined || last.role !== "assistant" || last.stopReason !== "error")
84
85
  return undefined;
85
- const failure = notConfiguredFailure(last) ??
86
- describeProviderFailure(sanitizeProviderDiagnostic(last.providerDiagnostic), last.provider, {
86
+ const diagnostic = sanitizeProviderDiagnostic(last.providerDiagnostic);
87
+ const described = notConfiguredFailure(last) ??
88
+ describeProviderFailure(diagnostic, last.provider, {
87
89
  model: last.model,
88
90
  requestRefused: isRequestLevelRefusal(last),
89
91
  });
92
+ const trust = transportTrustFailure(diagnostic);
93
+ const failure = trust === undefined ? described : { ...described, trust };
90
94
  if (selected === undefined || (selected.provider === last.provider && selected.model === last.model)) {
91
95
  return failure;
92
96
  }
@@ -0,0 +1,18 @@
1
+ import type { FallbackReason } from "./controller-types.ts";
2
+ /** One chain rung that failed during a turn's fallback walk: the model it ran on and its provider error. */
3
+ export interface FallbackRungFailure {
4
+ model: string;
5
+ reason: FallbackReason;
6
+ error: string;
7
+ }
8
+ /** Appends a rung failure unless it repeats the last one (one failure can be offered to the walk twice). */
9
+ export declare function recordRungFailure(failures: FallbackRungFailure[], failure: FallbackRungFailure): void;
10
+ /**
11
+ * The exhaustion text every surface shows: the failure that started the walk first, then each later rung
12
+ * (senpi#4084). Without recorded rungs it falls back to the last error, as older events carried only that.
13
+ */
14
+ export declare function describeFallbackExhaustion(event: {
15
+ lastError: string;
16
+ failures?: readonly FallbackRungFailure[];
17
+ }): string;
18
+ //# sourceMappingURL=exhaustion.d.ts.map
@@ -0,0 +1,20 @@
1
+ /** Appends a rung failure unless it repeats the last one (one failure can be offered to the walk twice). */
2
+ export function recordRungFailure(failures, failure) {
3
+ const last = failures.at(-1);
4
+ if (last && last.model === failure.model && last.error === failure.error)
5
+ return;
6
+ failures.push(failure);
7
+ }
8
+ /**
9
+ * The exhaustion text every surface shows: the failure that started the walk first, then each later rung
10
+ * (senpi#4084). Without recorded rungs it falls back to the last error, as older events carried only that.
11
+ */
12
+ export function describeFallbackExhaustion(event) {
13
+ const failures = event.failures ?? [];
14
+ if (failures.length === 0)
15
+ return event.lastError;
16
+ return failures
17
+ .map((failure, index) => `${index === 0 ? "" : "then "}${failure.model}: ${failure.error}`)
18
+ .join("\n");
19
+ }
20
+ //# sourceMappingURL=exhaustion.js.map
@@ -6,7 +6,7 @@ import { formatSkillsForPrompt } from "./skills.js";
6
6
  /** Contributions include eval-only tools even when the selected model-facing list does not. */
7
7
  export function getEvalOnlyGrepGuideline(selectedTools, toolSnippets) {
8
8
  return toolSnippets?.grep && !selectedTools.includes("grep")
9
- ? "Search file contents with tool.grep({ pattern, path }) inside eval; prefer it over rg/grep in shell"
9
+ ? "Content search: tool.grep({ pattern, path }) in eval, not rg/grep in shell; page full -> skip, narrow with glob/path."
10
10
  : undefined;
11
11
  }
12
12
  const SYSTEM_PROMPT_SECTION_NAME = /^[a-z][a-z0-9_-]*$/;
@@ -1,5 +1,6 @@
1
1
  import type { AgentTool } from "@earendil-works/pi-agent-core";
2
2
  import { type Static, Type } from "typebox";
3
+ import { type ShellTrust } from "../../engine-trust/shell-trust.ts";
3
4
  import { type ShellConfig } from "../../utils/shell.ts";
4
5
  import type { ToolDefinition } from "../extensions/types.ts";
5
6
  import { type TruncationResult } from "./truncate.ts";
@@ -9,7 +10,7 @@ declare const bashSchema: Type.TObject<{
9
10
  }>;
10
11
  export declare const bashToolSystemPromptContribution: {
11
12
  readonly snippet: "Execute bash commands (ls, rg, find, etc.)";
12
- readonly guidelines: readonly ["You can inspect PI_* environment variables for current model and session details."];
13
+ readonly guidelines: readonly ["PI_* env vars = current model + session details."];
13
14
  };
14
15
  export type BashToolInput = Static<typeof bashSchema>;
15
16
  /**
@@ -27,6 +28,7 @@ export type BashToolOutput = Static<typeof bashOutputSchema>;
27
28
  export interface BashToolDetails {
28
29
  truncation?: TruncationResult;
29
30
  fullOutputPath?: string;
31
+ trust?: ShellTrust;
30
32
  }
31
33
  /**
32
34
  * Pluggable operations for the bash tool.
@@ -4,6 +4,7 @@ import { access as fsAccess } from "node:fs/promises";
4
4
  import { constants as osConstants } from "node:os";
5
5
  import { spawn } from "child_process";
6
6
  import { Type } from "typebox";
7
+ import { annotateShellTrust } from "../../engine-trust/shell-trust.js";
7
8
  import { waitForChildProcess } from "../../utils/child-process.js";
8
9
  import { getShellConfig, getShellEnv, killProcessTree, noteDetachedChildExited, pruneTrackedDetachedChildren, trackDetachedChildPid, } from "../../utils/shell.js";
9
10
  import { modelOnlyText } from "./model-only-text.js";
@@ -34,7 +35,7 @@ const bashSchema = Type.Object({
34
35
  });
35
36
  export const bashToolSystemPromptContribution = {
36
37
  snippet: "Execute bash commands (ls, rg, find, etc.)",
37
- guidelines: ["You can inspect PI_* environment variables for current model and session details."],
38
+ guidelines: ["PI_* env vars = current model + session details."],
38
39
  };
39
40
  /**
40
41
  * Result for programmatic callers such as codemode scripts. A non-zero exit code is an error result for the model, but scripts still resolve to this value.
@@ -432,10 +433,19 @@ export function createShellToolDefinition(cwd, config, options) {
432
433
  wall_time_seconds: wallTimeSeconds,
433
434
  };
434
435
  if (exitCode !== 0) {
436
+ const text = appendStatus(outputText, `Command exited with code ${exitCode}`);
437
+ // The engine trust annotation (omo#10762) rides only on error results, so the success
438
+ // `outputSchema` (and the prompt bytes it costs) stays unchanged.
439
+ const annotation = annotateShellTrust({ exitCode, output: fullOutput.content });
440
+ if (annotation === undefined) {
441
+ return { content: [{ type: "text", text }], details, structuredContent, isError: true };
442
+ }
435
443
  return {
436
- content: [{ type: "text", text: appendStatus(outputText, `Command exited with code ${exitCode}`) }],
437
- details,
438
- structuredContent,
444
+ content: [
445
+ { type: "text", text: annotation.text === undefined ? text : `${text}\n\n${annotation.text}` },
446
+ ],
447
+ details: { ...details, trust: annotation.trust },
448
+ structuredContent: { ...structuredContent, trust: annotation.trust },
439
449
  isError: true,
440
450
  };
441
451
  }
@@ -11,7 +11,7 @@ declare const editSchema: Type.TObject<{
11
11
  }>;
12
12
  export declare const editToolSystemPromptContribution: {
13
13
  readonly snippet: "Make precise file edits with exact text replacement, including multiple disjoint edits in one call";
14
- readonly guidelines: readonly ["Use edit for precise changes (edits[].oldText must match exactly)", "When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls", "Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.", "Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions."];
14
+ readonly guidelines: readonly ["All changes to one file -> one edit call."];
15
15
  };
16
16
  export type EditToolInput = Static<typeof editSchema>;
17
17
  export interface EditToolDetails {
@@ -22,12 +22,7 @@ const editSchema = Type.Object({
22
22
  }, {});
23
23
  export const editToolSystemPromptContribution = {
24
24
  snippet: "Make precise file edits with exact text replacement, including multiple disjoint edits in one call",
25
- guidelines: [
26
- "Use edit for precise changes (edits[].oldText must match exactly)",
27
- "When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls",
28
- "Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.",
29
- "Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.",
30
- ],
25
+ guidelines: ["All changes to one file -> one edit call."],
31
26
  };
32
27
  function isSingleEditInput(value) {
33
28
  if (!value || typeof value !== "object" || Array.isArray(value)) {