my-durable 0.0.0-stage → 0.0.2

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 (259) hide show
  1. package/.env.example +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +328 -2
  4. package/ai_docs/durable_pi/announcement.md +860 -0
  5. package/ai_docs/durable_pi/changelog.md +51 -0
  6. package/ai_docs/durable_pi/chord-delta-findings.md +362 -0
  7. package/ai_docs/durable_pi/coding-agent-integration.md +70 -0
  8. package/ai_docs/durable_pi/pico-v5-chord-usage.md +391 -0
  9. package/ai_docs/durable_pi/pico-v5-handoff.md +921 -0
  10. package/ai_docs/durable_pi/readme.md +617 -0
  11. package/ai_docs/durable_pi/sources.md +22 -0
  12. package/ai_docs/durable_pi/spec.md +4696 -0
  13. package/ai_docs/durable_pi/vacation-planner.md +57 -0
  14. package/ai_docs/pi/chord.md +268 -0
  15. package/ai_docs/pi/cli-integration.md +110 -0
  16. package/ai_docs/pi/cli.md +325 -0
  17. package/ai_docs/pi/codemode.md +211 -0
  18. package/ai_docs/pi/compaction.md +467 -0
  19. package/ai_docs/pi/configuration.md +51 -0
  20. package/ai_docs/pi/containerization.md +187 -0
  21. package/ai_docs/pi/custom-provider.md +177 -0
  22. package/ai_docs/pi/environment-variables.md +102 -0
  23. package/ai_docs/pi/extensions.md +278 -0
  24. package/ai_docs/pi/how-pi-works.md +53 -0
  25. package/ai_docs/pi/index.md +43 -0
  26. package/ai_docs/pi/json.md +230 -0
  27. package/ai_docs/pi/keybindings.md +198 -0
  28. package/ai_docs/pi/llama-cpp.md +118 -0
  29. package/ai_docs/pi/mcp.md +274 -0
  30. package/ai_docs/pi/message-types.md +265 -0
  31. package/ai_docs/pi/models.md +209 -0
  32. package/ai_docs/pi/overview.md +166 -0
  33. package/ai_docs/pi/packages.md +133 -0
  34. package/ai_docs/pi/pi-agent-core.md +568 -0
  35. package/ai_docs/pi/pi-ai.md +1942 -0
  36. package/ai_docs/pi/prompt-templates.md +63 -0
  37. package/ai_docs/pi/providers.md +204 -0
  38. package/ai_docs/pi/quickstart.md +140 -0
  39. package/ai_docs/pi/readme.md +99 -0
  40. package/ai_docs/pi/rpc-commands.md +864 -0
  41. package/ai_docs/pi/rpc-extension-ui.md +204 -0
  42. package/ai_docs/pi/rpc.md +197 -0
  43. package/ai_docs/pi/sdk.md +151 -0
  44. package/ai_docs/pi/security.md +103 -0
  45. package/ai_docs/pi/session-format.md +297 -0
  46. package/ai_docs/pi/sessions.md +70 -0
  47. package/ai_docs/pi/settings.md +173 -0
  48. package/ai_docs/pi/shell-aliases.md +97 -0
  49. package/ai_docs/pi/skills.md +97 -0
  50. package/ai_docs/pi/slash-commands.md +64 -0
  51. package/ai_docs/pi/sources.md +57 -0
  52. package/ai_docs/pi/terminal-setup.md +223 -0
  53. package/ai_docs/pi/termux.md +121 -0
  54. package/ai_docs/pi/themes.md +140 -0
  55. package/ai_docs/pi/tmux.md +59 -0
  56. package/ai_docs/pi/toolkit.md +168 -0
  57. package/ai_docs/pi/tui.md +129 -0
  58. package/ai_docs/pi/usage.md +98 -0
  59. package/ai_docs/pi/virtual-models.md +118 -0
  60. package/ai_docs/pi/windows.md +71 -0
  61. package/apps/dpi-backend/.nvmrc +1 -0
  62. package/apps/dpi-backend/API.md +99 -0
  63. package/apps/dpi-backend/README.md +119 -0
  64. package/apps/dpi-backend/deploy/backend.env.example +9 -0
  65. package/apps/dpi-backend/deploy/dpi-backend.plist +26 -0
  66. package/apps/dpi-backend/deploy/dpi-backend.service +22 -0
  67. package/apps/dpi-backend/package.json +33 -0
  68. package/apps/dpi-backend/prompts/one_sentence.md +3 -0
  69. package/apps/dpi-backend/prompts/smart_ass_opus_fix.md +169 -0
  70. package/apps/dpi-backend/scripts/check-contract.mjs +151 -0
  71. package/apps/dpi-backend/scripts/migrate-playground.ts +66 -0
  72. package/apps/dpi-backend/scripts/record-nested-fixture.mts +154 -0
  73. package/apps/dpi-backend/scripts/token.ts +57 -0
  74. package/apps/dpi-backend/src/addons.ts +139 -0
  75. package/apps/dpi-backend/src/auth.ts +20 -0
  76. package/apps/dpi-backend/src/config.ts +54 -0
  77. package/apps/dpi-backend/src/contract-check.ts +118 -0
  78. package/apps/dpi-backend/src/errors.ts +50 -0
  79. package/apps/dpi-backend/src/extensions.ts +510 -0
  80. package/apps/dpi-backend/src/faux.ts +80 -0
  81. package/apps/dpi-backend/src/history.ts +165 -0
  82. package/apps/dpi-backend/src/idempotency.ts +20 -0
  83. package/apps/dpi-backend/src/jev.ts +43 -0
  84. package/apps/dpi-backend/src/jobs.ts +790 -0
  85. package/apps/dpi-backend/src/main.ts +76 -0
  86. package/apps/dpi-backend/src/model-catalog.ts +180 -0
  87. package/apps/dpi-backend/src/pairing-files.ts +142 -0
  88. package/apps/dpi-backend/src/pairing.ts +459 -0
  89. package/apps/dpi-backend/src/routes.ts +217 -0
  90. package/apps/dpi-backend/src/shared-state.ts +85 -0
  91. package/apps/dpi-backend/src/store.ts +1240 -0
  92. package/apps/dpi-backend/src/streams.ts +256 -0
  93. package/apps/dpi-backend/src/tunnel.ts +955 -0
  94. package/apps/dpi-backend/tsconfig.json +13 -0
  95. package/apps/dpi-playground/README.md +83 -0
  96. package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-CFGc7lHT.js +77 -0
  97. package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-DFdKpwm_.css +1 -0
  98. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/anthropic.png +0 -0
  99. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/deepseek.png +0 -0
  100. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/durable-pi.svg +14 -0
  101. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/gemini.png +0 -0
  102. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/grok.png +0 -0
  103. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/kimi.png +0 -0
  104. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/lab.png +0 -0
  105. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/meta.png +0 -0
  106. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/minimax.png +0 -0
  107. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/openai.png +0 -0
  108. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/qwen.png +0 -0
  109. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/xiaomi.png +0 -0
  110. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/zai.png +0 -0
  111. package/apps/dpi-playground/apps/dpi-playground-fe/dist/index.html +16 -0
  112. package/apps/dpi-playground/apps/dpi-playground-fe/index.html +15 -0
  113. package/apps/dpi-playground/apps/dpi-playground-fe/package.json +5 -0
  114. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/anthropic.png +0 -0
  115. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/deepseek.png +0 -0
  116. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/durable-pi.svg +14 -0
  117. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/gemini.png +0 -0
  118. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/grok.png +0 -0
  119. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/kimi.png +0 -0
  120. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/lab.png +0 -0
  121. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/meta.png +0 -0
  122. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/minimax.png +0 -0
  123. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/openai.png +0 -0
  124. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/qwen.png +0 -0
  125. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/xiaomi.png +0 -0
  126. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/zai.png +0 -0
  127. package/apps/dpi-playground/apps/dpi-playground-fe/src/App.vue +180 -0
  128. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/commands.ts +119 -0
  129. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/composables.ts +129 -0
  130. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/hub.ts +40 -0
  131. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/icon-path.ts +7 -0
  132. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/ids.ts +2 -0
  133. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/jobs.ts +80 -0
  134. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/link-policy.ts +16 -0
  135. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown-policy.ts +115 -0
  136. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown.ts +60 -0
  137. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-close.ts +23 -0
  138. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-handoff.ts +56 -0
  139. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair.ts +166 -0
  140. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/qrcode.ts +323 -0
  141. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/signature.ts +146 -0
  142. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/store.ts +373 -0
  143. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/stream.ts +183 -0
  144. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/AgentView.vue +342 -0
  145. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Composer.vue +198 -0
  146. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ContextBar.vue +254 -0
  147. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceCredentialsDialog.vue +40 -0
  148. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceIcon.vue +56 -0
  149. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DirBrowser.vue +95 -0
  150. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ExtensionsPanel.vue +96 -0
  151. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/HelpDialog.vue +69 -0
  152. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Inspector.vue +425 -0
  153. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/JobCard.vue +221 -0
  154. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/MobileNav.vue +39 -0
  155. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ModelPicker.vue +71 -0
  156. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NetworkSidebar.vue +236 -0
  157. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NewSessionDialog.vue +114 -0
  158. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/OfflinePanel.vue +128 -0
  159. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PairDeviceWizard.vue +433 -0
  160. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PromptDialog.vue +89 -0
  161. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/StateEditor.vue +179 -0
  162. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TasksPanel.vue +280 -0
  163. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Toast.vue +11 -0
  164. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TokenManager.vue +175 -0
  165. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TopBar.vue +114 -0
  166. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TranscriptItem.vue +152 -0
  167. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TreeRow.vue +195 -0
  168. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TunnelStatusBar.vue +107 -0
  169. package/apps/dpi-playground/apps/dpi-playground-fe/src/extensions.css +105 -0
  170. package/apps/dpi-playground/apps/dpi-playground-fe/src/jobs.css +655 -0
  171. package/apps/dpi-playground/apps/dpi-playground-fe/src/main.ts +22 -0
  172. package/apps/dpi-playground/apps/dpi-playground-fe/src/network.css +675 -0
  173. package/apps/dpi-playground/apps/dpi-playground-fe/src/style.css +3203 -0
  174. package/apps/dpi-playground/apps/dpi-playground-fe/tsconfig.json +16 -0
  175. package/apps/dpi-playground/apps/dpi-playground-fe/vite.config.ts +14 -0
  176. package/apps/dpi-playground/apps/dpi-playground-hub/package.json +5 -0
  177. package/apps/dpi-playground/apps/dpi-playground-hub/src/audit.ts +406 -0
  178. package/apps/dpi-playground/apps/dpi-playground-hub/src/client-ip.ts +72 -0
  179. package/apps/dpi-playground/apps/dpi-playground-hub/src/credentials.ts +265 -0
  180. package/apps/dpi-playground/apps/dpi-playground-hub/src/guard.ts +43 -0
  181. package/apps/dpi-playground/apps/dpi-playground-hub/src/http-errors.ts +156 -0
  182. package/apps/dpi-playground/apps/dpi-playground-hub/src/identity.ts +60 -0
  183. package/apps/dpi-playground/apps/dpi-playground-hub/src/limiter.ts +165 -0
  184. package/apps/dpi-playground/apps/dpi-playground-hub/src/main.ts +460 -0
  185. package/apps/dpi-playground/apps/dpi-playground-hub/src/monitor.ts +320 -0
  186. package/apps/dpi-playground/apps/dpi-playground-hub/src/mux.ts +277 -0
  187. package/apps/dpi-playground/apps/dpi-playground-hub/src/pairing.ts +816 -0
  188. package/apps/dpi-playground/apps/dpi-playground-hub/src/proxy.ts +128 -0
  189. package/apps/dpi-playground/apps/dpi-playground-hub/src/rotation.ts +583 -0
  190. package/apps/dpi-playground/apps/dpi-playground-hub/src/seed.ts +11 -0
  191. package/apps/dpi-playground/apps/dpi-playground-hub/src/transport.ts +108 -0
  192. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-token.ts +11 -0
  193. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-transport.ts +560 -0
  194. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel.ts +873 -0
  195. package/apps/dpi-playground/apps/dpi-playground-hub/tsconfig.json +13 -0
  196. package/apps/dpi-playground/package.json +37 -0
  197. package/apps/dpi-sdk/README.md +74 -0
  198. package/apps/dpi-sdk/package.json +20 -0
  199. package/apps/dpi-sdk/scripts/net.ts +73 -0
  200. package/apps/dpi-sdk/src/api.ts +631 -0
  201. package/apps/dpi-sdk/src/app-state.ts +111 -0
  202. package/apps/dpi-sdk/src/client.ts +330 -0
  203. package/apps/dpi-sdk/src/errors.ts +16 -0
  204. package/apps/dpi-sdk/src/jobs.ts +99 -0
  205. package/apps/dpi-sdk/src/models.ts +472 -0
  206. package/apps/dpi-sdk/src/node/chunk.ts +488 -0
  207. package/apps/dpi-sdk/src/node/config.ts +175 -0
  208. package/apps/dpi-sdk/src/node/drivers.ts +402 -0
  209. package/apps/dpi-sdk/src/node/ops.ts +114 -0
  210. package/apps/dpi-sdk/src/node/private-dir.ts +48 -0
  211. package/apps/dpi-sdk/src/node/registry-migration.ts +549 -0
  212. package/apps/dpi-sdk/src/node/registry.ts +390 -0
  213. package/apps/dpi-sdk/src/node/sse-writer.ts +151 -0
  214. package/apps/dpi-sdk/src/protocol/backoff.ts +31 -0
  215. package/apps/dpi-sdk/src/protocol/limits.ts +199 -0
  216. package/apps/dpi-sdk/src/protocol/pairing.ts +311 -0
  217. package/apps/dpi-sdk/src/protocol/rest-routes.ts +105 -0
  218. package/apps/dpi-sdk/src/protocol/tunnel.ts +331 -0
  219. package/apps/dpi-sdk/src/sse.ts +117 -0
  220. package/apps/dpi-sdk/src/transcript.ts +291 -0
  221. package/apps/dpi-sdk/tsconfig.json +13 -0
  222. package/apps/dpi-sdk/w +20 -0
  223. package/apps/dpi-tui/README.md +54 -0
  224. package/apps/dpi-tui/bin/dpi-tui.mjs +5 -0
  225. package/apps/dpi-tui/package.json +20 -0
  226. package/apps/dpi-tui/src/app.ts +433 -0
  227. package/apps/dpi-tui/src/commands.ts +151 -0
  228. package/apps/dpi-tui/src/main.ts +122 -0
  229. package/apps/dpi-tui/src/pairing.ts +287 -0
  230. package/apps/dpi-tui/src/pickers.ts +238 -0
  231. package/apps/dpi-tui/src/render.ts +164 -0
  232. package/apps/dpi-tui/src/theme.ts +57 -0
  233. package/apps/dpi-tui/tsconfig.json +14 -0
  234. package/bin/device.mjs +5 -0
  235. package/bin/hub.mjs +5 -0
  236. package/bin/tui.mjs +5 -0
  237. package/deploy/Caddyfile +120 -0
  238. package/docs/verification.md +102 -0
  239. package/dpi-network.example.yaml +62 -0
  240. package/images/01_durable_agent_network.svg +185 -0
  241. package/images/02_pi_made_durable.svg +204 -0
  242. package/images/03_restart_mid_tool.svg +168 -0
  243. package/images/07_one_api_every_client.svg +123 -0
  244. package/images/08_device_session_agent.svg +123 -0
  245. package/images/09_compact_keep_history.svg +143 -0
  246. package/images/10_shared_app_state.svg +177 -0
  247. package/images/11_durable_jobs_wait.svg +141 -0
  248. package/images/12_tools_change_mid_run.svg +231 -0
  249. package/images/13_hook_blocks_tool.svg +122 -0
  250. package/images/14_fork_at_any_entry.svg +119 -0
  251. package/images/15_web_client.png +0 -0
  252. package/images/16_terminal_client.svg +43 -0
  253. package/images/17_devices_are_data.svg +182 -0
  254. package/images/18_should_every_agent_be_durable.svg +118 -0
  255. package/images/19_sqlite_on_every_device.svg +155 -0
  256. package/images/20_admission_and_effects.svg +134 -0
  257. package/justfile +33 -0
  258. package/package.json +41 -4
  259. package/scripts/verify.mjs +210 -0
