@code-yeongyu/senpi 2026.8.9 → 2026.8.10

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 (174) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/core/agent-session.d.ts +4 -0
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +19 -2
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/extensions/builtin/cache-keepalive/index.d.ts +30 -0
  7. package/dist/core/extensions/builtin/cache-keepalive/index.d.ts.map +1 -0
  8. package/dist/core/extensions/builtin/cache-keepalive/index.js +281 -0
  9. package/dist/core/extensions/builtin/cache-keepalive/index.js.map +1 -0
  10. package/dist/core/extensions/builtin/compaction/index.js +9 -9
  11. package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
  12. package/dist/core/extensions/builtin/goal/cache-warm-renderer.d.ts.map +1 -1
  13. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js +21 -8
  14. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js.map +1 -1
  15. package/dist/core/extensions/builtin/goal/cache-warm.d.ts +11 -2
  16. package/dist/core/extensions/builtin/goal/cache-warm.d.ts.map +1 -1
  17. package/dist/core/extensions/builtin/goal/cache-warm.js +16 -0
  18. package/dist/core/extensions/builtin/goal/cache-warm.js.map +1 -1
  19. package/dist/core/extensions/builtin/goal/index.d.ts.map +1 -1
  20. package/dist/core/extensions/builtin/goal/index.js +31 -1
  21. package/dist/core/extensions/builtin/goal/index.js.map +1 -1
  22. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts +2 -1
  23. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts.map +1 -1
  24. package/dist/core/extensions/builtin/goal/monitor-continuation.js +115 -54
  25. package/dist/core/extensions/builtin/goal/monitor-continuation.js.map +1 -1
  26. package/dist/core/extensions/builtin/goal/prompt.d.ts.map +1 -1
  27. package/dist/core/extensions/builtin/goal/prompt.js +4 -4
  28. package/dist/core/extensions/builtin/goal/prompt.js.map +1 -1
  29. package/dist/core/extensions/builtin/goal/store-changed-event.d.ts +8 -0
  30. package/dist/core/extensions/builtin/goal/store-changed-event.d.ts.map +1 -0
  31. package/dist/core/extensions/builtin/goal/store-changed-event.js +9 -0
  32. package/dist/core/extensions/builtin/goal/store-changed-event.js.map +1 -0
  33. package/dist/core/extensions/builtin/goal/wait-progress.js +5 -5
  34. package/dist/core/extensions/builtin/goal/wait-progress.js.map +1 -1
  35. package/dist/core/extensions/builtin/index.d.ts.map +1 -1
  36. package/dist/core/extensions/builtin/index.js +2 -0
  37. package/dist/core/extensions/builtin/index.js.map +1 -1
  38. package/dist/core/extensions/builtin/monitor-state-event.d.ts +16 -0
  39. package/dist/core/extensions/builtin/monitor-state-event.d.ts.map +1 -1
  40. package/dist/core/extensions/builtin/monitor-state-event.js +15 -0
  41. package/dist/core/extensions/builtin/monitor-state-event.js.map +1 -1
  42. package/dist/core/extensions/builtin/terminal/extension.d.ts.map +1 -1
  43. package/dist/core/extensions/builtin/terminal/extension.js +7 -8
  44. package/dist/core/extensions/builtin/terminal/extension.js.map +1 -1
  45. package/dist/core/extensions/builtin/terminal/session-bundle.d.ts +3 -3
  46. package/dist/core/extensions/builtin/terminal/session-bundle.d.ts.map +1 -1
  47. package/dist/core/extensions/builtin/terminal/session-bundle.js.map +1 -1
  48. package/dist/core/extensions/index.d.ts +1 -1
  49. package/dist/core/extensions/index.d.ts.map +1 -1
  50. package/dist/core/extensions/index.js.map +1 -1
  51. package/dist/core/extensions/loader.d.ts.map +1 -1
  52. package/dist/core/extensions/loader.js +7 -0
  53. package/dist/core/extensions/loader.js.map +1 -1
  54. package/dist/core/extensions/runner.d.ts +4 -0
  55. package/dist/core/extensions/runner.d.ts.map +1 -1
  56. package/dist/core/extensions/runner.js +19 -2
  57. package/dist/core/extensions/runner.js.map +1 -1
  58. package/dist/core/extensions/types.d.ts +50 -0
  59. package/dist/core/extensions/types.d.ts.map +1 -1
  60. package/dist/core/extensions/types.js.map +1 -1
  61. package/dist/core/omo-native-detect.d.ts +1 -1
  62. package/dist/core/omo-native-detect.d.ts.map +1 -1
  63. package/dist/core/omo-native-detect.js +7 -1
  64. package/dist/core/omo-native-detect.js.map +1 -1
  65. package/dist/core/omo-native-detect.test.d.ts +2 -0
  66. package/dist/core/omo-native-detect.test.d.ts.map +1 -0
  67. package/dist/core/omo-native-detect.test.js +47 -0
  68. package/dist/core/omo-native-detect.test.js.map +1 -0
  69. package/dist/core/resource-loader.d.ts +4 -0
  70. package/dist/core/resource-loader.d.ts.map +1 -1
  71. package/dist/core/resource-loader.js +6 -0
  72. package/dist/core/resource-loader.js.map +1 -1
  73. package/dist/core/session-manager.d.ts +1 -1
  74. package/dist/core/session-manager.d.ts.map +1 -1
  75. package/dist/core/session-manager.js.map +1 -1
  76. package/dist/core/settings-manager.d.ts +10 -0
  77. package/dist/core/settings-manager.d.ts.map +1 -1
  78. package/dist/core/settings-manager.js +14 -0
  79. package/dist/core/settings-manager.js.map +1 -1
  80. package/dist/core/tools/edit.d.ts +3 -1
  81. package/dist/core/tools/edit.d.ts.map +1 -1
  82. package/dist/core/tools/edit.js +12 -0
  83. package/dist/core/tools/edit.js.map +1 -1
  84. package/dist/core/tools/filesystem-policy.d.ts +10 -0
  85. package/dist/core/tools/filesystem-policy.d.ts.map +1 -0
  86. package/dist/core/tools/filesystem-policy.js +59 -0
  87. package/dist/core/tools/filesystem-policy.js.map +1 -0
  88. package/dist/core/tools/find.d.ts +3 -1
  89. package/dist/core/tools/find.d.ts.map +1 -1
  90. package/dist/core/tools/find.js +15 -0
  91. package/dist/core/tools/find.js.map +1 -1
  92. package/dist/core/tools/grep.d.ts +3 -1
  93. package/dist/core/tools/grep.d.ts.map +1 -1
  94. package/dist/core/tools/grep.js +16 -1
  95. package/dist/core/tools/grep.js.map +1 -1
  96. package/dist/core/tools/ls.d.ts +3 -1
  97. package/dist/core/tools/ls.d.ts.map +1 -1
  98. package/dist/core/tools/ls.js +11 -0
  99. package/dist/core/tools/ls.js.map +1 -1
  100. package/dist/core/tools/read.d.ts +3 -1
  101. package/dist/core/tools/read.d.ts.map +1 -1
  102. package/dist/core/tools/read.js +13 -0
  103. package/dist/core/tools/read.js.map +1 -1
  104. package/dist/core/tools/write.d.ts +3 -1
  105. package/dist/core/tools/write.d.ts.map +1 -1
  106. package/dist/core/tools/write.js +12 -0
  107. package/dist/core/tools/write.js.map +1 -1
  108. package/dist/index.d.ts +1 -1
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js.map +1 -1
  111. package/dist/modes/app-server/threads/goal-handlers.d.ts.map +1 -1
  112. package/dist/modes/app-server/threads/goal-handlers.js +7 -0
  113. package/dist/modes/app-server/threads/goal-handlers.js.map +1 -1
  114. package/dist/modes/interactive/components/footer-omo-native-badge.test.d.ts +2 -0
  115. package/dist/modes/interactive/components/footer-omo-native-badge.test.d.ts.map +1 -0
  116. package/dist/modes/interactive/components/footer-omo-native-badge.test.js +52 -0
  117. package/dist/modes/interactive/components/footer-omo-native-badge.test.js.map +1 -0
  118. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  119. package/dist/modes/interactive/components/footer.js +3 -1
  120. package/dist/modes/interactive/components/footer.js.map +1 -1
  121. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  122. package/dist/modes/interactive/interactive-mode.js +1 -0
  123. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  124. package/docs/extensions.md +76 -0
  125. package/docs/providers.md +23 -3
  126. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +26 -0
  127. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  128. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/{resumption-channel.ts → wake-source-state.ts} +9 -9
  129. package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +7 -7
  130. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-manager.ts +12 -11
  131. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  132. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts +3 -1
  133. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
  134. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +13 -0
  135. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
  136. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  137. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js +6 -3
  138. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js.map +1 -1
  139. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts.map +1 -1
  140. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +6 -2
  141. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
  142. package/node_modules/@earendil-works/pi-ai/dist/api/warm-prompt-cache.d.ts +17 -0
  143. package/node_modules/@earendil-works/pi-ai/dist/api/warm-prompt-cache.d.ts.map +1 -0
  144. package/node_modules/@earendil-works/pi-ai/dist/api/warm-prompt-cache.js +52 -0
  145. package/node_modules/@earendil-works/pi-ai/dist/api/warm-prompt-cache.js.map +1 -0
  146. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts +2 -1
  147. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts.map +1 -1
  148. package/node_modules/@earendil-works/pi-ai/dist/index.js +2 -1
  149. package/node_modules/@earendil-works/pi-ai/dist/index.js.map +1 -1
  150. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  151. package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai-cn.json +1 -1
  152. package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai.json +1 -1
  153. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  154. package/node_modules/@earendil-works/pi-ai/dist/providers/data/together.json +1 -1
  155. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  156. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/antml/recovery-stream.d.ts.map +1 -1
  157. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/antml/recovery-stream.js +120 -1
  158. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/antml/recovery-stream.js.map +1 -1
  159. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +2 -0
  160. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
  161. package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
  162. package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.d.ts +3 -1
  163. package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.d.ts.map +1 -1
  164. package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.js +17 -3
  165. package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.js.map +1 -1
  166. package/node_modules/@earendil-works/pi-ai/package.json +1 -1
  167. package/node_modules/@earendil-works/pi-pty/native/prebuilds/darwin-arm64/senpi_pty.darwin-arm64.node +0 -0
  168. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  169. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  170. package/package.json +6 -6
  171. package/dist/core/extensions/builtin/resumption-channel-event.d.ts +0 -13
  172. package/dist/core/extensions/builtin/resumption-channel-event.d.ts.map +0 -1
  173. package/dist/core/extensions/builtin/resumption-channel-event.js +0 -13
  174. package/dist/core/extensions/builtin/resumption-channel-event.js.map +0 -1
