@ixo/oracle-runtime-workers 0.2.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 +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- package/src/tasks/store.ts +351 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import type { BotStatus } from '@ixo/matrix-bot-workers-sdk';
|
|
2
|
+
import type { AttachmentInput } from '../attachments/types';
|
|
3
|
+
import type { RealtimeStatus } from '../realtime/realtime-endpoint';
|
|
4
|
+
/**
|
|
5
|
+
* Cross-object contracts for the Workers runtime.
|
|
6
|
+
*
|
|
7
|
+
* Topology (one Worker deployment = one oracle):
|
|
8
|
+
*
|
|
9
|
+
* Worker (Hono shell) ──HTTP──▶ UserOracleDO (one per user DID)
|
|
10
|
+
* ▲ ▼ RPC
|
|
11
|
+
* Matrix homeserver ◀──/sync──▶ MatrixGatewayDO (one per oracle)
|
|
12
|
+
*
|
|
13
|
+
* - `UserOracleDO` owns the user's SQLite working copy (LangGraph checkpoints,
|
|
14
|
+
* sessions, transcript), runs the agent turn, and streams SSE back to the
|
|
15
|
+
* shell. Its durable copy of the SQLite file lives in an `OwnerStore` the
|
|
16
|
+
* user controls (their Matrix room today, their IXO VFS going forward).
|
|
17
|
+
* - `MatrixGatewayDO` owns the bot identity: the E2EE crypto store, the sync
|
|
18
|
+
* loop, room→user routing, and media upload/download on behalf of user
|
|
19
|
+
* objects (it is the only object that can encrypt/decrypt).
|
|
20
|
+
*
|
|
21
|
+
* Everything below is plain data so it survives DO RPC / `fetch` boundaries.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Bindings every oracle Worker must declare (see the example `wrangler.jsonc`). */
|
|
25
|
+
export interface OracleWorkerEnv {
|
|
26
|
+
USER_ORACLE: DurableObjectNamespace<UserOracleObject>;
|
|
27
|
+
MATRIX_GATEWAY: DurableObjectNamespace<MatrixGatewayObject>;
|
|
28
|
+
|
|
29
|
+
// --- identity -----------------------------------------------------------
|
|
30
|
+
ORACLE_NAME: string;
|
|
31
|
+
/** UCAN audience — the DID users address invocations/delegations to. */
|
|
32
|
+
ORACLE_DID: string;
|
|
33
|
+
/** On-chain entity DID of the oracle (identity surfaced to the agent). */
|
|
34
|
+
ORACLE_ENTITY_DID?: string;
|
|
35
|
+
NETWORK?: 'mainnet' | 'testnet' | 'devnet';
|
|
36
|
+
|
|
37
|
+
// --- auth ---------------------------------------------------------------
|
|
38
|
+
/** Blocksync GraphQL endpoint used to resolve `did:ixo` verification keys. */
|
|
39
|
+
BLOCKSYNC_GRAPHQL_URL: string;
|
|
40
|
+
UCAN_AUTH_MAX_TTL_SECONDS?: string;
|
|
41
|
+
|
|
42
|
+
// --- matrix -------------------------------------------------------------
|
|
43
|
+
MATRIX_BASE_URL: string;
|
|
44
|
+
MATRIX_ORACLE_ADMIN_USER_ID: string;
|
|
45
|
+
/** The bot's password: the gateway logs its own device in with it (and a second, crypto-less device for plugins). */
|
|
46
|
+
MATRIX_ORACLE_ADMIN_PASSWORD: string;
|
|
47
|
+
/** SSSS recovery passphrase — lets a fresh device restore room keys from backup. */
|
|
48
|
+
MATRIX_RECOVERY_PHRASE?: string;
|
|
49
|
+
/** Matrix server name used when composing per-user room aliases. */
|
|
50
|
+
MATRIX_HOMESERVER_NAME?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Outgoing-message pacing for the oracle bot (gateway), passed to
|
|
53
|
+
* `@ixo/matrix-bot-workers-sdk`. Mirror the homeserver's `rc_message`
|
|
54
|
+
* (`per_second` / `burst_count`, infra default 3 / 40);
|
|
55
|
+
* `MATRIX_SEND_CONCURRENCY` caps parallel sends across rooms (SDK default
|
|
56
|
+
* 4). Unset = the SDK defaults.
|
|
57
|
+
*/
|
|
58
|
+
MATRIX_SEND_RATE_PER_SECOND?: string;
|
|
59
|
+
MATRIX_SEND_BURST?: string;
|
|
60
|
+
MATRIX_SEND_CONCURRENCY?: string;
|
|
61
|
+
/** Room turns in flight at once in the gateway (each ends in an encrypted reply; default 4). */
|
|
62
|
+
MATRIX_TURN_CONCURRENCY?: string;
|
|
63
|
+
/** Idle recycle of the gateway object after this many sends (default 300; 0 disables). */
|
|
64
|
+
MATRIX_RECYCLE_AFTER_SENDS?: string;
|
|
65
|
+
/** Rooms kept fully built in memory (SDK default 64; idle rooms are released). */
|
|
66
|
+
MATRIX_HOT_ROOMS?: string;
|
|
67
|
+
/** Cap on events replayed per room after a restart (SDK default 0 = unbounded). */
|
|
68
|
+
MATRIX_BACKFILL_MAX_EVENTS?: string;
|
|
69
|
+
|
|
70
|
+
// --- storage ------------------------------------------------------------
|
|
71
|
+
/**
|
|
72
|
+
* Where the user-owned SQLite file lives. Default (unset) = `vfs`: the IXO
|
|
73
|
+
* VFS is the system of record, with the Matrix room media checked once as a
|
|
74
|
+
* legacy source and migrated in. `matrix` forces legacy room-media storage
|
|
75
|
+
* (dev/harness environments without a VFS worker).
|
|
76
|
+
*/
|
|
77
|
+
OWNER_STORE?: 'matrix' | 'vfs';
|
|
78
|
+
/**
|
|
79
|
+
* Per-object budget of clean 64 KiB chunks the DO VFS keeps in memory
|
|
80
|
+
* (bytes, or `4m`/`4096k`; default 8 MiB, range 1–64 MiB). All user
|
|
81
|
+
* objects of a script share one 128 MB isolate — see `GET /debug/storage`
|
|
82
|
+
* for the hit/miss counters that show what a budget buys.
|
|
83
|
+
*/
|
|
84
|
+
CHUNK_CACHE_BYTES?: string;
|
|
85
|
+
/** IXO VFS worker base URL when `OWNER_STORE=vfs` (defaults per NETWORK). */
|
|
86
|
+
VFS_BASE_URL?: string;
|
|
87
|
+
/** UCAN store worker base URL when `OWNER_STORE=vfs` (defaults per NETWORK). */
|
|
88
|
+
UCAN_STORE_URL?: string;
|
|
89
|
+
|
|
90
|
+
// --- ops ----------------------------------------------------------------
|
|
91
|
+
/**
|
|
92
|
+
* Slack incoming-webhook URL for operator alerts (currently: a user's
|
|
93
|
+
* working copy crossing each GB watermark from 1 GB toward the 10 GB DO
|
|
94
|
+
* storage cap). Unset = alerts off.
|
|
95
|
+
*/
|
|
96
|
+
SLACK_ALERT_WEBHOOK_URL?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Cloudflare native rate-limit binding, keyed per authenticated user DID
|
|
99
|
+
* (limit/period are set on the binding in `wrangler.jsonc`). Absent in the
|
|
100
|
+
* local harness and vitest pool, where the shell degrades to no limiting.
|
|
101
|
+
*/
|
|
102
|
+
RATE_LIMIT?: RateLimit;
|
|
103
|
+
|
|
104
|
+
// --- llm ----------------------------------------------------------------
|
|
105
|
+
OPEN_ROUTER_API_KEY: string;
|
|
106
|
+
DEFAULT_MODEL?: string;
|
|
107
|
+
/** Display markup for `GET /models` prices (default 1.6). */
|
|
108
|
+
MODEL_PRICE_MARKUP?: string;
|
|
109
|
+
MAIN_REASONING_EFFORT?: string;
|
|
110
|
+
/** Platform model provider. Default `openrouter`; `nebius` for self-hosted. */
|
|
111
|
+
LLM_PROVIDER?: 'openrouter' | 'nebius';
|
|
112
|
+
/** Nebius Token Factory API key — required when `LLM_PROVIDER=nebius`. */
|
|
113
|
+
NEBIUS_API_KEY?: string;
|
|
114
|
+
|
|
115
|
+
// --- langsmith tracing ---------------------------------------------------
|
|
116
|
+
/** `'true'` traces every turn (explicit tracer — Workers has no env auto-attach). */
|
|
117
|
+
LANGSMITH_TRACING?: string;
|
|
118
|
+
LANGSMITH_API_KEY?: string;
|
|
119
|
+
LANGSMITH_PROJECT?: string;
|
|
120
|
+
/** LangSmith API endpoint override (self-hosted / EU). */
|
|
121
|
+
LANGSMITH_ENDPOINT?: string;
|
|
122
|
+
/**
|
|
123
|
+
* Comma-separated DID allowlist for selective per-user tracing (`*` traces
|
|
124
|
+
* everyone). Requires `LANGSMITH_API_KEY`; ignored when `LANGSMITH_TRACING`
|
|
125
|
+
* is `'true'` (global mode already traces every turn).
|
|
126
|
+
*/
|
|
127
|
+
LANGSMITH_TRACED_DIDS?: string;
|
|
128
|
+
|
|
129
|
+
// --- byo-llm -------------------------------------------------------------
|
|
130
|
+
/** `'true'` enables bring-your-own-credential LLMs (`/byo-llm/*` surface). */
|
|
131
|
+
BYO_LLM_ENABLED?: string;
|
|
132
|
+
/** ChatGPT-subscription backend override (a transparent proxy off Cloudflare egress). */
|
|
133
|
+
BYO_CHATGPT_BACKEND_URL?: string;
|
|
134
|
+
BYO_CHATGPT_PROXY_AUTH_TOKEN?: string;
|
|
135
|
+
/** Override of the public ChatGPT OAuth client id (Codex CLI client). */
|
|
136
|
+
BYO_CHATGPT_CLIENT_ID?: string;
|
|
137
|
+
|
|
138
|
+
// --- secrets -------------------------------------------------------------
|
|
139
|
+
/**
|
|
140
|
+
* The oracle's Matrix *account* room — where the CLI published the P-256
|
|
141
|
+
* encryption key (`ixo.room.encryption_key.index` / `p256_encryption`).
|
|
142
|
+
* Only the gateway (holding the bot session) can read it.
|
|
143
|
+
*/
|
|
144
|
+
MATRIX_ACCOUNT_ROOM_ID?: string;
|
|
145
|
+
/** PIN the published private JWK is AES-256-CBC-encrypted with. */
|
|
146
|
+
MATRIX_VALUE_PIN?: string;
|
|
147
|
+
|
|
148
|
+
// --- misc ---------------------------------------------------------------
|
|
149
|
+
LOG_LEVEL?: string;
|
|
150
|
+
CORS_ORIGIN?: string;
|
|
151
|
+
|
|
152
|
+
/** Plugin-declared env is read through the same object. */
|
|
153
|
+
[key: string]: unknown;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* RPC payload rule: Durable Object RPC types reject `unknown`/deeply nested
|
|
158
|
+
* generic shapes, so anything nested (event content, transcripts, metadata)
|
|
159
|
+
* crosses the boundary as a JSON **string**. Flat records of primitives are
|
|
160
|
+
* passed as objects.
|
|
161
|
+
*/
|
|
162
|
+
export type JsonString = string;
|
|
163
|
+
|
|
164
|
+
/** Who is talking and through which transport. */
|
|
165
|
+
export interface TurnIdentity {
|
|
166
|
+
/** Validated user DID (from the UCAN signer or the Matrix sender mapping). */
|
|
167
|
+
userDid: string;
|
|
168
|
+
/** `@did-ixo-…:server` when known. */
|
|
169
|
+
matrixUserId?: string;
|
|
170
|
+
/** Raw UCAN delegation CAR forwarded by the client (HTTP turns only). */
|
|
171
|
+
ucanDelegation?: string;
|
|
172
|
+
/** Its expiration (unix seconds) when the shell validated one. */
|
|
173
|
+
ucanDelegationExpiration?: number;
|
|
174
|
+
timezone?: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Request the shell / gateway sends to `UserOracleDO` to run one turn. */
|
|
178
|
+
export interface TurnRequest {
|
|
179
|
+
identity: TurnIdentity;
|
|
180
|
+
sessionId: string;
|
|
181
|
+
message: string;
|
|
182
|
+
client: 'portal' | 'matrix';
|
|
183
|
+
/** Matrix room the turn arrived in (Matrix turns) or the user's DM room. */
|
|
184
|
+
roomId?: string;
|
|
185
|
+
/** Thread root when the user replied inside a thread. */
|
|
186
|
+
threadId?: string;
|
|
187
|
+
/** The user's message event this turn answers (Matrix turns): anchors the `work_status` card. */
|
|
188
|
+
eventId?: string;
|
|
189
|
+
requestId: string;
|
|
190
|
+
/** Per-request model override (validated against the catalog). */
|
|
191
|
+
model?: string;
|
|
192
|
+
/** JSON-encoded free-form metadata forwarded by the client. */
|
|
193
|
+
metadata?: JsonString;
|
|
194
|
+
/** Files attached to the message (validated `AttachmentDto` shape). */
|
|
195
|
+
attachments?: AttachmentInput[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Non-streaming turn result. */
|
|
199
|
+
export interface TurnResult {
|
|
200
|
+
sessionId: string;
|
|
201
|
+
requestId: string;
|
|
202
|
+
/** Final assistant text. */
|
|
203
|
+
text: string;
|
|
204
|
+
/** Id of the final assistant message, when one was produced. */
|
|
205
|
+
messageId?: string;
|
|
206
|
+
/** Tool calls made during the turn (name + summarized status), for logging. */
|
|
207
|
+
toolCalls: Array<{ name: string; status: 'done' | 'error' }>;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Row shape served by `GET /sessions`. */
|
|
211
|
+
export interface SessionSummary {
|
|
212
|
+
sessionId: string;
|
|
213
|
+
title?: string;
|
|
214
|
+
createdAt: string;
|
|
215
|
+
lastUpdatedAt: string;
|
|
216
|
+
roomId?: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** `search_memory_engine`'s raw upstream schema + whether it converts to Zod. */
|
|
220
|
+
export interface MemorySchemaSnapshot {
|
|
221
|
+
tool: string;
|
|
222
|
+
toolCount: number;
|
|
223
|
+
rawSchema: string;
|
|
224
|
+
convertible: boolean;
|
|
225
|
+
capturedAt: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** `GET /debug/memory-schema` payload. */
|
|
229
|
+
export type MemorySchemaDebug = MemorySchemaSnapshot | { error: string };
|
|
230
|
+
|
|
231
|
+
/** `GET /debug/storage`: the user object's working-copy and owner-store state. */
|
|
232
|
+
export interface StorageStatus {
|
|
233
|
+
/**
|
|
234
|
+
* Raw upstream schema of the memory engine's `search_memory_engine` tool
|
|
235
|
+
* as this isolate last received it, plus whether it converts to Zod —
|
|
236
|
+
* evidence for "did the model see the enum" questions. Present only after
|
|
237
|
+
* a memory turn ran in this isolate.
|
|
238
|
+
*/
|
|
239
|
+
memorySchemaDump?: MemorySchemaSnapshot | null;
|
|
240
|
+
/** Random per in-memory instance; changes whenever the platform unloaded and re-created the object. */
|
|
241
|
+
instanceId: string;
|
|
242
|
+
/** Milliseconds since this instance was constructed. */
|
|
243
|
+
instanceUptimeMs: number;
|
|
244
|
+
fileBytes: number;
|
|
245
|
+
pages: number;
|
|
246
|
+
ownerEtag?: string;
|
|
247
|
+
lastFlushAt?: number;
|
|
248
|
+
dirty: boolean;
|
|
249
|
+
flushInFlight: boolean;
|
|
250
|
+
/** VFS write generation now vs. at the last successful upload. */
|
|
251
|
+
writeGeneration?: number;
|
|
252
|
+
uploadedGeneration?: number;
|
|
253
|
+
lastChecksum?: string;
|
|
254
|
+
flushFailures: number;
|
|
255
|
+
lastVacuumAt?: number;
|
|
256
|
+
/** True once the user's legacy Matrix copy has been removed or found absent. */
|
|
257
|
+
legacyCleared?: boolean;
|
|
258
|
+
/** Background session-history indexing runs in progress (keeps the object resident). */
|
|
259
|
+
indexingInFlight?: number;
|
|
260
|
+
/** Turns currently running in this object (streaming or not). */
|
|
261
|
+
activeTurns?: number;
|
|
262
|
+
/** Live JavaScript timers (debug routes only) — any keeps the object resident. */
|
|
263
|
+
pendingTimers?: number;
|
|
264
|
+
lastAccessAt?: number;
|
|
265
|
+
alarmAt: number | null;
|
|
266
|
+
pageCount?: number;
|
|
267
|
+
freelistCount?: number;
|
|
268
|
+
/** SQLite's own page cache (`PRAGMA cache_size`; negative = KiB). */
|
|
269
|
+
sqliteCacheSize?: number;
|
|
270
|
+
/** The DO VFS clean-chunk cache and its storage counters (isolate lifetime). */
|
|
271
|
+
chunkCache?: {
|
|
272
|
+
budgetBytes: number;
|
|
273
|
+
capacityChunks: number;
|
|
274
|
+
cachedBytes: number;
|
|
275
|
+
hits: number;
|
|
276
|
+
misses: number;
|
|
277
|
+
storageReads: number;
|
|
278
|
+
rowsRead: number;
|
|
279
|
+
storageWrites: number;
|
|
280
|
+
rowsWritten: number;
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* RPC surface of `UserOracleDO` (methods callable on the stub). HTTP-shaped
|
|
286
|
+
* calls (streaming turns) go through `fetch()` instead so the SSE body can be
|
|
287
|
+
* piped straight to the client.
|
|
288
|
+
*/
|
|
289
|
+
export interface UserOracleObject extends Rpc.DurableObjectBranded {
|
|
290
|
+
/** Run a turn and return the final text (used by the Matrix gateway). */
|
|
291
|
+
runTurn(req: TurnRequest): Promise<TurnResult>;
|
|
292
|
+
createSession(
|
|
293
|
+
identity: TurnIdentity,
|
|
294
|
+
opts?: { roomId?: string; sessionId?: string },
|
|
295
|
+
): Promise<SessionSummary>;
|
|
296
|
+
listSessions(
|
|
297
|
+
identity: TurnIdentity,
|
|
298
|
+
opts?: { limit?: number; offset?: number },
|
|
299
|
+
): Promise<{ sessions: SessionSummary[]; total: number }>;
|
|
300
|
+
deleteSession(identity: TurnIdentity, sessionId: string): Promise<boolean>;
|
|
301
|
+
/** JSON-encoded `MessageDto[]` for `GET /messages/:sessionId`. */
|
|
302
|
+
listMessages(identity: TurnIdentity, sessionId: string): Promise<JsonString>;
|
|
303
|
+
abortTurn(sessionId: string): Promise<boolean>;
|
|
304
|
+
/** Adopt a freshly deposited delegation for header-less turns (no boot). */
|
|
305
|
+
setDelegation(
|
|
306
|
+
userDid: string,
|
|
307
|
+
raw: string,
|
|
308
|
+
expiration?: number,
|
|
309
|
+
): Promise<void>;
|
|
310
|
+
/** Forget the cached delegation at once (revocation). */
|
|
311
|
+
clearDelegation(userDid: string): Promise<void>;
|
|
312
|
+
/**
|
|
313
|
+
* Close the Matrix `work_status` card of a turn once its reply has been
|
|
314
|
+
* posted (or the turn ended without one). No-op for unregistered turns.
|
|
315
|
+
*/
|
|
316
|
+
finishWorkStatus(
|
|
317
|
+
requestId: string,
|
|
318
|
+
phase: 'done' | 'superseded',
|
|
319
|
+
): Promise<void>;
|
|
320
|
+
/** Operator probe: what header-less turns would mint from. */
|
|
321
|
+
delegationStatus(userDid: string): Promise<{
|
|
322
|
+
present: boolean;
|
|
323
|
+
expiration?: number;
|
|
324
|
+
source: 'memory' | 'storage' | 'none';
|
|
325
|
+
}>;
|
|
326
|
+
/** Force an owner-copy export now (tests / admin). */
|
|
327
|
+
flushToOwnerStore(): Promise<{
|
|
328
|
+
uploaded: boolean;
|
|
329
|
+
bytes: number;
|
|
330
|
+
etag?: string;
|
|
331
|
+
/** Why nothing was sent: no write since the last upload, or same bytes. */
|
|
332
|
+
skipped?: 'unchanged-generation' | 'unchanged-checksum';
|
|
333
|
+
/** The upload went to legacy Matrix media after repeated VFS failures. */
|
|
334
|
+
fallback?: 'matrix';
|
|
335
|
+
}>;
|
|
336
|
+
/**
|
|
337
|
+
* Drop the working copy (pages + caches) so the next access re-imports from
|
|
338
|
+
* the owner store. Simulates eviction/loss of the DO cache in tests and is
|
|
339
|
+
* the "user deleted their file upstream" path.
|
|
340
|
+
*/
|
|
341
|
+
resetWorkingCopy(): Promise<{ reloadedFromOwnerStore: boolean }>;
|
|
342
|
+
/** Raw upstream schema of the memory search tool as fetched right now. */
|
|
343
|
+
debugMemorySchema(userDid: string): Promise<MemorySchemaDebug>;
|
|
344
|
+
/** Raw session row for `GET /debug/sessions/:id` (null when unknown). */
|
|
345
|
+
debugSession(
|
|
346
|
+
userDid: string,
|
|
347
|
+
sessionId: string,
|
|
348
|
+
): Promise<Record<string, unknown> | null>;
|
|
349
|
+
/** Diagnostics: file size, page count, owner-store state, cache counters. */
|
|
350
|
+
storageStatus(): Promise<StorageStatus>;
|
|
351
|
+
/** Realtime (socket.io) channel: attached sockets and pending browser calls. */
|
|
352
|
+
realtimeStatus(): Promise<RealtimeStatus>;
|
|
353
|
+
/**
|
|
354
|
+
* Diagnostics: the object's current alarm and the user's task records — the
|
|
355
|
+
* pair that answers "is this task going to fire, and when?".
|
|
356
|
+
*/
|
|
357
|
+
tasksStatus(): Promise<{
|
|
358
|
+
now: number;
|
|
359
|
+
alarm: number | null;
|
|
360
|
+
schedulerActive: boolean;
|
|
361
|
+
tasks: Array<{
|
|
362
|
+
id: string;
|
|
363
|
+
title: string;
|
|
364
|
+
status: string;
|
|
365
|
+
nextRunAt: string | null;
|
|
366
|
+
lastResult: { ok: boolean; summary: string; at: string } | null;
|
|
367
|
+
consecutiveFailures: number;
|
|
368
|
+
deliveryRoomId: string | null;
|
|
369
|
+
}>;
|
|
370
|
+
}>;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** A private room the bot creates and owns (dedicated `[Task]` rooms). */
|
|
374
|
+
export interface CreateRoomOptions {
|
|
375
|
+
name: string;
|
|
376
|
+
topic?: string;
|
|
377
|
+
/** Matrix ids to invite (the user, typically). */
|
|
378
|
+
invite: string[];
|
|
379
|
+
/** The user this room belongs to: replies in it route to their object. */
|
|
380
|
+
userDid: string;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* RPC surface of `MatrixGatewayDO`. User objects call it for anything that
|
|
385
|
+
* needs the bot identity (sending, media, room lookups); the shell calls
|
|
386
|
+
* `ensureStarted()` on boot/health so the sync loop is running. The
|
|
387
|
+
* lifecycle, send and state members are the bot SDK's (`MatrixBotDO`); the
|
|
388
|
+
* rest is the oracle's.
|
|
389
|
+
*/
|
|
390
|
+
export interface MatrixGatewayObject extends Rpc.DurableObjectBranded {
|
|
391
|
+
/** Bytes of a media event (decrypted when it carries an encrypted `file`); null when unknown. */
|
|
392
|
+
downloadEventMedia(
|
|
393
|
+
roomId: string,
|
|
394
|
+
eventId: string,
|
|
395
|
+
): Promise<{
|
|
396
|
+
bytes: Uint8Array;
|
|
397
|
+
mimetype?: string;
|
|
398
|
+
filename?: string;
|
|
399
|
+
} | null>;
|
|
400
|
+
/** Authenticated media download of an `mxc://` URI. */
|
|
401
|
+
downloadMxcMedia(mxc: string): Promise<Uint8Array>;
|
|
402
|
+
/** Stop the bot (operator/debug; `ensureStarted`/cron brings it back). */
|
|
403
|
+
stop(): Promise<void>;
|
|
404
|
+
/** Idempotent — starts the bot + keep-alive alarm if not running. */
|
|
405
|
+
ensureStarted(): Promise<{
|
|
406
|
+
started: boolean;
|
|
407
|
+
userId: string;
|
|
408
|
+
deviceId: string;
|
|
409
|
+
}>;
|
|
410
|
+
/**
|
|
411
|
+
* Stop the bot and start it again from its persisted state — the same
|
|
412
|
+
* device, the same keys. Operators after a bad state; the matrix tests.
|
|
413
|
+
*/
|
|
414
|
+
restart(): Promise<{ started: boolean; userId: string; deviceId: string }>;
|
|
415
|
+
/**
|
|
416
|
+
* Log the bot in as a NEW device (one-time-key conflict recovery) and retire
|
|
417
|
+
* the old one; resolves once the new device serves.
|
|
418
|
+
*/
|
|
419
|
+
rotateDevice(
|
|
420
|
+
reason?: string,
|
|
421
|
+
): Promise<{ started: boolean; userId: string; deviceId: string }>;
|
|
422
|
+
/** Durable outbox rows (no bodies), oldest first. */
|
|
423
|
+
listOutbox(): Promise<OutboxRow[]>;
|
|
424
|
+
/**
|
|
425
|
+
* Credentials of the oracle's second, crypto-less device for plugins that
|
|
426
|
+
* run their own Matrix client (editor, flows). Logged in once with the
|
|
427
|
+
* account password, kept in the gateway's storage, re-logged in with the
|
|
428
|
+
* same device id if the homeserver ever rejects the token.
|
|
429
|
+
*/
|
|
430
|
+
botCredentials(): Promise<BotCredentials>;
|
|
431
|
+
status(): Promise<GatewayStatus>;
|
|
432
|
+
/** Send a text message (encrypted when the room is). Returns the event id. */
|
|
433
|
+
sendText(
|
|
434
|
+
roomId: string,
|
|
435
|
+
body: string,
|
|
436
|
+
opts?: {
|
|
437
|
+
threadId?: string;
|
|
438
|
+
formattedBody?: string;
|
|
439
|
+
/**
|
|
440
|
+
* `interactive` (default: markers, replies, notices) is dispatched
|
|
441
|
+
* ahead of `background` (HTTP-turn room replays).
|
|
442
|
+
*/
|
|
443
|
+
priority?: 'interactive' | 'background';
|
|
444
|
+
/**
|
|
445
|
+
* Persist in the gateway's outbox until acknowledged and re-issue
|
|
446
|
+
* after a restart (default true). `false` for the session marker,
|
|
447
|
+
* whose event id the caller consumes synchronously.
|
|
448
|
+
*/
|
|
449
|
+
durable?: boolean;
|
|
450
|
+
/** Caller-pinned transaction id (idempotent retries across restarts). */
|
|
451
|
+
txnId?: string;
|
|
452
|
+
},
|
|
453
|
+
): Promise<string>;
|
|
454
|
+
/** Send an arbitrary timeline event (`content` JSON-encoded). Returns the event id. */
|
|
455
|
+
sendEvent(roomId: string, type: string, content: JsonString): Promise<string>;
|
|
456
|
+
/** Resolve the canonical user↔oracle room for a user DID (alias lookup, cached). */
|
|
457
|
+
resolveUserRoom(
|
|
458
|
+
userDid: string,
|
|
459
|
+
): Promise<{ roomId: string; alias: string } | null>;
|
|
460
|
+
/** Create a private bot-owned room (dedicated `[Task]` rooms) and invite users. */
|
|
461
|
+
createDedicatedRoom(opts: CreateRoomOptions): Promise<{ roomId: string }>;
|
|
462
|
+
/**
|
|
463
|
+
* Upload a user's SQLite snapshot as room media (`m.ixo.media_upload` +
|
|
464
|
+
* `m.ixo.media_state[storageKey]`), encrypting when the room is E2EE.
|
|
465
|
+
* Returns the new event id. Wire-compatible with the Node runtime so
|
|
466
|
+
* existing users migrate transparently.
|
|
467
|
+
*/
|
|
468
|
+
uploadUserSnapshot(
|
|
469
|
+
userDid: string,
|
|
470
|
+
storageKey: string,
|
|
471
|
+
bytes: Uint8Array,
|
|
472
|
+
filename: string,
|
|
473
|
+
): Promise<{ eventId: string }>;
|
|
474
|
+
/** Download the latest snapshot for `storageKey`, or null when none exists. */
|
|
475
|
+
downloadUserSnapshot(
|
|
476
|
+
userDid: string,
|
|
477
|
+
storageKey: string,
|
|
478
|
+
): Promise<{ bytes: Uint8Array; eventId: string } | null>;
|
|
479
|
+
/** Read a room state event's content, JSON-encoded (state is never E2EE). Null when absent. */
|
|
480
|
+
getRoomStateEvent(
|
|
481
|
+
roomId: string,
|
|
482
|
+
type: string,
|
|
483
|
+
stateKey?: string,
|
|
484
|
+
): Promise<JsonString | null>;
|
|
485
|
+
/** Write a room state event (`content` JSON-encoded). Returns the event id. */
|
|
486
|
+
sendStateEvent(
|
|
487
|
+
roomId: string,
|
|
488
|
+
type: string,
|
|
489
|
+
content: JsonString,
|
|
490
|
+
stateKey?: string,
|
|
491
|
+
): Promise<string>;
|
|
492
|
+
/** Full room state as a JSON-encoded array of `{ type, state_key, content, sender, event_id }`. */
|
|
493
|
+
getRoomState(roomId: string): Promise<JsonString>;
|
|
494
|
+
/** One timeline event (decrypted when E2EE) as JSON `{ event_id, type, content, sender, origin_server_ts }`, or null. */
|
|
495
|
+
getEvent(roomId: string, eventId: string): Promise<JsonString | null>;
|
|
496
|
+
/**
|
|
497
|
+
* The oracle's P-256 secrets key, JSON-encoded private JWK. Read from the
|
|
498
|
+
* oracle's Matrix *account* room (`MATRIX_ACCOUNT_ROOM_ID`) — the published
|
|
499
|
+
* `ixo.room.encryption_key.index` state names the timeline event carrying
|
|
500
|
+
* the PIN-encrypted private key, decrypted here with `MATRIX_VALUE_PIN`.
|
|
501
|
+
* Null when the key is not provisioned or the env vars are unset. User
|
|
502
|
+
* objects fetch it once at boot and seat it into their secrets service.
|
|
503
|
+
*/
|
|
504
|
+
getOracleSecretsKey(): Promise<JsonString | null>;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export interface BotCredentials {
|
|
508
|
+
baseUrl: string;
|
|
509
|
+
userId: string;
|
|
510
|
+
accessToken: string;
|
|
511
|
+
deviceId: string;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export interface OutboxRow {
|
|
515
|
+
txnId: string;
|
|
516
|
+
roomId: string;
|
|
517
|
+
kind: 'text' | 'notice';
|
|
518
|
+
threadId?: string;
|
|
519
|
+
bodyChars: number;
|
|
520
|
+
htmlChars: number;
|
|
521
|
+
attempts: number;
|
|
522
|
+
enqueuedAt: number;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** `MatrixBotDO.status()` plus the gateway's own turn bookkeeping. */
|
|
526
|
+
export interface GatewayStatus extends BotStatus {
|
|
527
|
+
turns: { inFlight: number; waiting: number };
|
|
528
|
+
/** Debounce buffers waiting to become turns. */
|
|
529
|
+
ingestPending: number;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Derive the durable-object name for a user's object. */
|
|
533
|
+
export function userObjectName(userDid: string, oracleDid: string): string {
|
|
534
|
+
return `${oracleDid}::${userDid}`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Storage key for a user's checkpoint snapshot — identical to the Node runtime. */
|
|
538
|
+
export async function checkpointStorageKey(
|
|
539
|
+
userDid: string,
|
|
540
|
+
oracleDid: string,
|
|
541
|
+
): Promise<string> {
|
|
542
|
+
const data = new TextEncoder().encode(`checkpoint_${userDid}_${oracleDid}`);
|
|
543
|
+
const digest = await crypto.subtle.digest('SHA-256', data);
|
|
544
|
+
return Array.from(new Uint8Array(digest))
|
|
545
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
546
|
+
.join('')
|
|
547
|
+
.slice(0, 17);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** `did:ixo:ixo1abc` → `did-ixo-ixo1abc` (room-alias form, same as the Node runtime). */
|
|
551
|
+
export function didToAliasPart(did: string): string {
|
|
552
|
+
return did.replace(/:/g, '-');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Canonical user↔oracle room alias local part: `<user>_<oracle>`. */
|
|
556
|
+
export function userOracleRoomAlias(
|
|
557
|
+
userDid: string,
|
|
558
|
+
oracleDid: string,
|
|
559
|
+
serverName: string,
|
|
560
|
+
): string {
|
|
561
|
+
return `#${didToAliasPart(userDid)}_${didToAliasPart(oracleDid)}:${serverName}`;
|
|
562
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- patching the global timer functions */
|
|
2
|
+
/**
|
|
3
|
+
* Debug-only bookkeeping of the isolate's pending JavaScript timers.
|
|
4
|
+
*
|
|
5
|
+
* A Durable Object holding hibernatable WebSockets is only hibernated while
|
|
6
|
+
* it is idle; a pending `setTimeout`/`setInterval` keeps it resident (and
|
|
7
|
+
* billed). Finding the timer that pins an object means knowing which timers
|
|
8
|
+
* exist — the platform offers no introspection, so with debug routes on the
|
|
9
|
+
* object patches the four timer globals once per isolate and records every
|
|
10
|
+
* live timer with its creation site. Read via `GET /debug/realtime`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface PendingTimer {
|
|
14
|
+
id: number;
|
|
15
|
+
kind: 'timeout' | 'interval';
|
|
16
|
+
delayMs: number;
|
|
17
|
+
ageMs: number;
|
|
18
|
+
/** First frames of the creating stack, trimmed. */
|
|
19
|
+
createdAt: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface TimerRecord {
|
|
23
|
+
kind: 'timeout' | 'interval';
|
|
24
|
+
delayMs: number;
|
|
25
|
+
createdAtMs: number;
|
|
26
|
+
stack: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Under `nodejs_compat` the timer globals return Node-style `Timeout`
|
|
30
|
+
// objects, not numbers — those cannot cross the RPC boundary, so every live
|
|
31
|
+
// timer is keyed by our own sequence number and the handle is kept aside.
|
|
32
|
+
const live = new Map<number, TimerRecord>();
|
|
33
|
+
const seqOfHandle = new Map<unknown, number>();
|
|
34
|
+
let seq = 0;
|
|
35
|
+
let installed = false;
|
|
36
|
+
|
|
37
|
+
function stackOf(): string[] {
|
|
38
|
+
const raw = new Error().stack ?? '';
|
|
39
|
+
return raw
|
|
40
|
+
.split('\n')
|
|
41
|
+
.slice(3, 9)
|
|
42
|
+
.map((l) => l.trim())
|
|
43
|
+
.filter((l) => l.length > 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function track(
|
|
47
|
+
kind: 'timeout' | 'interval',
|
|
48
|
+
handle: unknown,
|
|
49
|
+
delayMs: number,
|
|
50
|
+
): number {
|
|
51
|
+
const id = ++seq;
|
|
52
|
+
live.set(id, { kind, delayMs, createdAtMs: Date.now(), stack: stackOf() });
|
|
53
|
+
seqOfHandle.set(handle, id);
|
|
54
|
+
return id;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function untrack(handle: unknown): void {
|
|
58
|
+
const id = seqOfHandle.get(handle);
|
|
59
|
+
if (id === undefined) return;
|
|
60
|
+
seqOfHandle.delete(handle);
|
|
61
|
+
live.delete(id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function installTimerTracker(): void {
|
|
65
|
+
if (installed) return;
|
|
66
|
+
installed = true;
|
|
67
|
+
const g = globalThis as any;
|
|
68
|
+
const origSetTimeout = g.setTimeout.bind(g);
|
|
69
|
+
const origSetInterval = g.setInterval.bind(g);
|
|
70
|
+
const origClearTimeout = g.clearTimeout.bind(g);
|
|
71
|
+
const origClearInterval = g.clearInterval.bind(g);
|
|
72
|
+
g.setTimeout = (
|
|
73
|
+
fn: (...a: unknown[]) => void,
|
|
74
|
+
delay?: number,
|
|
75
|
+
...args: unknown[]
|
|
76
|
+
) => {
|
|
77
|
+
const handle: unknown = origSetTimeout(
|
|
78
|
+
(...a: unknown[]) => {
|
|
79
|
+
untrack(handle);
|
|
80
|
+
fn(...a);
|
|
81
|
+
},
|
|
82
|
+
delay,
|
|
83
|
+
...args,
|
|
84
|
+
);
|
|
85
|
+
track('timeout', handle, delay ?? 0);
|
|
86
|
+
return handle;
|
|
87
|
+
};
|
|
88
|
+
g.setInterval = (
|
|
89
|
+
fn: (...a: unknown[]) => void,
|
|
90
|
+
delay?: number,
|
|
91
|
+
...args: unknown[]
|
|
92
|
+
) => {
|
|
93
|
+
const handle: unknown = origSetInterval(fn, delay, ...args);
|
|
94
|
+
track('interval', handle, delay ?? 0);
|
|
95
|
+
return handle;
|
|
96
|
+
};
|
|
97
|
+
g.clearTimeout = (handle?: unknown) => {
|
|
98
|
+
if (handle !== undefined) untrack(handle);
|
|
99
|
+
return origClearTimeout(handle);
|
|
100
|
+
};
|
|
101
|
+
g.clearInterval = (handle?: unknown) => {
|
|
102
|
+
if (handle !== undefined) untrack(handle);
|
|
103
|
+
return origClearInterval(handle);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function pendingTimers(): PendingTimer[] {
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
return [...live.entries()].map(([id, t]) => ({
|
|
110
|
+
id,
|
|
111
|
+
kind: t.kind,
|
|
112
|
+
delayMs: t.delayMs,
|
|
113
|
+
ageMs: now - t.createdAtMs,
|
|
114
|
+
createdAt: t.stack,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function timerTrackerInstalled(): boolean {
|
|
119
|
+
return installed;
|
|
120
|
+
}
|