@@ -0,0 +1,165 @@
1
+ // search_history: look back through an agent's whole stored history, including entries before a reset, handoff or
2
+ // compaction that the model no longer sees. Pure over a list of entries, so it is testable without a harness.
3
+ import type { WireAssistantMessage, WireEntry, WireMessage, WireToolResultMessage } from "../../dpi-sdk/src/api.js";
4
+ import { messageText } from "../../dpi-sdk/src/transcript.js";
5
+
6
+ export type HistoryKind = "user" | "assistant" | "tool_call" | "tool_result" | "event";
7
+
8
+ export interface HistoryQuery {
9
+ /** Words that must all appear, case-insensitive. */
10
+ query?: string;
11
+ /** Only calls to and results from this tool. */
12
+ tool?: string;
13
+ /** One entry by number, returned in full. */
14
+ entry?: number;
15
+ kind?: HistoryKind;
16
+ from_entry?: number;
17
+ to_entry?: number;
18
+ order?: "newest" | "oldest";
19
+ limit?: number;
20
+ offset?: number;
21
+ }
22
+
23
+ export type HistoryContext = "active" | "compacted" | "before reset";
24
+
25
+ export interface HistoryHit {
26
+ entry: number;
27
+ kind: Exclude<HistoryKind, "tool_call">;
28
+ context: HistoryContext;
29
+ tools?: string[];
30
+ text: string;
31
+ }
32
+
33
+ export interface HistoryPage {
34
+ total: number;
35
+ offset: number;
36
+ limit: number;
37
+ next_offset: number | null;
38
+ /** The first entry the model still sees; older entries are only reachable through this tool. */
39
+ active_from?: number;
40
+ results: HistoryHit[];
41
+ }
42
+
43
+ const SEARCH_TOOL = "search_history";
44
+ const SNIPPET = 360;
45
+ const FULL = 4000;
46
+
47
+ interface Parsed {
48
+ id: number;
49
+ kind: HistoryHit["kind"];
50
+ tools: string[];
51
+ /** Searchable text: message text, tool names and arguments, result text. */
52
+ text: string;
53
+ }
54
+
55
+ const clip = (s: string, n: number) => (s.length > n ? `${s.slice(0, n)}…` : s);
56
+
57
+ function parse(entry: WireEntry): Parsed | undefined {
58
+ const messages = (entry.model ?? []) as WireMessage[];
59
+ if (entry.kind === "pi.system") return undefined;
60
+ if (entry.kind === "pi.user")
61
+ return { id: entry.id, kind: "user", tools: [], text: messages.map(messageText).join("\n").trim() };
62
+ if (entry.kind === "pi.assistant") {
63
+ const tools: string[] = [];
64
+ const parts: string[] = [];
65
+ for (const m of messages) {
66
+ if (m.role !== "assistant") continue;
67
+ for (const block of (m as WireAssistantMessage).content) {
68
+ if (block.type === "text") parts.push(block.text);
69
+ if (block.type === "toolCall") {
70
+ tools.push(block.name);
71
+ parts.push(`→ ${block.name} ${JSON.stringify(block.arguments ?? {})}`);
72
+ }
73
+ }
74
+ }
75
+ return { id: entry.id, kind: "assistant", tools, text: parts.join("\n").trim() };
76
+ }
77
+ if (entry.kind === "pi.tool-result") {
78
+ const results = messages.filter((m): m is WireToolResultMessage => m.role === "toolResult");
79
+ return {
80
+ id: entry.id,
81
+ kind: "tool_result",
82
+ tools: results.map((r) => r.toolName),
83
+ text: results.map((r) => `${r.toolName}${r.isError ? " (error)" : ""}: ${messageText(r)}`).join("\n").trim(),
84
+ };
85
+ }
86
+ const data = entry.data as { text?: unknown } | undefined;
87
+ const note = messages.map(messageText).join("\n");
88
+ const title =
89
+ typeof data?.text === "string"
90
+ ? data.text
91
+ : entry.kind === "pi.reset"
92
+ ? "New context started (reset or handoff)."
93
+ : entry.kind === "pi.compaction"
94
+ ? "Context compacted. Summary of the older entries:"
95
+ : entry.kind;
96
+ const text = [title, note].filter(Boolean).join("\n");
97
+ return { id: entry.id, kind: "event", tools: [], text: text.trim() };
98
+ }
99
+
100
+ function snippet(text: string, terms: string[]) {
101
+ if (!terms.length) return clip(text, SNIPPET);
102
+ const at = text.toLowerCase().indexOf(terms[0]);
103
+ const start = Math.max(0, at - 100);
104
+ return `${start > 0 ? "…" : ""}${clip(text.slice(start), SNIPPET)}`;
105
+ }
106
+
107
+ /**
108
+ * `entries` is every visible entry in any order. `activeFrom` is the first entry of the active context (the latest
109
+ * head marker's head); `lastReset` is the newest `pi.reset` entry, which separates reset history from compacted history.
110
+ */
111
+ export function searchHistory(
112
+ entries: readonly WireEntry[],
113
+ q: HistoryQuery,
114
+ marks: { activeFrom?: number; lastReset?: number },
115
+ ): HistoryPage {
116
+ const limit = Math.min(Math.max(Math.trunc(q.limit ?? 10), 1), 50);
117
+ const offset = Math.max(Math.trunc(q.offset ?? 0), 0);
118
+ const context = (id: number): HistoryContext =>
119
+ marks.activeFrom === undefined || id >= marks.activeFrom
120
+ ? "active"
121
+ : marks.lastReset !== undefined && id < marks.lastReset
122
+ ? "before reset"
123
+ : "compacted";
124
+ const hit = (p: Parsed, terms: string[], full: boolean): HistoryHit => ({
125
+ entry: p.id,
126
+ kind: p.kind,
127
+ context: context(p.id),
128
+ ...(p.tools.length ? { tools: p.tools } : {}),
129
+ text: full ? clip(p.text, FULL) : snippet(p.text, terms),
130
+ });
131
+ const page = (results: HistoryHit[], total: number): HistoryPage => ({
132
+ total,
133
+ offset,
134
+ limit,
135
+ next_offset: offset + results.length < total ? offset + results.length : null,
136
+ ...(marks.activeFrom !== undefined ? { active_from: marks.activeFrom } : {}),
137
+ results,
138
+ });
139
+
140
+ const parsed = entries.flatMap((e) => parse(e) ?? []);
141
+ if (q.entry !== undefined) {
142
+ const found = parsed.find((p) => p.id === q.entry);
143
+ return page(found && offset === 0 ? [hit(found, [], true)] : [], found ? 1 : 0);
144
+ }
145
+
146
+ const terms = (q.query ?? "").toLowerCase().split(/\s+/).filter(Boolean);
147
+ const tool = q.tool?.trim().toLowerCase();
148
+ const matches = parsed
149
+ .filter((p) => {
150
+ // The search's own calls and results only match when asked for by name.
151
+ if (tool !== SEARCH_TOOL && p.tools.length && p.tools.every((t) => t === SEARCH_TOOL)) return false;
152
+ if (q.from_entry !== undefined && p.id < q.from_entry) return false;
153
+ if (q.to_entry !== undefined && p.id > q.to_entry) return false;
154
+ if (tool && !p.tools.some((t) => t.toLowerCase() === tool)) return false;
155
+ if (q.kind === "tool_call" && !(p.kind === "assistant" && p.tools.length)) return false;
156
+ if (q.kind && q.kind !== "tool_call" && p.kind !== q.kind) return false;
157
+ const text = p.text.toLowerCase();
158
+ return terms.every((t) => text.includes(t));
159
+ })
160
+ .sort((a, b) => (q.order === "oldest" ? a.id - b.id : b.id - a.id));
161
+ return page(
162
+ matches.slice(offset, offset + limit).map((p) => hit(p, terms, false)),
163
+ matches.length,
164
+ );
165
+ }
@@ -0,0 +1,20 @@
1
+ import type { RequestHandler } from "express";
2
+ import type { Store } from "./store.js";
3
+
4
+ /** Replays the stored response for a repeated `Idempotency-Key` on create routes. */
5
+ export function idempotent(store: Store): RequestHandler {
6
+ return (req, res, next) => {
7
+ const key = req.get("idempotency-key");
8
+ if (!key) return next();
9
+ if (key.length > 128) return void res.status(400).json({ error: { code: "invalid", message: "Idempotency-Key too long" } });
10
+ const route = `${req.method} ${req.baseUrl}${req.path}`;
11
+ const stored = store.replay(key, route);
12
+ if (stored) return void res.status(stored.status).set("Idempotent-Replay", "true").json(stored.body);
13
+ const json = res.json.bind(res);
14
+ res.json = (body: unknown) => {
15
+ if (res.statusCode < 400) store.remember(key, route, res.statusCode, body);
16
+ return json(body);
17
+ };
18
+ next();
19
+ };
20
+ }
@@ -0,0 +1,43 @@
1
+ // Minimal Jev client (TypeSafe System One), from harness/extensions/jev-bash/core/client.ts: one call answers a few
2
+ // bounded questions about a JSON state in a few hundred milliseconds.
3
+ export type JevQuestion =
4
+ | { type: "noul"; instructions: string; criteria?: { true?: string; false?: string } }
5
+ | { type: "choice"; instructions: string; criteria: Record<string, string> };
6
+ export type JevAnswer =
7
+ | { type: "noul"; noul: number }
8
+ | { type: "choice"; choice: string; confidence: number; probabilities: Record<string, number> };
9
+
10
+ const RETRY = new Set([429, 502, 503, 529]);
11
+
12
+ export async function askJev(
13
+ state: unknown,
14
+ questions: Record<string, JevQuestion>,
15
+ signal?: AbortSignal,
16
+ ): Promise<Record<string, JevAnswer>> {
17
+ const [endpoint, key, model] = process.env.TYPESAFE_API_KEY
18
+ ? ["https://api.typesafe.ai/v1/systemone", process.env.TYPESAFE_API_KEY, "jev-latest"]
19
+ : process.env.OPENROUTER_API_KEY
20
+ ? ["https://openrouter.ai/api/alpha/decisions", process.env.OPENROUTER_API_KEY, "~typesafe/jev-latest"]
21
+ : [];
22
+ if (!endpoint) throw Error("Jev needs TYPESAFE_API_KEY or OPENROUTER_API_KEY");
23
+ const body = JSON.stringify({ model, state, questions });
24
+ for (let attempt = 1; ; attempt++) {
25
+ const res = await fetch(endpoint, {
26
+ method: "POST",
27
+ headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
28
+ body,
29
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15_000)]) : AbortSignal.timeout(15_000),
30
+ redirect: "error",
31
+ });
32
+ if (RETRY.has(res.status) && attempt < 3) {
33
+ await res.body?.cancel();
34
+ await new Promise((r) => setTimeout(r, 400 * 2 ** attempt));
35
+ continue;
36
+ }
37
+ if (!res.ok) throw Error(`Jev HTTP ${res.status}`);
38
+ const data = (await res.json()) as { answers?: Record<string, JevAnswer> };
39
+ for (const [id, q] of Object.entries(questions))
40
+ if (data.answers?.[id]?.type !== q.type) throw Error(`Jev answer missing: ${id}`);
41
+ return data.answers!;
42
+ }
43
+ }