@@ -1437,6 +1437,82 @@ pi.registerTool({
1437
1437
  });
1438
1438
  ```
1439
1439
 
1440
+ ### pi.registerFilesystemPolicy(policy)
1441
+
1442
+ Register a filesystem access policy for Senpi's built-in `read`, `write`,
1443
+ `edit`, `ls`, `find`, and `grep` tools. This is a **factory-time only** API: call
1444
+ it directly from the extension factory so the checker is compiled before the
1445
+ built-in tools are constructed.
1446
+
1447
+ The policy receives a canonical absolute target:
1448
+
1449
+ ```typescript
1450
+ interface FilesystemPolicyRequest {
1451
+ operation: "read" | "enumerate" | "write";
1452
+ canonicalPath: string;
1453
+ toolName: string;
1454
+ }
1455
+ ```
1456
+
1457
+ Operation mapping:
1458
+
1459
+ | Operation | Built-in tools |
1460
+ |---|---|
1461
+ | `read` | `read` |
1462
+ | `write` | `write`, `edit` |
1463
+ | `enumerate` | `ls`, `find`, `grep` |
1464
+
1465
+ Existing targets are resolved through filesystem symlinks. A missing write
1466
+ target is resolved through its nearest existing real parent, then its missing
1467
+ suffix is reattached. The policy runs after that resolution and immediately
1468
+ before the tool's target I/O. A denial is returned as a normal tool error with
1469
+ the policy's reason.
1470
+
1471
+ ```typescript
1472
+ import { isAbsolute, relative, resolve, sep } from "node:path";
1473
+ import type { ExtensionAPI } from "@code-yeongyu/senpi";
1474
+
1475
+ function isContained(root: string, candidate: string): boolean {
1476
+ const pathFromRoot = relative(root, candidate);
1477
+ return pathFromRoot === "" || (
1478
+ pathFromRoot !== ".." &&
1479
+ !pathFromRoot.startsWith(`..${sep}`) &&
1480
+ !isAbsolute(pathFromRoot)
1481
+ );
1482
+ }
1483
+
1484
+ export default function (pi: ExtensionAPI) {
1485
+ const writableRoot = resolve(process.cwd());
1486
+
1487
+ pi.registerFilesystemPolicy({
1488
+ check({ operation, canonicalPath }) {
1489
+ if (operation !== "write" || isContained(writableRoot, canonicalPath)) {
1490
+ return { allow: true };
1491
+ }
1492
+ return {
1493
+ allow: false,
1494
+ reason: `Writes are limited to ${writableRoot}`,
1495
+ };
1496
+ },
1497
+ });
1498
+ }
1499
+ ```
1500
+
1501
+ Policies run in extension load and registration order. Every policy must allow a
1502
+ request; the first denial wins. A thrown policy error also stops the tool before
1503
+ its target I/O.
1504
+
1505
+ An optional `deniedRoots: readonly string[]` field exposes metadata for future
1506
+ inherited process sandbox support. Use canonical absolute roots. The current
1507
+ runtime does not interpret this metadata and does not apply filesystem policies
1508
+ to `bash` or descendants; command-string rewriting is not a filesystem security
1509
+ boundary.
1510
+
1511
+ This enforcement is separate from `tool_call` permission and approval hooks.
1512
+ An unrestricted approval mode, or a hook that returns `{ block: false }`, cannot
1513
+ bypass the built-in executor check. Custom extension tools are not covered
1514
+ automatically and must call their own policy if they access the filesystem.
1515
+
1440
1516
  ### pi.registerMcpServer(name, config)
1441
1517
 
1442
1518
  Register an MCP server that the agent can use. This is a **factory-time only**
package/docs/providers.md CHANGED
@@ -88,8 +88,18 @@ If your Claude Pro/Max subscription usage through `claude-sdk-oauth` feels unexp
88
88
  1. **Upgrade to v2026.8.3 or later.** Resume-first session continuity (#634-637) landed on 2026-08-03. On older builds, every turn after a divergence (compaction, abort, model switch, restart, failover) re-sends the entire conversation, which is the dominant token-burn mechanism.
89
89
  2. **Check which lane you are on.** The `ambient` lane (default) inherits the environment. `oauth-slots` and `config-dir` are managed lanes set via `SENPI_CLAUDE_SDK_OAUTH_TOKEN_INJECTION`. The `config-dir` lane keeps each account's credentials in its own `CLAUDE_CONFIG_DIR`; no official SDK API moves a transcript across roots, so account failover on that lane always flattens (re-sends the full history) — this is a declared residual, not a bug.
90
90
  3. **Read the continuity observations.** Tail the session log and filter for `flatten` — each `flatten` line means the lane re-sent the whole conversation and lost prompt-cache hits. A healthy conversation shows one `bootstrap` followed by `delta` lines. Common flatten reasons: `transcript_missing`, `registry_miss`, `resume_initialization_failed`, `cross_root_unsupported` (config-dir only).
91
- 4. **Prompt-cache retention.** On the direct Anthropic API path (`anthropic` provider with an OAuth token), cache retention defaults to `long` with a 1-hour TTL on the native endpoint. Set `PI_CACHE_RETENTION=long` to ensure 1h caching. The SDK OAuth lane manages its own caching internally and senpi cannot add `cache_control` breakpoints to it.
92
- 5. **Directive-block deduplication.** As of v2026.8.4, the flatten serialization collapses repeated `<ultrawork-mode>` directive blocks to a single copy, preventing the issue-#494 scenario where duplicated ~17KB directive blocks consumed up to 73% of the re-sent prompt. The continuity observation reports how many were collapsed and the payload size.
91
+ 4. **Prompt-cache retention.** Effective cache TTL depends on which lane you are on:
92
+
93
+ | Lane | Effective TTL | Who controls it | How to override |
94
+ | --- | --- | --- | --- |
95
+ | Claude SDK OAuth (subscription, `claude-sdk-oauth`) | 5 minutes | The Claude SDK owns `cache_control`; senpi cannot add breakpoints. senpi reports 300s for this lane so cache-aware budgets (tool waits, goal timing) size themselves correctly. | Not overridable |
96
+ | Direct Anthropic API (`api.anthropic.com`, API key or OAuth token) | 1 hour | senpi defaults retention to `long` on this lane. Note: Anthropic's own API default is 5 minutes; the 1h TTL is senpi's choice, and 1h cache writes cost 2x base input vs 1.25x for 5m ([Anthropic prompt caching](https://docs.claude.com/en/docs/build-with-claude/prompt-caching)). | `PI_CACHE_RETENTION`, `cacheRetention` |
97
+ | Anthropic-compatible providers (kimi-coding, fireworks, gateways) | 5 minutes | The 1h TTL is gated on the native `api.anthropic.com` base URL, so these lanes stay short. | `cacheRetention` |
98
+
99
+ Override precedence: `cacheRetention` in `models.json` / the model catalog wins over everything. `PI_CACHE_RETENTION=long` selects long; any other set value forces short; unset falls back to the lane default above.
100
+ 5. **Goal-monitor timing.** The goal monitor's continuation backstop is derived from the model's cache-safe wait (TTL minus `promptCache.safetyBufferSeconds`, default 30), capped by `promptCache.goalBackstopMaxSeconds` (default 3570), instead of a fixed 4 minutes. On 5m lanes that means wakes every ~4m30s; on the 1h direct-API lane, up to 59m30s. Cache-warm notices show which warm iteration you are on.
101
+ 6. **Wake sources that hold the goal backstop.** Anything that can wake a parked session publishes a `wake_source_state` event (`{source, activeCount}`): terminal monitors (`terminal-monitors`), background bash sessions including auto-detached and killed ones (`terminal-background-sessions`), detached `eval` cells (`senpi-codemode`), and omo-senpi background task children plus owned team members (`senpi-task`). The goal extension sums every source, so a goal waits inside the prompt-cache TTL while ANY of them is on duty instead of continuing immediately. The legacy `terminal_monitor_state` event is still emitted for external consumers and is folded onto the same `terminal-monitors` count.
102
+ 7. **Directive-block deduplication.** As of v2026.8.4, the flatten serialization collapses repeated `<ultrawork-mode>` directive blocks to a single copy, preventing the issue-#494 scenario where duplicated ~17KB directive blocks consumed up to 73% of the re-sent prompt. The continuity observation reports how many were collapsed and the payload size.
93
103
 
94
104
  ### GitHub Copilot
95
105
 
@@ -108,6 +118,16 @@ If your Claude Pro/Max subscription usage through `claude-sdk-oauth` feels unexp
108
118
  - On remote/headless machines (e.g. over SSH) the browser cannot reach the loopback callback; paste the final redirect URL (or the authorization code) into the login prompt instead
109
119
  - `OPENROUTER_API_KEY` remains available through **Use an API key**
110
120
 
121
+ #### OpenRouter prompt caching and sticky routing
122
+
123
+ senpi pins every OpenRouter request to the same upstream from the first call by sending the session id both as the `x-session-id` header and as the request-body `session_id` field. OpenRouter's precedence is body > header > `prompt_cache_key`, with a 256-character limit on the value; specifying a manual `provider.order` disables sticky routing entirely ([OpenRouter prompt caching](https://openrouter.ai/docs/guides/best-practices/prompt-caching)).
124
+
125
+ Upstreams that require explicit cache breakpoints get `cache_control` blocks through OpenRouter: model ids starting with `anthropic/`, `qwen/`, or `google/` (catalog ids carrying one leading `~` are matched after stripping it). Other upstreams (OpenAI, DeepSeek, Moonshot, Groq, Z.AI, Grok) cache automatically and need no markers.
126
+
127
+ #### Moonshot / Kimi prompt caching
128
+
129
+ senpi sends `prompt_cache_key` (set to the session id) on Moonshot requests. Kimi documents the field as required for the Kimi Code Plan and recommended for any multi-turn agent ([Kimi context caching](https://platform.kimi.ai/docs/guide/use-context-caching-feature-of-kimi-api)). Kimi reports cache hits as a flat `usage.cached_tokens` field, which senpi parses as cache-read tokens.
130
+
111
131
  ### Radius
112
132
 
113
133
  Radius is a dynamic `pi-messages` gateway. `/login radius` stores OAuth tokens in `auth.json`; the gateway catalog is refreshed independently and cached in `models-store.json`. Custom Radius gateways can be declared in `models.json` with `"oauth": "radius"` and a gateway `baseUrl`.
@@ -296,7 +316,7 @@ Also supports ECS task roles (`AWS_CONTAINER_CREDENTIALS_*`) and IRSA (`AWS_WEB_
296
316
  pi --provider amazon-bedrock --model us.anthropic.claude-sonnet-4-20250514-v1:0
