iterate 0.2.7 → 0.3.0

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 (218) hide show
  1. package/README.md +86 -76
  2. package/THIRD_PARTY_NOTICES.md +55 -0
  3. package/bin/iterate.js +11 -3
  4. package/dist/api-url-B6404M82.mjs +17 -0
  5. package/dist/api-url-B6404M82.mjs.map +1 -0
  6. package/dist/app-ref-BipL0feU.mjs +35 -0
  7. package/dist/app-ref-BipL0feU.mjs.map +1 -0
  8. package/dist/app-ref-C1CrgXqX.mjs +7 -0
  9. package/dist/app-ref-C1CrgXqX.mjs.map +1 -0
  10. package/dist/app-ref-DYai_om1.mjs +7 -0
  11. package/dist/app-ref-DYai_om1.mjs.map +1 -0
  12. package/dist/cli-D0c-pDL_.mjs +1010 -0
  13. package/dist/cli-D0c-pDL_.mjs.map +1 -0
  14. package/dist/client.d.ts +3 -0
  15. package/dist/client.mjs +4 -0
  16. package/dist/cloudflare-BTm90gQ4.mjs +951 -0
  17. package/dist/cloudflare-BTm90gQ4.mjs.map +1 -0
  18. package/dist/contract-s4FW4eES.mjs +309 -0
  19. package/dist/contract-s4FW4eES.mjs.map +1 -0
  20. package/dist/document-review/index.d.ts +5 -0
  21. package/dist/document-review/types.d.ts +107 -0
  22. package/dist/document-review.mjs +7015 -0
  23. package/dist/document-review.mjs.map +1 -0
  24. package/dist/durable-object-processor-durability-CNsTjAJS.mjs +205 -0
  25. package/dist/durable-object-processor-durability-CNsTjAJS.mjs.map +1 -0
  26. package/dist/idempotency-DleloJNt.mjs +28 -0
  27. package/dist/idempotency-DleloJNt.mjs.map +1 -0
  28. package/dist/index.mjs +1 -1
  29. package/dist/itx/api-url.d.ts +6 -0
  30. package/dist/itx/itx-node-client.d.ts +65 -0
  31. package/dist/itx/itx-session.d.ts +215 -0
  32. package/dist/itx/owned-rpc-session.d.ts +14 -0
  33. package/dist/itx/query-client.d.ts +10 -0
  34. package/dist/itx-api.generated.d.ts +6195 -0
  35. package/dist/itx-session-sjud8GiT.mjs +534 -0
  36. package/dist/itx-session-sjud8GiT.mjs.map +1 -0
  37. package/dist/live-state-BJNqOwFw.mjs +299 -0
  38. package/dist/live-state-BJNqOwFw.mjs.map +1 -0
  39. package/dist/next/api.d.ts +479 -0
  40. package/dist/next/api.mjs +0 -0
  41. package/dist/next/app-server.d.ts +44 -0
  42. package/dist/next/app-server.mjs +479 -0
  43. package/dist/next/app-server.mjs.map +1 -0
  44. package/dist/next/app-session.d.ts +49 -0
  45. package/dist/next/app-session.mjs +238 -0
  46. package/dist/next/app-session.mjs.map +1 -0
  47. package/dist/next/app.d.ts +29 -0
  48. package/dist/next/app.mjs +141 -0
  49. package/dist/next/app.mjs.map +1 -0
  50. package/dist/next/client/live-state.d.ts +63 -0
  51. package/dist/next/client/oauth.d.ts +12 -0
  52. package/dist/next/client/react.d.ts +109 -0
  53. package/dist/next/client/socket.d.ts +6 -0
  54. package/dist/next/client.mjs +156 -0
  55. package/dist/next/client.mjs.map +1 -0
  56. package/dist/next/expression.d.ts +146 -0
  57. package/dist/next/expression.mjs +399 -0
  58. package/dist/next/expression.mjs.map +1 -0
  59. package/dist/next/lib.d.ts +56 -0
  60. package/dist/next/lib.mjs +199 -0
  61. package/dist/next/lib.mjs.map +1 -0
  62. package/dist/next/oauth-scopes.d.ts +32 -0
  63. package/dist/next/oauth-scopes.mjs +40 -0
  64. package/dist/next/oauth-scopes.mjs.map +1 -0
  65. package/dist/next/oauth.mjs +29 -0
  66. package/dist/next/oauth.mjs.map +1 -0
  67. package/dist/next/principal.d.ts +64 -0
  68. package/dist/next/principal.mjs +98 -0
  69. package/dist/next/principal.mjs.map +1 -0
  70. package/dist/next/project-ingress.d.ts +37 -0
  71. package/dist/next/project-ingress.mjs +75 -0
  72. package/dist/next/project-ingress.mjs.map +1 -0
  73. package/dist/next/react.mjs +285 -0
  74. package/dist/next/react.mjs.map +1 -0
  75. package/dist/next/sdk/auth.d.ts +5 -0
  76. package/dist/next/sdk/index.d.ts +112 -0
  77. package/dist/next/sdk.mjs +139 -0
  78. package/dist/next/sdk.mjs.map +1 -0
  79. package/dist/next/stream/processor.d.ts +378 -0
  80. package/dist/next/stream/processor.mjs +582 -0
  81. package/dist/next/stream/processor.mjs.map +1 -0
  82. package/dist/next/stream/run.d.ts +58 -0
  83. package/dist/next/stream/run.mjs +40 -0
  84. package/dist/next/stream/run.mjs.map +1 -0
  85. package/dist/next-node.d.ts +15 -0
  86. package/dist/next-node.mjs +51 -0
  87. package/dist/next-node.mjs.map +1 -0
  88. package/dist/node.d.ts +3 -0
  89. package/dist/node.mjs +185 -0
  90. package/dist/node.mjs.map +1 -0
  91. package/dist/processor-host-capabilities-BMFH3KTM.mjs +56 -0
  92. package/dist/processor-host-capabilities-BMFH3KTM.mjs.map +1 -0
  93. package/dist/processors/cloudflare.d.ts +3 -0
  94. package/dist/processors/durable-object-processor-durability.d.ts +79 -0
  95. package/dist/processors/event-consumption-metrics.d.ts +82 -0
  96. package/dist/processors/idempotency.d.ts +13 -0
  97. package/dist/processors/index.d.ts +12 -0
  98. package/dist/processors/processor-contracts.d.ts +342 -0
  99. package/dist/processors/processor-facet.d.ts +186 -0
  100. package/dist/processors/processor-host-capabilities.d.ts +60 -0
  101. package/dist/processors/prompt-sections.d.ts +17 -0
  102. package/dist/processors/rpc-types.d.ts +515 -0
  103. package/dist/processors/schemas.d.ts +102 -0
  104. package/dist/processors/stream-handle.d.ts +45 -0
  105. package/dist/processors/stream-processor-keepalive.d.ts +95 -0
  106. package/dist/processors/stream-processor-registry.d.ts +233 -0
  107. package/dist/processors/stream-processor-runner.d.ts +289 -0
  108. package/dist/processors/stream-processor.d.ts +339 -0
  109. package/dist/processors/stream-runtime-metrics.d.ts +107 -0
  110. package/dist/processors/testing.d.ts +302 -0
  111. package/dist/processors-BoNyeBfQ.mjs +10 -0
  112. package/dist/processors-BoNyeBfQ.mjs.map +1 -0
  113. package/dist/processors-cloudflare.mjs +3 -0
  114. package/dist/processors-testing.mjs +435 -0
  115. package/dist/processors-testing.mjs.map +1 -0
  116. package/dist/processors.mjs +52 -0
  117. package/dist/processors.mjs.map +1 -0
  118. package/dist/protocol-DnK_f2m6.mjs +251 -0
  119. package/dist/protocol-DnK_f2m6.mjs.map +1 -0
  120. package/dist/sdk/capnweb/index.d.ts +2 -0
  121. package/dist/sdk/capnweb/live-state/compact.d.ts +5 -0
  122. package/dist/sdk/capnweb/live-state/diff.d.ts +41 -0
  123. package/dist/sdk/capnweb/live-state/engine.d.ts +44 -0
  124. package/dist/sdk/capnweb/live-state/index.d.ts +41 -0
  125. package/dist/sdk/capnweb/live-state/protocol.d.ts +87 -0
  126. package/dist/sdk/capnweb/live-state/retain.d.ts +23 -0
  127. package/dist/sdk/capnweb/live-state/store.d.ts +20 -0
  128. package/dist/sdk/capnweb/live-state/types.d.ts +11 -0
  129. package/dist/sdk/capnweb/react.d.ts +45 -0
  130. package/dist/sdk/capnweb/react.mjs +316 -0
  131. package/dist/sdk/capnweb/react.mjs.map +1 -0
  132. package/dist/sdk/capnweb.mjs +4 -0
  133. package/dist/sdk/itx/react.d.ts +191 -0
  134. package/dist/sdk/itx/react.mjs +383 -0
  135. package/dist/sdk/itx/react.mjs.map +1 -0
  136. package/dist/sdk-DMB-IM11.mjs +933 -0
  137. package/dist/sdk-DMB-IM11.mjs.map +1 -0
  138. package/dist/sdk.d.ts +339 -0
  139. package/dist/sdk.mjs +2 -0
  140. package/dist/serve-itx.d.ts +46 -0
  141. package/dist/starter-apps/flake-dashboard/app-ref.d.ts +31 -0
  142. package/dist/starter-apps/flake-dashboard/configured-worker.mjs +1055 -0
  143. package/dist/starter-apps/flake-dashboard/configured-worker.mjs.map +1 -0
  144. package/dist/starter-apps/flake-dashboard/contract.d.ts +4839 -0
  145. package/dist/starter-apps/flake-dashboard/contract.mjs +2 -0
  146. package/dist/starter-apps/flake-dashboard/index.d.ts +17 -0
  147. package/dist/starter-apps/flake-dashboard/index.mjs +56 -0
  148. package/dist/starter-apps/flake-dashboard/index.mjs.map +1 -0
  149. package/dist/starter-apps/flake-dashboard/worker.d.ts +4607 -0
  150. package/dist/starter-apps/github-ai-linter/ai-linter.d.ts +8914 -0
  151. package/dist/starter-apps/github-ai-linter/configured-worker.mjs +17987 -0
  152. package/dist/starter-apps/github-ai-linter/configured-worker.mjs.map +1 -0
  153. package/dist/starter-apps/github-ai-linter/contract.d.ts +9193 -0
  154. package/dist/starter-apps/github-ai-linter/index.d.ts +10 -0
  155. package/dist/starter-apps/github-ai-linter/index.mjs +36 -0
  156. package/dist/starter-apps/github-ai-linter/index.mjs.map +1 -0
  157. package/dist/starter-apps/github-ai-linter/prompt.d.ts +13 -0
  158. package/dist/starter-apps/github-ai-linter/review-bot.d.ts +808 -0
  159. package/dist/starter-apps/github-ai-linter/rules.d.ts +34 -0
  160. package/dist/starter-apps/github-ai-linter/worker-ref.d.ts +19 -0
  161. package/dist/starter-apps/github-ai-linter/worker.d.ts +19 -0
  162. package/dist/starter-apps/github-ai-linter/worker.mjs +947 -0
  163. package/dist/starter-apps/github-ai-linter/worker.mjs.map +1 -0
  164. package/dist/starter-apps/guestbook/app-ref.d.ts +27 -0
  165. package/dist/starter-apps/guestbook/client.d.ts +7 -0
  166. package/dist/starter-apps/guestbook/client.mjs +59 -0
  167. package/dist/starter-apps/guestbook/configured-worker.mjs +205 -0
  168. package/dist/starter-apps/guestbook/configured-worker.mjs.map +1 -0
  169. package/dist/starter-apps/guestbook/index.d.ts +9 -0
  170. package/dist/starter-apps/guestbook/index.mjs +31 -0
  171. package/dist/starter-apps/guestbook/index.mjs.map +1 -0
  172. package/dist/starter-apps/guestbook/processor.d.ts +2267 -0
  173. package/dist/starter-apps/guestbook/worker.d.ts +26 -0
  174. package/dist/starter-apps/guestbook/worker.mjs +191 -0
  175. package/dist/starter-apps/guestbook/worker.mjs.map +1 -0
  176. package/dist/starter-apps/media/configured-worker.mjs +577 -0
  177. package/dist/starter-apps/media/configured-worker.mjs.map +1 -0
  178. package/dist/starter-apps/media/index.mjs +36 -0
  179. package/dist/starter-apps/media/index.mjs.map +1 -0
  180. package/dist/starter-apps/media/ref.mjs +20 -0
  181. package/dist/starter-apps/media/ref.mjs.map +1 -0
  182. package/dist/starter-apps/media/worker.mjs +579 -0
  183. package/dist/starter-apps/media/worker.mjs.map +1 -0
  184. package/dist/starter-apps/notes/configured-worker.mjs +6134 -0
  185. package/dist/starter-apps/notes/configured-worker.mjs.map +1 -0
  186. package/dist/starter-apps/notes/index.mjs +23 -0
  187. package/dist/starter-apps/notes/index.mjs.map +1 -0
  188. package/dist/starter-apps/notes/ref.mjs +21 -0
  189. package/dist/starter-apps/notes/ref.mjs.map +1 -0
  190. package/dist/starter-apps/notes/worker.mjs +427 -0
  191. package/dist/starter-apps/notes/worker.mjs.map +1 -0
  192. package/dist/starter-apps/todo/client.mjs +59 -0
  193. package/dist/starter-apps/todo/configured-worker.mjs +2864 -0
  194. package/dist/starter-apps/todo/configured-worker.mjs.map +1 -0
  195. package/dist/starter-apps/todo/index.d.ts +8 -0
  196. package/dist/starter-apps/todo/index.mjs +29 -0
  197. package/dist/starter-apps/todo/index.mjs.map +1 -0
  198. package/dist/stream-processor-keepalive-DAQTP6m3.mjs +2082 -0
  199. package/dist/stream-processor-keepalive-DAQTP6m3.mjs.map +1 -0
  200. package/dist/usingCtx-inzbY1Qz.mjs +57 -0
  201. package/dist/usingCtx-mZx5nsAW.mjs +11800 -0
  202. package/dist/usingCtx-mZx5nsAW.mjs.map +1 -0
  203. package/dist/worker-ref-DZxPDmb_.mjs +390 -0
  204. package/dist/worker-ref-DZxPDmb_.mjs.map +1 -0
  205. package/menubar/Iterate.entitlements +12 -0
  206. package/menubar/Iterate.swift +914 -0
  207. package/menubar/IterateIcon.swift +145 -0
  208. package/menubar/README.md +28 -0
  209. package/menubar/build-menubar-app.sh +59 -0
  210. package/package.json +235 -18
  211. package/dist/cli-DMS4kJph.mjs +0 -868
  212. package/dist/cli-DMS4kJph.mjs.map +0 -1
  213. package/dist/config-DtnR7Lv7.mjs +0 -170
  214. package/dist/config-DtnR7Lv7.mjs.map +0 -1
  215. package/dist/index.d.mts.map +0 -1
  216. package/dist/stream-tui/agent-chat-terminal.d.mts +0 -1
  217. package/dist/stream-tui/agent-chat-terminal.mjs +0 -933
  218. package/dist/stream-tui/agent-chat-terminal.mjs.map +0 -1
