@xmemory/temporal 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) xmemory Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,27 @@
1
+ xmemory Temporal integration
2
+ Copyright (c) xmemory Inc.
3
+
4
+ This product is licensed under the MIT License (see LICENSE). The following
5
+ notices supplement — but do not modify — that license.
6
+
7
+ SCOPE. The MIT license covers only this Temporal integration — the source code
8
+ in this directory (the `xmemory-temporal` and `@xmemory/temporal` plugin
9
+ packages), which is a thin client over the xmemory API wired into Temporal.
10
+
11
+ PROPRIETARY SERVICE. The xmemory service that this integration talks to, and the
12
+ underlying technology behind it — including the xmemory backend, its memory
13
+ engine, schemas, extraction and reader models, APIs, and hosted
14
+ infrastructure — are proprietary to xmemory Inc. and are NOT licensed under the
15
+ MIT license. Nothing in this license grants any right to access, copy, reverse
16
+ engineer, reimplement, or create derivative works of the xmemory service or its
17
+ underlying technology. Use of the service is governed by the Terms & Conditions
18
+ at https://xmemory.ai/terms-and-conditions.html and the Privacy Policy at
19
+ https://xmemory.ai/privacy-policy.html, and requires valid credentials issued by
20
+ xmemory Inc.
21
+
22
+ TRADEMARKS. The "xmemory" name and logo are trademarks of xmemory Inc. The MIT
23
+ license does not grant permission to use them, except as required for reasonable
24
+ and customary use in describing the origin of the Software. "Temporal" is a
25
+ trademark of Temporal Technologies, Inc.; this is an independent, unofficial
26
+ integration and is not affiliated with, endorsed by, or sponsored by Temporal
27
+ Technologies, Inc.
package/README.md ADDED
@@ -0,0 +1,371 @@
1
+ # @xmemory/temporal
2
+
3
+ Durable agent memory for [Temporal](https://temporal.io) — add
4
+ [xmemory](https://xmemory.ai) reads and writes to your workflows as replay-safe
5
+ Temporal Activities, with one plugin line on your Worker.
6
+
7
+ > An agent's memory is exactly the state you don't want to lose when a worker
8
+ > crashes mid-turn. Putting xmemory behind Temporal makes a memory write a
9
+ > durable step: it survives process death, redeploys, and rolling upgrades, and
10
+ > Temporal — not your code — owns its retries and timeouts.
11
+
12
+ A Python port with the same API ships as
13
+ [`xmemory-temporal`](https://github.com/xmemory-ai/xmemory-temporal).
14
+
15
+ ### Memory is untrusted data, both ways
16
+
17
+ What goes in is user-controlled text. What comes back is that text plus whatever
18
+ the extraction engine made of it. Neither is a safe source of instructions: a read
19
+ result in a prompt is the indirect prompt-injection path, and the same string in a
20
+ shell or a query is the ordinary injection path. Quote it, bound it, and keep it
21
+ out of anything that decides what to do next.
22
+
23
+ ## What you get
24
+
25
+ - **Memory as Activities.** `read`, `write`, `writeAsyncStart` + `writeStatus` run as
26
+ Activities (all I/O stays out of workflow code, so workflows replay
27
+ deterministically).
28
+ - **A durable deep write.** `writeDurable(text)` enqueues a write and polls it to
29
+ completion from the workflow, so a multi-minute extraction survives worker
30
+ restarts — the poll state lives in workflow history, not a worker process.
31
+ - **A near-zero-diff migration.** The workflow-side handle mirrors the plain
32
+ xmemory client's methods, so agent code that already calls `inst.read(...)` /
33
+ `inst.write(...)` keeps working — it just dispatches to an Activity. Two
34
+ deliberate differences: the enqueue is `writeAsyncStart` (the client calls it
35
+ `writeAsync`), and results are projected into this package's own DTOs so a
36
+ client field rename cannot break replay of a completed workflow.
37
+ - **Temporal-owned retries and timeouts.** xmemory errors map to typed
38
+ `ApplicationFailure`s with retryable/non-retryable verdicts, so you can tune
39
+ `RetryPolicy` against stable error-type strings.
40
+ - **Opt-in auto-capture** of activity results into memory, via an Activity
41
+ interceptor that never touches the replay path.
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ npm install @xmemory/temporal
47
+ ```
48
+
49
+ Requires Node.js 22.12+ and `@temporalio/*` 1.20.
50
+
51
+ Two things to know if you type-check with `skipLibCheck: false`:
52
+
53
+ - Add `@types/ms` to your own devDependencies. `@temporalio/common` references
54
+ `ms`, which ships no types.
55
+ - Use `@types/node` 22 or 24. Temporal's own declarations do not compile against
56
+ 25 or newer (`EventEmitter<[never]>` fails its own constraint, still true in
57
+ `@temporalio/worker` 1.22.0), so those need `skipLibCheck: true` until the SDK
58
+ catches up. The runtime is unaffected — only the types.
59
+
60
+ ## Quickstart
61
+
62
+ Register the plugin on your **Worker**:
63
+
64
+ ```ts
65
+ import { NativeConnection, Worker } from '@temporalio/worker';
66
+ import { XmemoryPlugin } from '@xmemory/temporal';
67
+
68
+ const plugin = new XmemoryPlugin({ instanceId: '<your-instance-id>' }); // reads XMEM_API_KEY
69
+ const connection = await NativeConnection.connect({ address: 'localhost:7233' });
70
+ const worker = await Worker.create({
71
+ connection,
72
+ taskQueue: 'my-agent',
73
+ workflowsPath: require.resolve('./workflows'),
74
+ plugins: [plugin],
75
+ });
76
+ ```
77
+
78
+ Then call memory from inside a workflow:
79
+
80
+ ```ts
81
+ // workflows.ts
82
+ import { xmemoryForWorkflow } from '@xmemory/temporal/workflow';
83
+
84
+ export async function myWorkflow(userName: string, userMessage: string): Promise<unknown> {
85
+ const mem = xmemoryForWorkflow();
86
+ // Name whom the fact is about — a memory store has no ambient "current user".
87
+ await mem.writeDurable(`${userName}: ${userMessage}`);
88
+ return (await mem.read(`what do we know about ${userName}?`)).readerResult;
89
+ }
90
+ ```
91
+
92
+ One worker, one instance. The activities bind to whatever instance the plugin
93
+ configured, so **every worker polling a task queue must share that configuration**.
94
+ Per-tenant isolation means a task queue per tenant, not a per-workflow option; the
95
+ example above uses one shared instance and names the user in the text rather than
96
+ isolating them.
97
+
98
+ Which queue is a trust decision: derive the tenant from an authenticated identity,
99
+ never from a caller- or model-supplied value. A workflow argument naming a task
100
+ queue is a request to read someone else's memory.
101
+
102
+ Workflow code imports from **`@xmemory/temporal/workflow`**, not the package
103
+ root. That subpath is a leaf: it reaches no Activity code and no xmemory client,
104
+ so Temporal's workflow bundler accepts it. The root entry loads the plugin and
105
+ the client, which the bundler rejects by design, so it belongs in worker setup
106
+ only. A CI step bundles a workflow against the built package to keep it that
107
+ way.
108
+
109
+ > **Use one plugin instance per Worker.** The bound client lives in a per-plugin
110
+ > holder, so reusing one plugin object across two Workers is last-bind-wins.
111
+
112
+ Runnable end-to-end scripts live in [`examples/`](./examples): create an instance
113
+ with a schema, run a worker, and drive a support-agent workflow.
114
+
115
+ ## Timeouts
116
+
117
+ **The workflow owns every activity budget.** `xmemoryForWorkflow()` sets each
118
+ call's `startToCloseTimeout`, and the activity derives its xmemory client timeout
119
+ from the deadline Temporal assigned it, always a margin below, so the client gives
120
+ up first and you get an attributable xmemory error instead of an opaque Temporal
121
+ activity timeout. An activity scheduled with neither close timeout fails fast with
122
+ `XmemoryNoDeadline` rather than picking a budget of its own.
123
+
124
+ The margin covers the whole attempt, including work Temporal does before the
125
+ activity function runs. Keep it larger than your Client-level activity interceptors.
126
+ The ordering holds above timer resolution: a deadline of a few milliseconds is too
127
+ short to fit a call and a margin, and which side fires first is then a coin toss.
128
+
129
+ ```ts
130
+ const mem = xmemoryForWorkflow({
131
+ readTimeout: '60s', // a deep read on a large instance
132
+ writeTimeout: '5m',
133
+ });
134
+ ```
135
+
136
+ The client timeout is derived, not configured separately, so lowering a workflow's
137
+ budget lowers the client's with it. `DEFAULT_TIMEOUTS` supplies the defaults;
138
+ `{ clientMarginMs }` tunes the gap.
139
+
140
+ Each of those bounds one **attempt**. Nothing bounds the retry sequence unless you
141
+ say so: a server asking for an hour before the next try is honoured as asked, so a
142
+ rate-limited read can sit in retries far longer than its own timeout suggests. Set
143
+ `totalTimeout` when a call has a deadline of its own — it becomes the activity's
144
+ `scheduleToCloseTimeout`, covering every attempt.
145
+
146
+ ```ts
147
+ const mem = xmemoryForWorkflow({ readTimeout: '30s', totalTimeout: '2m' });
148
+ ```
149
+
150
+ **A timed-out write is indeterminate.** Nothing distinguishes "never arrived" from
151
+ "arrived, response lost", and the request is abandoned rather than cancelled (the
152
+ client exposes no `AbortSignal`). So write Activities default to
153
+ `maximumAttempts: 1` and surface the failure. Treat a timed-out write as *may or may
154
+ not have happened*, and reconcile with a read if it matters.
155
+
156
+ ## Durable writes
157
+
158
+ `writeDurable(text)` enqueues a deep write and polls it to completion from the
159
+ workflow, so the wait is a Temporal timer in server-side history rather than a
160
+ blocked activity slot. Redeploy the worker fleet mid-write and nothing is lost:
161
+ the poll loop resumes on the new worker and completes.
162
+
163
+ ```ts
164
+ const status = await mem.writeDurable(text, { maxWaitMs: 15 * 60_000 });
165
+ ```
166
+
167
+ Each poll adds history events — an activity, a timer, and the workflow tasks driving
168
+ them — and how many is a detail of your SDK and server versions, not something this
169
+ package can predict for you. Backoff slows the growth until the interval reaches
170
+ `maxPollIntervalMs`, after which history grows linearly with the wait. Keep
171
+ `pollIntervalMs` at seconds rather than milliseconds, since that history is shared
172
+ with the rest of your workflow; the loop warns once Temporal itself suggests
173
+ continuing as new.
174
+
175
+ Two different kinds of pacing sit here, so the names are worth separating.
176
+ `pollIntervalMs` and `maxPollIntervalMs` set how long the loop waits *between*
177
+ polls. `writeStatusRetry` sets how one poll retries when it fails — attempts and
178
+ backoff for the activity itself:
179
+
180
+ ```ts
181
+ const mem = xmemoryForWorkflow({
182
+ writeStatusRetry: { attempts: 4, intervalMs: 2_000, maxIntervalMs: 8_000 },
183
+ });
184
+ ```
185
+
186
+ Those are plain numbers rather than a Temporal `RetryPolicy`, and this package
187
+ builds the policy from them. Temporal compiles a policy when it schedules the
188
+ activity, which for the first poll is *after* the write is enqueued — so a policy it
189
+ refuses would leave a queued write nobody is watching. There is no policy to refuse
190
+ if the package builds it.
191
+
192
+ This helper cannot call `continueAsNew` for you — it runs inside *your* workflow,
193
+ and restarting that would discard your state. For multi-hour waits, run
194
+ `writeDurable` in a child workflow.
195
+
196
+ `maxWaitMs` bounds the *waiting*: every poll is scheduled to finish inside it. The
197
+ one exception is the last observation, which happens **at** the deadline so a write
198
+ that lands late is still seen rather than reported as a timeout — so a call can
199
+ return up to one status poll after `maxWaitMs`. When the server has asked for a
200
+ retry delay longer than the wait has left, that final poll is skipped instead:
201
+ arriving before the server said it would answer is worse than not looking.
202
+
203
+ For the fire-and-forget pattern (kick off several writes, keep working, join
204
+ before the turn ends), `writeAsyncStart()` and `writeStatus()` are public too.
205
+
206
+ ## Credentials never reach workflow history
207
+
208
+ The config holds the **name** of the environment variable that supplies the API
209
+ key (`XMEM_API_KEY` by default), never the key itself — so nothing secret is ever
210
+ serialized into activity arguments, which Temporal persists in the clear. Pass
211
+ the key in-process instead with `new XmemoryPlugin(config, { apiKey })` if you
212
+ prefer.
213
+
214
+ **Your memory text and queries, however, *are* in history.** Queries, written text,
215
+ and `readerResult` are activity payloads, persisted in the clear and visible in the
216
+ Web UI. The error mapping keeps raw transport strings and the server's failure
217
+ detail out of failures (set `logServerErrorDetail: true` to log the reason
218
+ worker-side), but the payloads themselves remain.
219
+
220
+ `includeContentInSummary: false` (the default) only affects the one-line activity
221
+ *summary*. For sensitive memory text, install a Temporal **Payload Codec**; this
222
+ plugin does not impose one, since a codec applies to every payload in the
223
+ namespace, not just xmemory's.
224
+
225
+ ## Replay safety and idempotency
226
+
227
+ Two things keep memory operations correct under retries and replay:
228
+
229
+ - **Replay never re-issues an operation.** All I/O is in Activities; workflow
230
+ code only schedules Activities and sleeps. Temporal replays workflow code but
231
+ never re-runs a completed Activity, so a replay never repeats a memory read or
232
+ write. The suite proves this with a forced-replay (`maxCachedWorkflows: 0`)
233
+ side-effects test.
234
+ - **Writes default to at-most-once.** Primary-key dedup looks like it would make
235
+ retries safe, but PK extraction is non-deterministic: a model normalizes the same
236
+ value differently across runs (`Dr. Robert Kim` vs `Robert Kim`), and a
237
+ disagreement forks a new row. So a lost-response retry can duplicate. Write
238
+ Activities default to `maximumAttempts: 1` and surface the failure to your
239
+ workflow. Reads and status polls are idempotent and retry generously.
240
+
241
+ **Structured writes are the reliable way to make a write retryable.** Pass
242
+ explicit mutations instead of free text and the primary key is one you supply, so
243
+ nothing is extracted and re-applying the write is deterministic.
244
+
245
+ Retrying is opted into per handle, through `writeRetryPolicy`, and an opted-in handle
246
+ is no longer at-most-once: every write through it retries, text writes and creates
247
+ included. So give keyed structured writes a handle of their own, and keep the
248
+ default handle for everything else. Always set `maximumAttempts` — Temporal reads an
249
+ omitted one as unlimited.
250
+
251
+ ```ts
252
+ const keyedWrites = xmemoryForWorkflow({ writeRetryPolicy: { maximumAttempts: 3 } });
253
+ await keyedWrites.write('', {
254
+ structuredMutations: [
255
+ {
256
+ object_mutation: {
257
+ object_type: 'Customer',
258
+ update: { key: { customerId: 'c-1' }, values: { tier: 'gold' } },
259
+ },
260
+ },
261
+ ],
262
+ });
263
+ ```
264
+
265
+ A mutation is a `create`, `update`, or `delete` on one object or relation. An
266
+ update or delete names the key it addresses, so a retry hits the same row. A create
267
+ does not — the server assigns the key — so a create is not safe to retry.
268
+
269
+ For text writes, opt into retries only when your primary keys are literal
270
+ identifiers appearing verbatim in the text, such as a `customerId` you supply. That
271
+ is a convention you keep, not something the API enforces.
272
+
273
+ [`examples/setup-memory.ts`](./examples/setup-memory.ts) shows creating an
274
+ instance with a schema.
275
+
276
+ ## Error handling
277
+
278
+ xmemory errors become `ApplicationFailure`s with stable `type` strings you can
279
+ match in a `RetryPolicy` (`nonRetryableErrorTypes: [...]`). The mapping is
280
+ derived from the server's error codes:
281
+
282
+ | xmemory condition | `type` | Retryable? |
283
+ |---|---|---|
284
+ | transport error / timeout / HTTP ≥ 500 / 408 | `XmemoryServerError` / `XmemoryUnavailable` | yes |
285
+ | `RATE_LIMITED` (429) | `XmemoryRateLimited` | yes — honors `Retry-After` |
286
+ | `QUOTA_EXCEEDED` + `daily_quota_exceeded` | `XmemoryDailyQuotaExceeded` | yes (long backoff) |
287
+ | `QUOTA_EXCEEDED` + `monthly_quota_exceeded` | `XmemoryMonthlyQuotaExceeded` | no |
288
+ | `QUOTA_EXCEEDED` (kind unknown) | `XmemoryQuotaExceeded` | no |
289
+ | `UNAUTHORIZED` / `FORBIDDEN` | `XmemoryAuthFailed` | no |
290
+ | `NOT_FOUND` | `XmemoryNotFound` | no |
291
+ | validation / conflict / schema-evolution rejections | `XmemoryBadRequest` / `XmemorySchemaRejected` | no |
292
+ | activities registered without the plugin | `XmemoryNotBound` | no |
293
+ | an activity scheduled with neither close timeout | `XmemoryNoDeadline` | no |
294
+ | durable-write options that cannot be honored | `XmemoryBadOptions` | no — the same arguments fail identically |
295
+ | an unrecognized code | `XmemoryUnknown` | follows the HTTP status |
296
+
297
+ Plus three raised by the durable write loop (`writeDurable`), from a polled
298
+ `writeStatus`, all non-retryable:
299
+
300
+ | durable-write outcome | `type` |
301
+ |---|---|
302
+ | the queued write reported `failed` | `XmemoryWriteFailed` |
303
+ | the queued write id was `not_found` | `XmemoryWriteNotFound` |
304
+ | polling exceeded `maxWait` | `XmemoryWriteTimeout` |
305
+
306
+ An unrecognized error code never raises and keeps its own type — a stricter client
307
+ that crashed on a newer server's code would break during rolling deploys. Whether it
308
+ is retried comes from the HTTP status rather than the code: 5xx, 408, 429 and a
309
+ missing status are retried, while a 4xx is terminal however it is labelled. Retrying
310
+ a 401 until the attempts run out helps nobody, and on a write it repeats a call that
311
+ cannot succeed.
312
+
313
+ > **Note.** 402 means `QUOTA_EXCEEDED` only. `TRIAL_ENDED` was removed from the
314
+ > xmemory contract when trials were retired end-to-end; do not rely on it.
315
+
316
+ ## Auto-capture (opt-in)
317
+
318
+ ```ts
319
+ const plugin = new XmemoryPlugin(
320
+ { instanceId: '<your-instance-id>' },
321
+ {
322
+ autoCapture: {
323
+ project: (activityName, result) => summarize(result), // return undefined to skip
324
+ sampleRate: 0.25,
325
+ },
326
+ },
327
+ );
328
+ ```
329
+
330
+ Off by default. It runs as an **Activity** interceptor, outside the replay path;
331
+ `project` decides what to remember, sampling bounds fan-out, and a capture failure
332
+ never fails the wrapped activity. Capture is an enqueue (`writeAsync`) clamped to
333
+ what the activity has left of its deadline, and skipped when nothing is left, so it
334
+ cannot push the activity past its `startToClose`.
335
+
336
+ Your `project` function is the exception: it is synchronous code, and JavaScript
337
+ cannot interrupt it. One that blocks longer than the activity has left will push it
338
+ past the deadline however carefully the enqueue is budgeted. Keep projections
339
+ cheap.
340
+
341
+ > **Auto-capture is at-least-once.** If a worker dies after the capture enqueue but
342
+ > before the Activity's completion is recorded, the Activity runs again and captures
343
+ > again — and since primary keys are extracted, a duplicate can fork an entity.
344
+ > Capture facts a duplicate would not corrupt.
345
+
346
+ > **Naming caveat.** Auto-capture skips any activity whose name starts with
347
+ > `xmemory_` (to avoid capturing its own writes). If you name one of *your* own
348
+ > activities `xmemory_...`, it will be silently skipped. It also never captures
349
+ > Queries.
350
+
351
+ ## Testing
352
+
353
+ ```bash
354
+ npm ci
355
+ npm run lint # tsc over src + test + examples (strict)
356
+ npm test # tsc, then node:test over test/*.test.ts
357
+ ```
358
+
359
+ The suite runs with no live backend (a fake instance is injected). See
360
+ [`TESTING.md`](./TESTING.md) for the full strategy.
361
+
362
+ ## Legal
363
+
364
+ - Privacy policy: <https://xmemory.ai/privacy-policy.html>
365
+ - Terms: <https://xmemory.ai/terms-and-conditions.html>
366
+
367
+ **MIT licensed** — see [`LICENSE`](./LICENSE). The grant covers this integration's
368
+ code only. The xmemory service and its technology remain proprietary to xmemory
369
+ Inc.; using it requires valid credentials and is governed by the Terms above. The
370
+ scope and trademark notices live in [`NOTICE`](./NOTICE), kept separate so the
371
+ package classifies cleanly as MIT.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The xmemory activities: the only place this package does I/O.
3
+ *
4
+ * The client is injected through a holder, so tests substitute a fake with no
5
+ * patching. Each call's client timeout is derived from the deadline Temporal
6
+ * assigned the activity.
7
+ */
8
+ import type { InstanceHandle } from 'xmemory';
9
+ import type { XmemoryConfig } from './config';
10
+ import { type ReadInput, type ReadOutput, type WriteInput, type WriteOutput, type WriteStartOutput, type WriteStatusInput, type WriteStatusOutput } from './dto';
11
+ import { ACTIVITY_READ, ACTIVITY_WRITE, ACTIVITY_WRITE_START, ACTIVITY_WRITE_STATUS } from './names';
12
+ export { ACTIVITY_READ, ACTIVITY_WRITE, ACTIVITY_WRITE_START, ACTIVITY_WRITE_STATUS };
13
+ /** The client methods this package calls. Injectable, so tests need no backend. */
14
+ export type XmemoryInstance = Pick<InstanceHandle, 'read' | 'write' | 'writeAsync' | 'writeStatus'>;
15
+ /** Holds the injected client so activity functions can close over it. */
16
+ export declare class InstanceHolder {
17
+ private instance;
18
+ bind(instance: XmemoryInstance): void;
19
+ get(): XmemoryInstance;
20
+ }
21
+ export interface XmemoryActivities {
22
+ [ACTIVITY_READ]: (input: ReadInput) => Promise<ReadOutput>;
23
+ [ACTIVITY_WRITE]: (input: WriteInput) => Promise<WriteOutput>;
24
+ [ACTIVITY_WRITE_START]: (input: WriteInput) => Promise<WriteStartOutput>;
25
+ [ACTIVITY_WRITE_STATUS]: (input: WriteStatusInput) => Promise<WriteStatusOutput>;
26
+ }
27
+ export declare function createActivities(holder: InstanceHolder, config: XmemoryConfig): XmemoryActivities;
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ /**
3
+ * The xmemory activities: the only place this package does I/O.
4
+ *
5
+ * The client is injected through a holder, so tests substitute a fake with no
6
+ * patching. Each call's client timeout is derived from the deadline Temporal
7
+ * assigned the activity.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.InstanceHolder = exports.ACTIVITY_WRITE_STATUS = exports.ACTIVITY_WRITE_START = exports.ACTIVITY_WRITE = exports.ACTIVITY_READ = void 0;
11
+ exports.createActivities = createActivities;
12
+ const activity_1 = require("@temporalio/activity");
13
+ const common_1 = require("@temporalio/common");
14
+ const deadline_1 = require("./deadline");
15
+ const defaults_1 = require("./defaults");
16
+ const dto_1 = require("./dto");
17
+ const errors_1 = require("./errors");
18
+ const names_1 = require("./names");
19
+ Object.defineProperty(exports, "ACTIVITY_READ", { enumerable: true, get: function () { return names_1.ACTIVITY_READ; } });
20
+ Object.defineProperty(exports, "ACTIVITY_WRITE", { enumerable: true, get: function () { return names_1.ACTIVITY_WRITE; } });
21
+ Object.defineProperty(exports, "ACTIVITY_WRITE_START", { enumerable: true, get: function () { return names_1.ACTIVITY_WRITE_START; } });
22
+ Object.defineProperty(exports, "ACTIVITY_WRITE_STATUS", { enumerable: true, get: function () { return names_1.ACTIVITY_WRITE_STATUS; } });
23
+ /** Holds the injected client so activity functions can close over it. */
24
+ class InstanceHolder {
25
+ instance;
26
+ bind(instance) {
27
+ this.instance = instance;
28
+ }
29
+ get() {
30
+ if (!this.instance) {
31
+ // The plugin's runWorker hook never bound a client. Retrying cannot fix it.
32
+ throw common_1.ApplicationFailure.create({
33
+ message: 'xmemory activities are not bound to a client — register XmemoryPlugin on the Worker ' +
34
+ 'rather than registering the activity functions directly.',
35
+ type: errors_1.TYPE_NOT_BOUND,
36
+ nonRetryable: true,
37
+ });
38
+ }
39
+ return this.instance;
40
+ }
41
+ }
42
+ exports.InstanceHolder = InstanceHolder;
43
+ /**
44
+ * What to hand the client: the text, or the mutations when there are any.
45
+ *
46
+ * Checked at runtime, because activity inputs arrive as JSON and nothing enforces
47
+ * their TypeScript types. The client's `write` and `writeAsync` are overloaded on
48
+ * the first argument and treat anything but a string as structured mutations, so a
49
+ * `text` that arrived as an array would skip extraction and apply as an update or
50
+ * delete, and a `structuredMutations` string would be written as memory.
51
+ *
52
+ * An empty list is refused too: the client answers `[]` with a plain Error, which
53
+ * the mapper reads as a retryable transport failure. Called outside the try, or
54
+ * `toApplicationFailure` would remap the verdict.
55
+ */
56
+ function requireWritePayload(input) {
57
+ const mutations = input.structuredMutations ?? undefined;
58
+ if (mutations !== undefined) {
59
+ if (!Array.isArray(mutations)) {
60
+ throw badOptions(`xmemory structuredMutations must be a list of mutations, got ${kindOf(mutations)}`);
61
+ }
62
+ if (mutations.length === 0) {
63
+ throw badOptions('xmemory write was given an empty structuredMutations list; omit it to write text instead');
64
+ }
65
+ return mutations;
66
+ }
67
+ const text = input.text;
68
+ if (typeof text !== 'string') {
69
+ throw badOptions(`xmemory write text must be a string, got ${kindOf(text)}`);
70
+ }
71
+ return text;
72
+ }
73
+ // The kind only, never the value: failure messages are persisted to history.
74
+ function kindOf(value) {
75
+ return value === null ? 'null' : Array.isArray(value) ? 'an array' : typeof value;
76
+ }
77
+ function badOptions(message) {
78
+ return common_1.ApplicationFailure.create({ message, type: errors_1.TYPE_BAD_OPTIONS, nonRetryable: true });
79
+ }
80
+ function createActivities(holder, config) {
81
+ const defaultLogic = config.defaultExtractionLogic ?? 'fast';
82
+ /**
83
+ * Client budget for the running activity, from its own deadline. Derived rather
84
+ * than kept as a second worker-side copy, so "the client gives up first" holds
85
+ * even when a workflow lowers its timeout.
86
+ */
87
+ const budgetMs = () => {
88
+ const info = activity_1.Context.current().info;
89
+ const deadlineMs = (0, deadline_1.activityBudgetMs)(info);
90
+ if (deadlineMs === null) {
91
+ throw common_1.ApplicationFailure.create({
92
+ message: `activity ${info.activityType} was scheduled without a deadline: ` +
93
+ 'set startToCloseTimeout or scheduleToCloseTimeout on it.',
94
+ type: errors_1.TYPE_NO_DEADLINE,
95
+ nonRetryable: true,
96
+ });
97
+ }
98
+ if (deadlineMs <= 0) {
99
+ // Temporal has given up on this attempt; a token budget would send a request
100
+ // nobody reads, and a write the server could still accept.
101
+ throw common_1.ApplicationFailure.create({
102
+ message: `activity ${info.activityType} is past its deadline`,
103
+ type: errors_1.TYPE_DEADLINE_EXPIRED,
104
+ });
105
+ }
106
+ return (0, defaults_1.clientTimeoutMs)(deadlineMs, config.clientMarginMs);
107
+ };
108
+ const writeOptions = (input, timeoutMs) => ({
109
+ extractionLogic: (input.extractionLogic ?? defaultLogic),
110
+ ...(input.diffEngine !== undefined ? { diffEngine: input.diffEngine } : {}),
111
+ timeoutMs,
112
+ });
113
+ return {
114
+ async [names_1.ACTIVITY_READ](input) {
115
+ // Outside the try: an unbound-client or missing-deadline failure must keep
116
+ // its non-retryable type rather than being re-mapped by toApplicationFailure.
117
+ const instance = holder.get();
118
+ const timeoutMs = budgetMs();
119
+ try {
120
+ const result = await (0, deadline_1.withDeadline)(instance.read(input.query, {
121
+ ...(input.readMode !== undefined ? { readMode: input.readMode } : {}),
122
+ ...(input.scope !== undefined ? { scope: input.scope } : {}),
123
+ timeoutMs,
124
+ }), timeoutMs);
125
+ return (0, dto_1.projectRead)(result);
126
+ }
127
+ catch (err) {
128
+ throw (0, errors_1.toApplicationFailure)(err);
129
+ }
130
+ },
131
+ async [names_1.ACTIVITY_WRITE](input) {
132
+ const instance = holder.get();
133
+ const ms = budgetMs();
134
+ const payload = requireWritePayload(input);
135
+ try {
136
+ // A structured write carries its own keys, so the server applies it without
137
+ // running the extractor; text and extractionLogic are moot.
138
+ const result = await (0, deadline_1.withDeadline)(typeof payload === 'string'
139
+ ? instance.write(payload, writeOptions(input, ms))
140
+ : instance.write(payload, { timeoutMs: ms }), ms);
141
+ return (0, dto_1.projectWrite)(result);
142
+ }
143
+ catch (err) {
144
+ throw (0, errors_1.toApplicationFailure)(err);
145
+ }
146
+ },
147
+ async [names_1.ACTIVITY_WRITE_START](input) {
148
+ const instance = holder.get();
149
+ const ms = budgetMs();
150
+ const payload = requireWritePayload(input);
151
+ try {
152
+ return (0, dto_1.projectWriteStart)(await (0, deadline_1.withDeadline)(typeof payload === 'string'
153
+ ? instance.writeAsync(payload, writeOptions(input, ms))
154
+ : instance.writeAsync(payload, { timeoutMs: ms }), ms));
155
+ }
156
+ catch (err) {
157
+ throw (0, errors_1.toApplicationFailure)(err);
158
+ }
159
+ },
160
+ async [names_1.ACTIVITY_WRITE_STATUS](input) {
161
+ const instance = holder.get();
162
+ const timeoutMs = budgetMs();
163
+ const writeId = input.writeId;
164
+ try {
165
+ const result = await (0, deadline_1.withDeadline)(instance.writeStatus(writeId, { timeoutMs }), timeoutMs);
166
+ const projected = (0, dto_1.projectWriteStatus)(result);
167
+ if (projected.writeId !== writeId) {
168
+ // Before logging: another write's detail would otherwise be logged under
169
+ // the id we asked about, and its outcome read as ours.
170
+ throw new Error('xmemory returned a status for a different write');
171
+ }
172
+ const errorDetail = result.error_detail;
173
+ if (errorDetail) {
174
+ // Never the return value: history keeps Activity results in the clear.
175
+ // Not verbatim in the log either unless asked for — the detail is not
176
+ // promised user-safe, and logs travel.
177
+ activity_1.Context.current().log.warn('xmemory write failed',
178
+ // `=== true`, not truthiness: this config can come from a file or an
179
+ // env var, and the string "false" would otherwise turn the detail on.
180
+ config.logServerErrorDetail === true
181
+ ? { writeId, errorDetail }
182
+ : {
183
+ writeId,
184
+ errorDetailLength: errorDetail.length,
185
+ note: 'detail withheld from logs and history; set logServerErrorDetail to include it',
186
+ });
187
+ }
188
+ return projected;
189
+ }
190
+ catch (err) {
191
+ throw (0, errors_1.toApplicationFailure)(err);
192
+ }
193
+ },
194
+ };
195
+ }