297
317
  ```
298
318
 
299
- Prompt caching is enabled automatically for Claude models whose ID contains a recognizable model name (base models and system-defined inference profiles). For application inference profiles (whose ARNs don't contain the model name), set `AWS_BEDROCK_FORCE_CACHE=1` to enable cache points:
319
+ Prompt caching is enabled automatically for Claude models whose ID contains a recognizable model name (base models and system-defined inference profiles). With `PI_CACHE_RETENTION=long`, the 1-hour cache TTL is only requested for Claude Opus 4.5, Sonnet 4.5, and Haiku 4.5, the models AWS documents as supporting it ([Bedrock prompt caching](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html)); every other cacheable Bedrock Claude model uses 5 minutes on both the wire and the TTL estimate. For application inference profiles (whose ARNs don't contain the model name), set `AWS_BEDROCK_FORCE_CACHE=1` to enable cache points:
300
320
 
301
321
  ```bash
302
322
  export AWS_BEDROCK_FORCE_CACHE=1
@@ -12,6 +12,32 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.8.10] - 2026-08-10
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ ### Changed
22
+
23
+ ### Fixed
24
+
25
+ ### Removed
26
+
27
+ ## [2026.8.9-2] - 2026-08-09
28
+
29
+ ### Breaking Changes
30
+
31
+ ### Added
32
+
33
+ ### Changed
34
+
35
+ - Detached eval cells now emit the shared `wake_source_state` event under source `senpi-codemode` when they detach, complete, stop, or are disposed. The optional host event passthrough remains guarded, synchronous cells emit no lifecycle transition, and per-cell snapshot metadata is preserved.
36
+
37
+ ### Fixed
38
+
39
+ ### Removed
40
+
15
41
  ## [2026.8.9] - 2026-08-09