@@ -0,0 +1,579 @@
1
+ import { r as StreamProcessorDurableObject } from "../../sdk-DMB-IM11.mjs";
2
+ import { h as defineProcessorContract, s as StreamProcessor } from "../../stream-processor-keepalive-DAQTP6m3.mjs";
3
+ import { n as isIdempotencyConflict } from "../../idempotency-DleloJNt.mjs";
4
+ import "../../processors.mjs";
5
+ import { t as _usingCtx } from "../../usingCtx-inzbY1Qz.mjs";
6
+ import { mediaStreamPath } from "./ref.mjs";
7
+ import { z } from "zod";
8
+ //#region src/starter-apps/media/analysis.ts
9
+ /** Sees the pixels: transcribes text verbatim and picks tags. */
10
+ const MEDIA_VISION_MODEL = "@cf/meta/llama-4-scout-17b-16e-instruct";
11
+ /**
12
+ * Starter taxonomy — expect churn. Multi-tag with overlap allowed; the model
13
+ * may also coin up to two novel kebab-case tags, and returning NO tags is an
14
+ * acceptable answer (deliberately conservative — early dogfood tagged
15
+ * everything `bug-report` on wild guesses).
16
+ */
17
+ const MEDIA_TAG_TAXONOMY = [
18
+ {
19
+ tag: "screenshot",
20
+ hint: "a captured device screen: app UI, web page, status bar visible"
21
+ },
22
+ {
23
+ tag: "photo",
24
+ hint: "a camera photograph of the physical world"
25
+ },
26
+ {
27
+ tag: "transient",
28
+ hint: "one-off info with no lasting value: OTP codes, confirmations"
29
+ },
30
+ {
31
+ tag: "clipping",
32
+ hint: "a saved post, article, meme, or quote worth keeping"
33
+ },
34
+ {
35
+ tag: "logistics",
36
+ hint: "tickets, bookings, schedules, travel details, QR codes"
37
+ },
38
+ {
39
+ tag: "receipt",
40
+ hint: "proof of purchase, invoices, order confirmations"
41
+ },
42
+ {
43
+ tag: "conversation",
44
+ hint: "chat threads, DMs, emails, comment sections"
45
+ },
46
+ {
47
+ tag: "code",
48
+ hint: "code, terminals, dev tools, technical docs"
49
+ },
50
+ {
51
+ tag: "reference",
52
+ hint: "info to keep long-term: settings, lists, instructions"
53
+ }
54
+ ];
55
+ /**
56
+ * One full analysis of a stored media file. Throws on conversion failure so
57
+ * the caller (the processor's obligation attempt) owns retry/settlement; an
58
+ * unparseable vision answer degrades to empty title/transcript and
59
+ * ["untagged"] so failures stay visible instead of hiding an item.
60
+ */
61
+ async function analyzeMediaImage(session, input) {
62
+ const bytes = await session.files.get(input.path).bytes();
63
+ const described = await session.ai.toMarkdown({
64
+ name: input.filename,
65
+ blob: bytes
66
+ });
67
+ if (described.format === "error") throw new Error(`toMarkdown failed for ${input.filename}: ${described.error}`);
68
+ const markdown = (described.data || "").trim();
69
+ let visionBytes = bytes;
70
+ let visionType = input.contentType;
71
+ if (bytes.length > 1e6) try {
72
+ const resized = await session.integrations.cf.images.transformBytes({
73
+ image: bytes,
74
+ transforms: [{ width: 1280 }],
75
+ output: {
76
+ format: "image/jpeg",
77
+ quality: 80
78
+ }
79
+ });
80
+ visionBytes = resized.bytes;
81
+ visionType = resized.contentType;
82
+ } catch {}
83
+ let binary = "";
84
+ for (let i = 0; i < visionBytes.length; i += 32768) binary += String.fromCharCode(...visionBytes.subarray(i, i + 32768));
85
+ return {
86
+ ...parseVisionAnswer(visionAnswerText(await session.ai.run(MEDIA_VISION_MODEL, {
87
+ messages: [{
88
+ role: "user",
89
+ content: [{
90
+ type: "text",
91
+ text: mediaVisionPrompt()
92
+ }, {
93
+ type: "image_url",
94
+ image_url: { url: `data:${visionType};base64,${btoa(binary)}` }
95
+ }]
96
+ }],
97
+ max_tokens: 1024
98
+ }))),
99
+ markdown
100
+ };
101
+ }
102
+ /** Both Workers AI answer shapes: `{ response }` and OpenAI-style choices. */
103
+ function visionAnswerText(answer) {
104
+ const bag = answer;
105
+ if (typeof bag?.response === "string") return bag.response;
106
+ const content = bag?.choices?.[0]?.message?.content;
107
+ return typeof content === "string" ? content : "";
108
+ }
109
+ /** Extract the JSON object from a chatty model answer, defensively. */
110
+ function parseVisionAnswer(text) {
111
+ let title = "";
112
+ let transcript = "";
113
+ let tags = ["untagged"];
114
+ const match = text.match(/\{[\s\S]*\}/);
115
+ if (match) try {
116
+ const parsed = JSON.parse(match[0]);
117
+ if (typeof parsed.title === "string") title = parsed.title.trim().slice(0, 120);
118
+ if (typeof parsed.transcript === "string") transcript = parsed.transcript.trim();
119
+ if (Array.isArray(parsed.tags)) tags = [...new Set(parsed.tags.filter((tag) => typeof tag === "string").map((tag) => tag.toLowerCase().trim().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "")).filter((tag) => tag.length > 0 && tag.length <= 30))].slice(0, 6);
120
+ } catch {}
121
+ return {
122
+ title,
123
+ transcript,
124
+ tags,
125
+ processedBy: MEDIA_VISION_MODEL
126
+ };
127
+ }
128
+ function mediaVisionPrompt() {
129
+ return [
130
+ "Reply with ONLY a JSON object: {\"title\": string, \"transcript\": string, \"tags\": string[]}.",
131
+ "title: ONE line saying what the image IS, specific not generic — 'Trenitalia ticket Rome→Florence 09:45', never 'Screenshot' or 'Image Description'.",
132
+ "transcript: ALL text visible in the image, verbatim, reading order. Empty string if there is none.",
133
+ "tags: pick from the list below. Include a tag ONLY when the image clearly shows it — no guesses.",
134
+ "Fewer tags is better; an empty array is a fine answer. Overlap is allowed.",
135
+ ...MEDIA_TAG_TAXONOMY.map(({ tag, hint }) => `- "${tag}": ${hint}`),
136
+ "You may add at most 2 extra kebab-case tags if something important has no tag above."
137
+ ].join("\n");
138
+ }
139
+ //#endregion
140
+ //#region src/starter-apps/media/processor.ts
141
+ /**
142
+ * An analysis obligation is abandoned (settled as failed, without dialing
143
+ * the AI) when its requesting event is older than this — the staleness
144
+ * doctrine's horizon. Generous on purpose: a phone can upload while the
145
+ * analyzer is broken and still get analyzed when the fix deploys; Re-analyze
146
+ * covers anything older.
147
+ */
148
+ const MEDIA_ANALYSIS_EXPIRY_MS = 1440 * 60 * 1e3;
149
+ /** Tries per attempt, with backoff between them — covers the transient
150
+ * Workers AI failure class ("8005: Internal server error") observed in prod
151
+ * without an extra durable event per retry. */
152
+ const ANALYSIS_TRIES = 3;
153
+ const ANALYSIS_RETRY_BACKOFF_MS = [2e3, 8e3];
154
+ /**
155
+ * Attempts started per caught-up pass, matching the phone's old 3-wide
156
+ * pipeline. A 19-item Delete-all re-sync once fired 19 parallel vision
157
+ * pipelines from one pass and Workers AI answered with 8004s and 60s
158
+ * binding timeouts — the in-attempt retries all landed inside the same
159
+ * overloaded burst. The rest of the queue needs no timer: every settlement
160
+ * append comes back through this processor's own delivery and the next
161
+ * caught-up pass starts the next wave.
162
+ */
163
+ const MAX_CONCURRENT_ANALYSES = 3;
164
+ const processingFields = {
165
+ title: z.string().default("").meta({ description: "One-line description of what the image IS (pre-title events fold to empty)." }),
166
+ markdown: z.string().meta({ description: "Vision-model description — half the search corpus." }),
167
+ transcript: z.string().meta({ description: "Verbatim OCR of visible text — the other half." }),
168
+ tags: z.array(z.string()).meta({ description: "Conservative multi-label tags." }),
169
+ processedBy: z.string().meta({ description: "Model id that produced this processing." })
170
+ };
171
+ const fileFields = {
172
+ stableKey: z.string().meta({ description: "Content hash — the item's identity." }),
173
+ path: z.string().meta({ description: "itx.files path holding the bytes." }),
174
+ filename: z.string().meta({ description: "Original filename as picked/synced." }),
175
+ contentType: z.string(),
176
+ width: z.number(),
177
+ height: z.number(),
178
+ source: z.string().default("picker").meta({ description: "\"picker\", \"library-sync\", or \"note\" (a note attachment)." }),
179
+ capturedAt: z.string().nullable().default(null).meta({ description: "Asset creation time (ISO) when the source knew it." }),
180
+ isScreenshot: z.boolean().nullable().default(null).meta({ description: "iOS mediaSubtypes screenshot flag." })
181
+ };
182
+ const MediaItem = z.object({
183
+ ...fileFields,
184
+ capturedEventAt: z.string().meta({ description: "Stream time the capture committed." }),
185
+ ...processingFields,
186
+ analysisError: z.string().nullable().default(null).meta({ description: "The latest analysis settlement's failure, or null. Rows born from media/uploaded exist regardless of analysis outcome; a terminal failure lands here." }),
187
+ settledRequestOffset: z.number().nullable().default(null).meta({ description: "requestOffset of the last APPLIED settlement — settlements apply monotonically, so a late one answering an older request can never overwrite a newer result (null until a requestOffset-carrying settlement lands; legacy settlements don't advance it)." })
188
+ });
189
+ const PendingAnalysis = z.object({
190
+ stableKey: z.string(),
191
+ path: z.string(),
192
+ filename: z.string(),
193
+ contentType: z.string(),
194
+ requestOffset: z.number().meta({ description: "Offset of the uploaded/reanalyze event that opened this obligation — the settlement's idempotency identity." }),
195
+ expiresAtMs: z.number().meta({ description: "Epoch ms (request createdAt + MEDIA_ANALYSIS_EXPIRY_MS); past it the obligation settles as expired without dialing the AI." })
196
+ });
197
+ const MediaProcessorContract = defineProcessorContract({
198
+ slug: "media",
199
+ version: "0.2.1",
200
+ description: "Reduces captured media (screenshots/photos) on /media into a searchable index, and drives server-side vision analysis of uploaded items (obligation pattern).",
201
+ stateSchema: z.object({
202
+ items: z.record(z.string(), MediaItem).default({}).meta({ description: "Items by stableKey, latest processing overlaid." }),
203
+ pendingAnalyses: z.record(z.string(), PendingAnalysis).default({}).meta({ description: "Open analysis obligations by stableKey (latest request wins); settled by one media/processed event each." })
204
+ }),
205
+ events: {
206
+ "events.iterate.com/media/uploaded": {
207
+ description: "A media item's bytes landed in project files: the phone appends this cheap durable fact right after files.put (metadata only — no analysis yet), idempotency-keyed by content hash + wipe generation, sharing the media/captured key scheme so an item already captured never re-uploads as a duplicate. Opens an analysis obligation.",
208
+ payloadSchema: z.object(fileFields),
209
+ examples: [{
210
+ description: "A synced screenshot, uploaded and awaiting analysis.",
211
+ payload: {
212
+ stableKey: "abc123",
213
+ path: "/media/abc123-IMG_0001.png",
214
+ filename: "IMG_0001.png",
215
+ contentType: "image/png",
216
+ width: 1170,
217
+ height: 2532,
218
+ source: "library-sync",
219
+ capturedAt: "2026-08-10T09:00:00.000Z",
220
+ isScreenshot: true
221
+ }
222
+ }]
223
+ },
224
+ "events.iterate.com/media/reanalyze-requested": {
225
+ description: "Re-analyze an existing item (the app's Re-analyze button): opens an analysis obligation for it; the settlement overlays the item's processing fields.",
226
+ payloadSchema: z.object({ stableKey: z.string() }),
227
+ examples: [{
228
+ description: "A user-requested re-run.",
229
+ payload: { stableKey: "abc123" }
230
+ }]
231
+ },
232
+ "events.iterate.com/media/captured": {
233
+ description: "LEGACY (pre media/uploaded): a media item entered the project with its first vision processing inline, appended by the old phone-driven capture script. Still folded so history keeps rendering; nothing appends it anymore.",
234
+ payloadSchema: z.object({
235
+ ...fileFields,
236
+ ...processingFields
237
+ }),
238
+ examples: [{
239
+ description: "A synced screenshot with its first processing.",
240
+ payload: {
241
+ stableKey: "abc123",
242
+ path: "/media/abc123-IMG_0001.png",
243
+ filename: "IMG_0001.png",
244
+ contentType: "image/png",
245
+ width: 1170,
246
+ height: 2532,
247
+ source: "library-sync",
248
+ capturedAt: "2026-08-10T09:00:00.000Z",
249
+ isScreenshot: true,
250
+ markdown: "A train ticket from Rome to Florence.",
251
+ transcript: "Trenitalia 09:45",
252
+ tags: ["screenshot", "logistics"],
253
+ processedBy: "@cf/meta/llama-4-scout-17b-16e-instruct"
254
+ }
255
+ }]
256
+ },
257
+ "events.iterate.com/media/wiped": {
258
+ description: "Everything before this tombstone is gone: the wipe script deleted the stored files and every derivation (phone list, this fold) resets. Deliberate per-invocation idempotency key.",
259
+ payloadSchema: z.object({
260
+ deletedFiles: z.number().meta({ description: "Files actually deleted." }),
261
+ items: z.number().meta({ description: "Items that existed at wipe time." })
262
+ }),
263
+ examples: [{
264
+ description: "A from-scratch reset.",
265
+ payload: {
266
+ deletedFiles: 24,
267
+ items: 24
268
+ }
269
+ }]
270
+ },
271
+ "events.iterate.com/media/processed": {
272
+ description: "One analysis settlement (the obligation's single terminal event): on success the processing fields overlay the item's, `error: null`; on terminal failure `error` says why and the item's previous fields stay. Also appended by legacy phone-driven re-analysis (no error/requestOffset).",
273
+ payloadSchema: z.object({
274
+ stableKey: z.string(),
275
+ ...processingFields,
276
+ error: z.string().nullable().default(null).meta({ description: "Terminal analysis failure, or null on success." }),
277
+ requestOffset: z.number().nullable().default(null).meta({ description: "The uploaded/reanalyze event this settles (null on legacy appends)." })
278
+ }),
279
+ examples: [{
280
+ description: "A successful server-side analysis of an uploaded item.",
281
+ payload: {
282
+ stableKey: "abc123",
283
+ title: "Trenitalia ticket Rome→Florence 09:45",
284
+ markdown: "A train ticket from Rome to Florence.",
285
+ transcript: "Trenitalia 09:45",
286
+ tags: ["screenshot", "logistics"],
287
+ processedBy: "@cf/meta/llama-4-scout-17b-16e-instruct",
288
+ error: null,
289
+ requestOffset: 12
290
+ }
291
+ }]
292
+ }
293
+ },
294
+ consumes: [
295
+ "events.iterate.com/media/uploaded",
296
+ "events.iterate.com/media/reanalyze-requested",
297
+ "events.iterate.com/media/captured",
298
+ "events.iterate.com/media/processed",
299
+ "events.iterate.com/media/wiped"
300
+ ],
301
+ emits: ["events.iterate.com/media/processed"]
302
+ });
303
+ var MediaProcessor = class extends StreamProcessor {
304
+ contract = MediaProcessorContract;
305
+ /**
306
+ * Runtime-only attempts by stableKey. The uploaded/reanalyze +
307
+ * processed events are the durable truth: after an eviction this set is
308
+ * empty and the next caught-up pass restarts any still-open obligation.
309
+ */
310
+ #liveAnalyses = /* @__PURE__ */ new Set();
311
+ reduce({ event, state }) {
312
+ switch (event.type) {
313
+ case "events.iterate.com/media/uploaded":
314
+ if (state.items[event.payload.stableKey] !== void 0) return state;
315
+ return {
316
+ ...state,
317
+ items: {
318
+ ...state.items,
319
+ [event.payload.stableKey]: {
320
+ ...event.payload,
321
+ capturedEventAt: event.createdAt,
322
+ title: "",
323
+ markdown: "",
324
+ transcript: "",
325
+ tags: [],
326
+ processedBy: "",
327
+ analysisError: null,
328
+ settledRequestOffset: null
329
+ }
330
+ },
331
+ pendingAnalyses: {
332
+ ...state.pendingAnalyses,
333
+ [event.payload.stableKey]: pendingAnalysisFor(event)
334
+ }
335
+ };
336
+ case "events.iterate.com/media/reanalyze-requested": {
337
+ const item = state.items[event.payload.stableKey];
338
+ if (item === void 0) return state;
339
+ return {
340
+ ...state,
341
+ pendingAnalyses: {
342
+ ...state.pendingAnalyses,
343
+ [item.stableKey]: {
344
+ stableKey: item.stableKey,
345
+ path: item.path,
346
+ filename: item.filename,
347
+ contentType: item.contentType,
348
+ requestOffset: event.offset,
349
+ expiresAtMs: Date.parse(event.createdAt) + MEDIA_ANALYSIS_EXPIRY_MS
350
+ }
351
+ }
352
+ };
353
+ }
354
+ case "events.iterate.com/media/captured":
355
+ if (state.items[event.payload.stableKey] !== void 0) return state;
356
+ return {
357
+ ...state,
358
+ items: {
359
+ ...state.items,
360
+ [event.payload.stableKey]: {
361
+ ...event.payload,
362
+ capturedEventAt: event.createdAt,
363
+ analysisError: null,
364
+ settledRequestOffset: null
365
+ }
366
+ }
367
+ };
368
+ case "events.iterate.com/media/wiped": return {
369
+ ...state,
370
+ items: {},
371
+ pendingAnalyses: {}
372
+ };
373
+ case "events.iterate.com/media/processed": {
374
+ const pendingEntry = state.pendingAnalyses[event.payload.stableKey];
375
+ const existing = state.items[event.payload.stableKey];
376
+ const requestOffset = event.payload.requestOffset === void 0 ? null : event.payload.requestOffset;
377
+ const settledFloor = Math.max(pendingEntry === void 0 ? 0 : pendingEntry.requestOffset, existing?.settledRequestOffset || 0);
378
+ if (requestOffset !== null && requestOffset < settledFloor) return state;
379
+ const { [event.payload.stableKey]: _settled, ...pendingAnalyses } = state.pendingAnalyses;
380
+ if (existing === void 0) return {
381
+ ...state,
382
+ pendingAnalyses
383
+ };
384
+ const error = event.payload.error || null;
385
+ const settledRequestOffset = requestOffset === null ? existing.settledRequestOffset : requestOffset;
386
+ const item = error === null ? {
387
+ ...existing,
388
+ title: event.payload.title,
389
+ markdown: event.payload.markdown,
390
+ transcript: event.payload.transcript,
391
+ tags: event.payload.tags,
392
+ processedBy: event.payload.processedBy,
393
+ analysisError: null,
394
+ settledRequestOffset
395
+ } : {
396
+ ...existing,
397
+ analysisError: error,
398
+ settledRequestOffset
399
+ };
400
+ return {
401
+ ...state,
402
+ items: {
403
+ ...state.items,
404
+ [event.payload.stableKey]: item
405
+ },
406
+ pendingAnalyses
407
+ };
408
+ }
409
+ default: return state;
410
+ }
411
+ }
412
+ processEvent(args) {
413
+ if (!args.delivery.caughtUp) return;
414
+ const now = this.deps.now();
415
+ const expired = [];
416
+ for (const pending of Object.values(args.state.pendingAnalyses)) {
417
+ if (this.#liveAnalyses.has(pending.stableKey)) continue;
418
+ if (now >= pending.expiresAtMs) {
419
+ expired.push(pending);
420
+ continue;
421
+ }
422
+ if (this.#liveAnalyses.size >= MAX_CONCURRENT_ANALYSES) continue;
423
+ this.#liveAnalyses.add(pending.stableKey);
424
+ args.runInBackground(async () => {
425
+ try {
426
+ const settlement = await this.#attemptAnalysis(pending);
427
+ await this.#appendSettlement(args.append, pending, settlement);
428
+ } finally {
429
+ this.#liveAnalyses.delete(pending.stableKey);
430
+ }
431
+ });
432
+ }
433
+ if (expired.length === 0) return;
434
+ args.blockProcessorWhile(async () => {
435
+ for (const pending of expired) await this.#appendSettlement(args.append, pending, { error: `analysis window expired before it could run (older than ${MEDIA_ANALYSIS_EXPIRY_MS / 36e5}h) — use Re-analyze to retry` });
436
+ });
437
+ }
438
+ /** One attempt = up to {@link ANALYSIS_TRIES} tries with short backoff. */
439
+ async #attemptAnalysis(pending) {
440
+ let lastError = "analysis failed";
441
+ for (let tryIndex = 0; tryIndex < ANALYSIS_TRIES; tryIndex++) {
442
+ if (tryIndex > 0) await this.deps.sleep(ANALYSIS_RETRY_BACKOFF_MS[Math.min(tryIndex - 1, ANALYSIS_RETRY_BACKOFF_MS.length - 1)]);
443
+ try {
444
+ return { result: await this.deps.analyze(pending) };
445
+ } catch (error) {
446
+ lastError = error instanceof Error ? error.message : String(error);
447
+ }
448
+ }
449
+ return { error: lastError.slice(0, 2e3) };
450
+ }
451
+ /**
452
+ * The obligation's single terminal event. The key is state-derived and
453
+ * deterministic per obligation (`@<stableKey>:<requestOffset>`); bodies
454
+ * contain AI output, so two racing incarnations can collide
455
+ * same-key/different-body — the first committed settlement is the
456
+ * authority and the loser tolerates the conflict (ai-linter shape).
457
+ */
458
+ async #appendSettlement(append, pending, outcome) {
459
+ const event = {
460
+ type: "events.iterate.com/media/processed",
461
+ idempotencyKey: this.idempotencyKey(`analysis-settled@${pending.stableKey}:${pending.requestOffset}`),
462
+ payload: {
463
+ stableKey: pending.stableKey,
464
+ requestOffset: pending.requestOffset,
465
+ ..."result" in outcome ? {
466
+ ...outcome.result,
467
+ error: null
468
+ } : {
469
+ title: "",
470
+ markdown: "",
471
+ transcript: "",
472
+ tags: [],
473
+ processedBy: "",
474
+ error: outcome.error
475
+ }
476
+ }
477
+ };
478
+ try {
479
+ await append(event);
480
+ } catch (error) {
481
+ if (!isIdempotencyConflict(error)) throw error;
482
+ }
483
+ }
484
+ };
485
+ function pendingAnalysisFor(event) {
486
+ return {
487
+ stableKey: event.payload.stableKey,
488
+ path: event.payload.path,
489
+ filename: event.payload.filename,
490
+ contentType: event.payload.contentType,
491
+ requestOffset: event.offset,
492
+ expiresAtMs: Date.parse(event.createdAt) + MEDIA_ANALYSIS_EXPIRY_MS
493
+ };
494
+ }
495
+ /**
496
+ * Search semantics shared with the phone's client-side filter
497
+ * (apps/mobile/src/lib/media.ts filterMedia — kept in sync by hand, the two
498
+ * runtimes can't share a module yet): every whitespace-separated term must
499
+ * appear in description, transcript, filename, stored name, or tags; every
500
+ * selected tag must be present. Newest first.
501
+ */
502
+ function searchMediaItems(state, query) {
503
+ const terms = (query.q || "").toLowerCase().split(/\s+/).filter(Boolean);
504
+ const requiredTags = query.tags || [];
505
+ return Object.values(state.items).filter((item) => {
506
+ const storedName = (item.path.split("/").at(-1) || "").replace(/^[0-9a-f]{32,}-/, "");
507
+ const haystack = `${item.title} ${item.markdown} ${item.transcript} ${item.filename} ${storedName} ${item.tags.join(" ")}`.toLowerCase();
508
+ return terms.every((term) => haystack.includes(term)) && requiredTags.every((tag) => item.tags.includes(tag));
509
+ }).sort((a, b) => (b.capturedAt || b.capturedEventAt).localeCompare(a.capturedAt || a.capturedEventAt)).slice(0, query.limit || 20);
510
+ }
511
+ //#endregion
512
+ //#region src/starter-apps/media/worker.ts
513
+ var MediaApp = class extends StreamProcessorDurableObject {
514
+ streamPath = mediaStreamPath;
515
+ /** Analysis obligations must survive eviction: the keepalive revives this
516
+ * DO and the caught-up pass restarts still-open work from reduced state. */
517
+ recovery = true;
518
+ createProcessor(deps) {
519
+ return new MediaProcessor({
520
+ ...deps,
521
+ now: Date.now,
522
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
523
+ analyze: async (input) => {
524
+ try {
525
+ var _usingCtx$1 = _usingCtx();
526
+ return await analyzeMediaImage(_usingCtx$1.u(await this.env.ITX.get()), input);
527
+ } catch (_) {
528
+ _usingCtx$1.e = _;
529
+ } finally {
530
+ _usingCtx$1.d();
531
+ }
532
+ }
533
+ });
534
+ }
535
+ /** Project-worker event delivery calls this after a durable /media event
536
+ * commits. Catch-up owns validation, ordering, checkpointing, and dedupe. */
537
+ async syncEvent(event) {
538
+ if (event.path !== "/media") return;
539
+ const registry = await this.registry();
540
+ await registry.catchUp("media");
541
+ registry.refreshLive();
542
+ }
543
+ /**
544
+ * Keyword search over descriptions, OCR transcripts, filenames, and tags —
545
+ * "train ticket" finds the train ticket because the vision model's prose
546
+ * says so. `tags` all must be present; newest first; default limit 20.
547
+ */
548
+ async search(query) {
549
+ try {
550
+ var _usingCtx3 = _usingCtx();
551
+ await (await this.registry()).catchUp("media");
552
+ const { state } = await this.snapshot();
553
+ const hits = searchMediaItems(state, query);
554
+ const project = _usingCtx3.u(await this.env.ITX.get());
555
+ return await Promise.all(hits.map(async (item) => ({
556
+ ...item,
557
+ url: await project.files.get(item.path).url()
558
+ })));
559
+ } catch (_) {
560
+ _usingCtx3.e = _;
561
+ } finally {
562
+ _usingCtx3.d();
563
+ }
564
+ }
565
+ /** Newest items, no filtering — `search` with an empty query. */
566
+ async list(query) {
567
+ return this.search({ limit: query.limit });
568
+ }
569
+ /** One item by its content hash, or null. */
570
+ async get(stableKey) {
571
+ await (await this.registry()).catchUp("media");
572
+ const { state } = await this.snapshot();
573
+ return state.items[stableKey] || null;
574
+ }
575
+ };
576
+ //#endregion
577
+ export { MediaApp };
578
+
579
+ //# sourceMappingURL=worker.mjs.map