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