16
42
 
17
43
  ### Breaking Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.8.9",
3
+ "version": "2026.8.10",
4
4
  "description": "Source-only senpi extension package for codemode evaluation tools",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -30,14 +30,14 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@babel/parser": "8.0.4",
33
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.9",
33
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.10",
34
34
  "typebox": "1.3.8"
35
35
  },
36
36
  "peerDependencies": {
37
- "@code-yeongyu/senpi": "2026.8.9"
37
+ "@code-yeongyu/senpi": "2026.8.10"
38
38
  },
39
39
  "devDependencies": {
40
- "@code-yeongyu/senpi": "2026.8.9"
40
+ "@code-yeongyu/senpi": "2026.8.10"
41
41
  },
42
42
  "keywords": [
43
43
  "senpi",
@@ -1,29 +1,29 @@
1
1
  /**
2
- * Resumption-channel liveness contract shared with the goal builtin.
2
+ * Wake-source liveness contract shared with the goal builtin.
3
3
  *
4
- * The goal builtin delays its hidden continuation while any live resumption
5
- * channel can still wake the session. Each emitter publishes a full per-source
4
+ * The goal builtin delays its hidden continuation while any live wake source
5
+ * can still wake the session. Each emitter publishes a full per-source
6
6
  * snapshot on every liveness transition and once on `session_start`.
7
7
  *
8
8
  * The event name is duplicated here on purpose: senpi-codemode is a separate
9
9
  * package and must not import across the packages/coding-agent boundary, so
10
10
  * the sentinel test pins this literal to the exact cross-package contract.
11
11
  */
12
- export const RESUMPTION_CHANNEL_STATE_EVENT = "resumption_channel_state";
12
+ export const WAKE_SOURCE_STATE_EVENT = "wake_source_state";
13
13
 
14
14
  /** Source key identifying detached eval cells in the per-source snapshot. */
15
- export const EVAL_DETACHED_CHANNEL_SOURCE = "eval-detached";
15
+ export const SENPI_CODEMODE_WAKE_SOURCE = "senpi-codemode";
16
16
 
17
- /** One live channel as broadcast on the resumption channel state event. */
18
- export interface ResumptionChannelEntry {
17
+ /** One detached cell as broadcast on the wake-source state event. */
18
+ export interface WakeSourceStateItem {
19
19
  readonly id: string;
20
20
  readonly description: string;
21
21
  /** Epoch milliseconds when the channel registered; lets consumers render their own elapsed labels. */
22
22
  readonly startedAtMs: number;
23
23
  }
24
24
 
25
- export interface ResumptionChannelState {
25
+ export interface WakeSourceState {
26
26
  readonly source: string;
27
27
  readonly activeCount: number;
28
- readonly channels?: readonly ResumptionChannelEntry[];
28
+ readonly items?: readonly WakeSourceStateItem[];
29
29
  }
@@ -7,7 +7,6 @@ import { defaultCodemodeSettings } from "./config/settings.ts";
7
7
  import { EvalNotifier } from "./extension/eval-notifier.ts";
8
8
  import { EVAL_CELLS_STATUS_KEY } from "./extension/eval-status.ts";
9
9
  import { EvalStatusTicker } from "./extension/eval-status-ticker.ts";
10
- import { RESUMPTION_CHANNEL_STATE_EVENT, type ResumptionChannelState } from "./extension/resumption-channel.ts";
11
10
  import {
12
11
  createExecuteTool,
13
12
  createRuntime,
@@ -16,6 +15,7 @@ import {
16
15
  } from "./extension/runtime-factory.ts";
17
16
  import type { CodemodeSessionManager, CreateCodemodeSessionManagerOptions } from "./extension/session-manager.ts";
18
17
  import { SessionManagerProxy } from "./extension/session-manager-proxy.ts";
18
+ import { WAKE_SOURCE_STATE_EVENT, type WakeSourceState } from "./extension/wake-source-state.ts";
19
19
  import { EvalDetachedCellManager, type EvalDetachedCellStatusEntry } from "./tool/detached-cell-manager.ts";
20
20
  import { createEvalTool } from "./tool/eval-tool.ts";
21
21
  import { renderEvalCall, renderEvalResult } from "./tool/render.ts";
@@ -39,7 +39,7 @@ export interface CodemodeExtensionAPI {
39
39
  getActiveTools(): string[];
40
40
  getAllTools(): readonly EvalSchemaToolInfo[];
41
41
  sendUserMessage(content: string, options?: { deliverAs?: "steer" | "followUp" }): void;
42
- /** Optional host event bus; a host without one turns resumption-channel emission into a harmless no-op. */
42
+ /** Optional host event bus; a host without one turns wake-source emission into a harmless no-op. */
43
43
  events?: { emit(name: string, data: unknown): void };
44
44
  }
45
45
 
@@ -82,8 +82,8 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
82
82
  const showDetachedCells = (entries: readonly EvalDetachedCellStatusEntry[]): void => {
83
83
  statusTicker.sync(entries);
84
84
  };
85
- const emitChannelState = (state: ResumptionChannelState): void => {
86
- pi.events?.emit(RESUMPTION_CHANNEL_STATE_EVENT, state);
85
+ const emitWakeSourceState = (state: WakeSourceState): void => {
86
+ pi.events?.emit(WAKE_SOURCE_STATE_EVENT, state);
87
87
  };
88
88
  const registerEvalForRuntime = (
89
89
  runtime: SessionRuntime,
@@ -132,7 +132,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
132
132
  cellManager: new EvalDetachedCellManager({
133
133
  notifier,
134
134
  onStatusChange: showDetachedCells,
135
- onChannelState: emitChannelState,
135
+ onWakeSourceState: emitWakeSourceState,
136
136
  ...(options.now === undefined ? {} : { now: options.now }),
137
137
  }),
138
138
  executionTracker: manager,
@@ -163,12 +163,12 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
163
163
  artifactsDir: runtime.artifactsDir,
164
164
  notifier,
165
165
  onStatusChange: showDetachedCells,
166
- onChannelState: emitChannelState,
166
+ onWakeSourceState: emitWakeSourceState,
167
167
  ...(options.now === undefined ? {} : { now: options.now }),
168
168
  });
169
169
  activeCells = cellManager;
170
170
  // The goal builtin clears its per-session counts at session_start; re-publish our snapshot.
171
- cellManager.publishChannelState();
171
+ cellManager.publishWakeSourceState();
172
172
  activeRuntime = runtime;
173
173
  activeModelId = ctx.model?.id;
174
174
  registerEvalForRuntime(runtime, activeModelId, cellManager);
@@ -1,5 +1,5 @@
1
1
  import type { AgentToolResult } from "@code-yeongyu/senpi";
2
- import { EVAL_DETACHED_CHANNEL_SOURCE, type ResumptionChannelState } from "../extension/resumption-channel.ts";
2
+ import { SENPI_CODEMODE_WAKE_SOURCE, type WakeSourceState } from "../extension/wake-source-state.ts";
3
3
  import { detachedNotificationSpillPath } from "./detached-cell-notification.ts";
4
4
  import { currentDetachedResult, detachedErrorResult, snapshotDetachedCell } from "./detached-cell-snapshot.ts";
5
5
  import {
@@ -60,14 +60,14 @@ export interface EvalDetachedCellManagerOptions {
60
60
  readonly notifier?: EvalDetachedCellNotifier;
61
61
  readonly onStatusChange?: (entries: readonly EvalDetachedCellStatusEntry[]) => void;
62
62
  /** Receives a full per-source liveness snapshot on every detached-cell transition; used by the goal builtin. */
63
- readonly onChannelState?: (state: ResumptionChannelState) => void;
63
+ readonly onWakeSourceState?: (state: WakeSourceState) => void;
64
64
  readonly now?: () => number;
65
65
  }
66
66
 
67
67
  export class EvalDetachedCellManager {
68
68
  readonly #artifactsDir: string | undefined;
69
69
  readonly #onStatusChange: ((entries: readonly EvalDetachedCellStatusEntry[]) => void) | undefined;
70
- readonly #onChannelState: ((state: ResumptionChannelState) => void) | undefined;
70
+ readonly #onWakeSourceState: ((state: WakeSourceState) => void) | undefined;
71
71
  readonly #cells = new Map<string, ManagedCell>();
72
72
  readonly #detachedByLanguage = new Map<EvalLanguage, ManagedCell>();
73
73
  readonly #notificationQueue: DetachedNotificationQueue;
@@ -76,7 +76,7 @@ export class EvalDetachedCellManager {
76
76
  constructor(options: EvalDetachedCellManagerOptions = {}) {
77
77
  this.#artifactsDir = options.artifactsDir;
78
78
  this.#onStatusChange = options.onStatusChange;
79
- this.#onChannelState = options.onChannelState;
79
+ this.#onWakeSourceState = options.onWakeSourceState;
80
80
  this.#notificationQueue = new DetachedNotificationQueue(options.notifier, options.artifactsDir);
81
81
  this.#now = options.now ?? Date.now;
82
82
  }
@@ -160,6 +160,7 @@ export class EvalDetachedCellManager {
160
160
  await Promise.allSettled(
161
161
  detached.map(async (cell) => await this.stop(cell.cellId, "Session ended; detached eval cell cancelled")),
162
162
  );
163
+ if (detached.length === 0) this.#emitWakeSourceState([]);
163
164
  await this.#notificationQueue.flush();
164
165
  }
165
166
 
@@ -168,8 +169,8 @@ export class EvalDetachedCellManager {
168
169
  }
169
170
 
170
171
  /** Re-publish the current snapshot; consumers reset their per-source counts at session_start. */
171
- publishChannelState(): void {
172
- this.#emitChannelState([...this.#detachedByLanguage.values()]);
172
+ publishWakeSourceState(): void {
173
+ this.#emitWakeSourceState([...this.#detachedByLanguage.values()]);
173
174
  }
174
175
 
175
176
  #settle(
@@ -207,14 +208,14 @@ export class EvalDetachedCellManager {
207
208
  ...(cell.input.summary === undefined ? {} : { summary: cell.input.summary }),
208
209
  })),
209
210
  );
210
- this.#emitChannelState(liveCells);
211
+ this.#emitWakeSourceState(liveCells);
211
212
  }
212
213
 
213
- #emitChannelState(liveCells: readonly ManagedCell[]): void {
214
- this.#onChannelState?.({
215
- source: EVAL_DETACHED_CHANNEL_SOURCE,
214
+ #emitWakeSourceState(liveCells: readonly ManagedCell[]): void {
215
+ this.#onWakeSourceState?.({
216
+ source: SENPI_CODEMODE_WAKE_SOURCE,
216
217
  activeCount: liveCells.length,
217
- channels: liveCells.map((cell) => ({
218
+ items: liveCells.map((cell) => ({
218
219
  id: cell.cellId,
219
220
  description:
220
221
  cell.input.summary === undefined || cell.input.summary.length === 0 ? cell.cellId : cell.input.summary,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@earendil-works/pi-agent-core",
3
3
  "private": true,
4
- "version": "2026.8.9",
4
+ "version": "2026.8.10",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -30,7 +30,7 @@
30
30
  "prepublishOnly": "npm run build"
31
31
  },
32
32
  "dependencies": {
33
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.9",
33
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.10",
34
34
  "diff": "9.0.0",
35
35
  "ignore": "7.0.6",
36
36
  "typebox": "1.3.8",
@@ -1,5 +1,6 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
- import type { SimpleStreamOptions, StreamFunction, StreamOptions } from "../types.ts";
2
+ import type { MessageCreateParamsNonStreaming } from "@anthropic-ai/sdk/resources/messages.js";
3
+ import type { Context, Model, SimpleStreamOptions, StreamFunction, StreamOptions } from "../types.ts";
3
4
  export { getAnthropicCompat } from "../utils/prompt-cache-ttl.ts";
4
5
  export type AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max";
5
6
  export type AnthropicThinkingDisplay = "summarized" | "omitted";
@@ -69,4 +70,5 @@ export interface AnthropicOptions extends StreamOptions {
69
70
  }
70
71
  export declare const stream: StreamFunction<"anthropic-messages", AnthropicOptions>;
71
72
  export declare const streamSimple: StreamFunction<"anthropic-messages", SimpleStreamOptions>;
73
+ export declare function buildAnthropicWarmPromptCacheParams(model: Model<"anthropic-messages">, context: Context, options?: AnthropicOptions): MessageCreateParamsNonStreaming;
72
74
  //# sourceMappingURL=anthropic-messages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAU1C,OAAO,KAAK,EAWX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAOb,MAAM,aAAa,CAAC;AAkCrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AA6JlE,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AAoEhE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AA4zBD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CA6ZzE,CAAC;AA0FF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CA2ClF,CAAC"}
1
+ {"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAC1C,OAAO,KAAK,EAGX,+BAA+B,EAK/B,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAKX,OAAO,EAGP,KAAK,EAGL,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAOb,MAAM,aAAa,CAAC;AAkCrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AA6JlE,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AAoEhE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AA4zBD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CA6ZzE,CAAC;AA0FF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CA2ClF,CAAC;AAsIF,wBAAgB,mCAAmC,CAClD,KAAK,EAAE,KAAK,CAAC,oBAAoB,CAAC,EAClC,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,gBAAgB,GACxB,+BAA+B,CAkBjC"}
@@ -1438,6 +1438,19 @@ function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStre
1438
1438
  });
1439
1439
  return { client, isOAuthToken: false };
1440
1440
  }
1441
+ export function buildAnthropicWarmPromptCacheParams(model, context, options) {
1442
+ const params = buildParams(model, context, false, {
1443
+ ...options,
1444
+ maxTokens: 0,
1445
+ thinkingEnabled: undefined,
1446
+ toolChoice: undefined,
1447
+ });
1448
+ const { stream: _stream, thinking: _thinking, output_config: _outputConfig, tool_choice: _toolChoice, ...nonStreaming } = params;
1449
+ return sanitizeAnthropicToolPairs({
1450
+ ...nonStreaming,
1451
+ max_tokens: 0,
1452
+ });
1453
+ }
1441
1454
  function buildParams(model, context, isOAuthToken, options, unsignedThinkingReplay = getAnthropicCompat(model).unsignedThinkingReplay) {
1442
1455
  const compat = getAnthropicCompat(model);
1443
1456
  const { cacheControl } = getCacheControl(model, options?.cacheRetention ?? model.cacheRetention, options?.env);