@puku-ai/sdk 4.0.3 → 4.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/README.md +24 -7
  2. package/dist/index.cjs +85 -0
  3. package/dist/vendor/_vendor/partial-json-parser/parser.cjs +225 -0
  4. package/dist/vendor/api-promise.cjs +18 -0
  5. package/dist/vendor/client.cjs +1013 -0
  6. package/dist/vendor/core/api-promise.cjs +80 -0
  7. package/dist/vendor/core/api.cjs +2 -0
  8. package/dist/vendor/core/credentials.cjs +313 -0
  9. package/dist/vendor/core/error.cjs +156 -0
  10. package/dist/vendor/core/middleware.cjs +173 -0
  11. package/dist/vendor/core/pagination.cjs +215 -0
  12. package/dist/vendor/core/resource.cjs +11 -0
  13. package/dist/vendor/core/streaming.cjs +346 -0
  14. package/dist/vendor/core/uploads.cjs +5 -0
  15. package/dist/vendor/error.cjs +18 -0
  16. package/dist/vendor/helpers/beta/environments.cjs +30 -0
  17. package/dist/vendor/helpers/beta/json-schema.cjs +56 -0
  18. package/dist/vendor/helpers/beta/mcp.cjs +418 -0
  19. package/dist/vendor/helpers/beta/memory.cjs +17 -0
  20. package/dist/vendor/helpers/beta/standard-schema.cjs +57 -0
  21. package/dist/vendor/helpers/beta/zod.cjs +88 -0
  22. package/dist/vendor/helpers/index.cjs +7 -0
  23. package/dist/vendor/helpers/json-schema.cjs +36 -0
  24. package/dist/vendor/helpers/zod.cjs +77 -0
  25. package/dist/vendor/index.cjs +54 -0
  26. package/dist/vendor/internal/builtin-types.cjs +3 -0
  27. package/dist/vendor/internal/constants.cjs +12 -0
  28. package/dist/vendor/internal/decoders/jsonl.cjs +40 -0
  29. package/dist/vendor/internal/decoders/line.cjs +109 -0
  30. package/dist/vendor/internal/detect-platform.cjs +159 -0
  31. package/dist/vendor/internal/errors.cjs +42 -0
  32. package/dist/vendor/internal/file-store.cjs +535 -0
  33. package/dist/vendor/internal/headers.cjs +119 -0
  34. package/dist/vendor/internal/message-stream-utils.cjs +33 -0
  35. package/dist/vendor/internal/node.browser.cjs +21 -0
  36. package/dist/vendor/internal/node.cjs +55 -0
  37. package/dist/vendor/internal/parse.cjs +65 -0
  38. package/dist/vendor/internal/qs/formats.cjs +12 -0
  39. package/dist/vendor/internal/qs/index.cjs +13 -0
  40. package/dist/vendor/internal/qs/stringify.cjs +276 -0
  41. package/dist/vendor/internal/qs/types.cjs +2 -0
  42. package/dist/vendor/internal/qs/utils.cjs +229 -0
  43. package/dist/vendor/internal/request-options.cjs +31 -0
  44. package/dist/vendor/internal/request-signal.cjs +49 -0
  45. package/dist/vendor/internal/shim-types.cjs +3 -0
  46. package/dist/vendor/internal/shims.cjs +91 -0
  47. package/dist/vendor/internal/stainless-helper-header.cjs +90 -0
  48. package/dist/vendor/internal/stream-utils.cjs +37 -0
  49. package/dist/vendor/internal/to-file.cjs +95 -0
  50. package/dist/vendor/internal/types.cjs +3 -0
  51. package/dist/vendor/internal/uploads.cjs +148 -0
  52. package/dist/vendor/internal/utils/abort.cjs +24 -0
  53. package/dist/vendor/internal/utils/async-queue.cjs +69 -0
  54. package/dist/vendor/internal/utils/backoff.cjs +43 -0
  55. package/dist/vendor/internal/utils/base64.cjs +37 -0
  56. package/dist/vendor/internal/utils/bytes.cjs +30 -0
  57. package/dist/vendor/internal/utils/env.cjs +21 -0
  58. package/dist/vendor/internal/utils/log.cjs +111 -0
  59. package/dist/vendor/internal/utils/path.cjs +78 -0
  60. package/dist/vendor/internal/utils/promise.cjs +17 -0
  61. package/dist/vendor/internal/utils/query.cjs +41 -0
  62. package/dist/vendor/internal/utils/sleep.cjs +27 -0
  63. package/dist/vendor/internal/utils/time.cjs +7 -0
  64. package/dist/vendor/internal/utils/uuid.cjs +18 -0
  65. package/dist/vendor/internal/utils/values.cjs +125 -0
  66. package/dist/vendor/internal/utils.cjs +24 -0
  67. package/dist/vendor/lib/BetaMessageStream.cjs +665 -0
  68. package/dist/vendor/lib/MessageStream.cjs +612 -0
  69. package/dist/vendor/lib/beta-parser.cjs +78 -0
  70. package/dist/vendor/lib/credentials/credential-chain.cjs +244 -0
  71. package/dist/vendor/lib/credentials/identity-token.cjs +71 -0
  72. package/dist/vendor/lib/credentials/oidc-federation.cjs +81 -0
  73. package/dist/vendor/lib/credentials/token-cache.cjs +113 -0
  74. package/dist/vendor/lib/credentials/types.cjs +267 -0
  75. package/dist/vendor/lib/credentials/user-oauth.cjs +129 -0
  76. package/dist/vendor/lib/credentials.cjs +7 -0
  77. package/dist/vendor/lib/environments/index.cjs +17 -0
  78. package/dist/vendor/lib/environments/poller.cjs +226 -0
  79. package/dist/vendor/lib/environments/worker.cjs +609 -0
  80. package/dist/vendor/lib/helper-client.cjs +57 -0
  81. package/dist/vendor/lib/middleware.cjs +779 -0
  82. package/dist/vendor/lib/parser.cjs +65 -0
  83. package/dist/vendor/lib/sessions/accumulate.cjs +44 -0
  84. package/dist/vendor/lib/standard-schema.cjs +44 -0
  85. package/dist/vendor/lib/tools/BetaRunnableTool.cjs +30 -0
  86. package/dist/vendor/lib/tools/BetaToolRunner.cjs +500 -0
  87. package/dist/vendor/lib/tools/CompactionControl.cjs +27 -0
  88. package/dist/vendor/lib/tools/SessionToolRunner.cjs +740 -0
  89. package/dist/vendor/lib/tools/ToolError.cjs +46 -0
  90. package/dist/vendor/lib/transform-json-schema.cjs +113 -0
  91. package/dist/vendor/pagination.cjs +18 -0
  92. package/dist/vendor/resource.cjs +18 -0
  93. package/dist/vendor/resources/beta/agents/agents.cjs +159 -0
  94. package/dist/vendor/resources/beta/agents/index.cjs +8 -0
  95. package/dist/vendor/resources/beta/agents/versions.cjs +35 -0
  96. package/dist/vendor/resources/beta/agents.cjs +18 -0
  97. package/dist/vendor/resources/beta/beta.cjs +105 -0
  98. package/dist/vendor/resources/beta/deployment-runs.cjs +54 -0
  99. package/dist/vendor/resources/beta/deployments.cjs +195 -0
  100. package/dist/vendor/resources/beta/dreams.cjs +115 -0
  101. package/dist/vendor/resources/beta/environments/environments.cjs +178 -0
  102. package/dist/vendor/resources/beta/environments/index.cjs +8 -0
  103. package/dist/vendor/resources/beta/environments/work.cjs +256 -0
  104. package/dist/vendor/resources/beta/environments.cjs +18 -0
  105. package/dist/vendor/resources/beta/files.cjs +123 -0
  106. package/dist/vendor/resources/beta/index.cjs +38 -0
  107. package/dist/vendor/resources/beta/memory-stores/index.cjs +10 -0
  108. package/dist/vendor/resources/beta/memory-stores/memories.cjs +129 -0
  109. package/dist/vendor/resources/beta/memory-stores/memory-stores.cjs +173 -0
  110. package/dist/vendor/resources/beta/memory-stores/memory-versions.cjs +80 -0
  111. package/dist/vendor/resources/beta/memory-stores.cjs +18 -0
  112. package/dist/vendor/resources/beta/messages/batches.cjs +206 -0
  113. package/dist/vendor/resources/beta/messages/index.cjs +11 -0
  114. package/dist/vendor/resources/beta/messages/messages.cjs +189 -0
  115. package/dist/vendor/resources/beta/messages.cjs +18 -0
  116. package/dist/vendor/resources/beta/models.cjs +59 -0
  117. package/dist/vendor/resources/beta/organization/api-keys.cjs +55 -0
  118. package/dist/vendor/resources/beta/organization/compliance-settings.cjs +51 -0
  119. package/dist/vendor/resources/beta/organization/external-keys.cjs +118 -0
  120. package/dist/vendor/resources/beta/organization/federation/federation.cjs +49 -0
  121. package/dist/vendor/resources/beta/organization/federation/index.cjs +10 -0
  122. package/dist/vendor/resources/beta/organization/federation/issuers.cjs +168 -0
  123. package/dist/vendor/resources/beta/organization/federation/rules/index.cjs +8 -0
  124. package/dist/vendor/resources/beta/organization/federation/rules/rules.cjs +226 -0
  125. package/dist/vendor/resources/beta/organization/federation/rules/workspaces.cjs +113 -0
  126. package/dist/vendor/resources/beta/organization/federation/rules.cjs +18 -0
  127. package/dist/vendor/resources/beta/organization/federation.cjs +18 -0
  128. package/dist/vendor/resources/beta/organization/index.cjs +24 -0
  129. package/dist/vendor/resources/beta/organization/invites.cjs +75 -0
  130. package/dist/vendor/resources/beta/organization/organization.cjs +90 -0
  131. package/dist/vendor/resources/beta/organization/rate-limits.cjs +30 -0
  132. package/dist/vendor/resources/beta/organization/service-accounts/index.cjs +8 -0
  133. package/dist/vendor/resources/beta/organization/service-accounts/service-accounts.cjs +202 -0
  134. package/dist/vendor/resources/beta/organization/service-accounts/workspaces.cjs +124 -0
  135. package/dist/vendor/resources/beta/organization/service-accounts.cjs +18 -0
  136. package/dist/vendor/resources/beta/organization/users.cjs +66 -0
  137. package/dist/vendor/resources/beta/organization/workspaces/index.cjs +12 -0
  138. package/dist/vendor/resources/beta/organization/workspaces/members.cjs +101 -0
  139. package/dist/vendor/resources/beta/organization/workspaces/rate-limits.cjs +33 -0
  140. package/dist/vendor/resources/beta/organization/workspaces/service-accounts.cjs +188 -0
  141. package/dist/vendor/resources/beta/organization/workspaces/workspaces.cjs +140 -0
  142. package/dist/vendor/resources/beta/organization/workspaces.cjs +18 -0
  143. package/dist/vendor/resources/beta/organization.cjs +18 -0
  144. package/dist/vendor/resources/beta/sessions/events.cjs +119 -0
  145. package/dist/vendor/resources/beta/sessions/index.cjs +12 -0
  146. package/dist/vendor/resources/beta/sessions/resources.cjs +131 -0
  147. package/dist/vendor/resources/beta/sessions/sessions.cjs +186 -0
  148. package/dist/vendor/resources/beta/sessions/threads/events.cjs +60 -0
  149. package/dist/vendor/resources/beta/sessions/threads/index.cjs +8 -0
  150. package/dist/vendor/resources/beta/sessions/threads/threads.cjs +116 -0
  151. package/dist/vendor/resources/beta/sessions/threads.cjs +18 -0
  152. package/dist/vendor/resources/beta/sessions.cjs +18 -0
  153. package/dist/vendor/resources/beta/skills/index.cjs +8 -0
  154. package/dist/vendor/resources/beta/skills/skills.cjs +132 -0
  155. package/dist/vendor/resources/beta/skills/versions.cjs +128 -0
  156. package/dist/vendor/resources/beta/skills.cjs +18 -0
  157. package/dist/vendor/resources/beta/tunnels/certificates.cjs +128 -0
  158. package/dist/vendor/resources/beta/tunnels/index.cjs +8 -0
  159. package/dist/vendor/resources/beta/tunnels/tunnels.cjs +209 -0
  160. package/dist/vendor/resources/beta/tunnels.cjs +18 -0
  161. package/dist/vendor/resources/beta/user-profiles.cjs +117 -0
  162. package/dist/vendor/resources/beta/vaults/credentials.cjs +176 -0
  163. package/dist/vendor/resources/beta/vaults/index.cjs +8 -0
  164. package/dist/vendor/resources/beta/vaults/vaults.cjs +177 -0
  165. package/dist/vendor/resources/beta/vaults.cjs +18 -0
  166. package/dist/vendor/resources/beta/webhooks.cjs +32 -0
  167. package/dist/vendor/resources/beta.cjs +18 -0
  168. package/dist/vendor/resources/completions.cjs +22 -0
  169. package/dist/vendor/resources/files.cjs +51 -0
  170. package/dist/vendor/resources/index.cjs +31 -0
  171. package/dist/vendor/resources/messages/batches.cjs +160 -0
  172. package/dist/vendor/resources/messages/index.cjs +8 -0
  173. package/dist/vendor/resources/messages/messages.cjs +159 -0
  174. package/dist/vendor/resources/messages.cjs +18 -0
  175. package/dist/vendor/resources/models.cjs +44 -0
  176. package/dist/vendor/resources/shared.cjs +3 -0
  177. package/dist/vendor/resources/skills/index.cjs +8 -0
  178. package/dist/vendor/resources/skills/skills.cjs +72 -0
  179. package/dist/vendor/resources/skills/versions.cjs +40 -0
  180. package/dist/vendor/resources/skills.cjs +18 -0
  181. package/dist/vendor/resources/top-level.cjs +3 -0
  182. package/dist/vendor/resources.cjs +17 -0
  183. package/dist/vendor/streaming.cjs +18 -0
  184. package/dist/vendor/tools/agent-toolset/fs-util.cjs +193 -0
  185. package/dist/vendor/tools/agent-toolset/memories.cjs +916 -0
  186. package/dist/vendor/tools/agent-toolset/node.browser.cjs +147 -0
  187. package/dist/vendor/tools/agent-toolset/node.cjs +939 -0
  188. package/dist/vendor/tools/agent-toolset/skills.cjs +325 -0
  189. package/dist/vendor/tools/agent-toolset/sync-interval.cjs +28 -0
  190. package/dist/vendor/tools/memory/node.browser.cjs +48 -0
  191. package/dist/vendor/tools/memory/node.cjs +364 -0
  192. package/dist/vendor/uploads.cjs +18 -0
  193. package/dist/vendor/version.cjs +4 -0
  194. package/package.json +12 -13
  195. package/sdk.cjs +0 -14271
  196. package/sdk.cjs.map +0 -149
  197. package/sdk.d.ts +0 -10866
  198. package/sdk.mjs +0 -68
  199. package/sdk.mjs.map +0 -149
@@ -0,0 +1,916 @@
1
+ "use strict";
2
+ /**
3
+ * Session-level memory-store download and sync.
4
+ *
5
+ * A session may have several memory stores attached. This module resolves
6
+ * where each store's folder goes on disk, opens a {@link LocalFileStore}
7
+ * there, and reconciles each folder with its remote store — the merge rules
8
+ * live on {@link SessionMemoryStores}.
9
+ *
10
+ * Node-only (it sits on the filesystem-backed FileStore); like `skills.ts`,
11
+ * it is reachable through the shimmed `node.ts` entry point.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.SessionMemoryStores = exports.SessionMemoryError = exports.UPLOAD_CONCURRENCY = exports.DELETE_CORROBORATION_MS = exports.MARKER_PATH = exports.MEMORY_FLUSH_TIMEOUT_MS = exports.MIN_MEMORY_SYNC_INTERVAL_MS = exports.DEFAULT_MEMORY_SYNC_INTERVAL_MS = void 0;
15
+ const node_1 = require("../../internal/node");
16
+ const error_1 = require("../../core/error");
17
+ const log_1 = require("../../internal/utils/log");
18
+ const bytes_1 = require("../../internal/utils/bytes");
19
+ const backoff_1 = require("../../internal/utils/backoff");
20
+ const file_store_1 = require("../../internal/file-store");
21
+ const sync_interval_1 = require("./sync-interval");
22
+ var sync_interval_2 = require("./sync-interval");
23
+ Object.defineProperty(exports, "DEFAULT_MEMORY_SYNC_INTERVAL_MS", { enumerable: true, get: function () { return sync_interval_2.DEFAULT_MEMORY_SYNC_INTERVAL_MS; } });
24
+ Object.defineProperty(exports, "MIN_MEMORY_SYNC_INTERVAL_MS", { enumerable: true, get: function () { return sync_interval_2.MIN_MEMORY_SYNC_INTERVAL_MS; } });
25
+ /**
26
+ * Time bound the worker puts on each teardown pass — the final
27
+ * {@link SessionMemoryStores.finish}, then {@link SessionMemoryStores.flushWrites} —
28
+ * so a slow server cannot stall teardown.
29
+ */
30
+ exports.MEMORY_FLUSH_TIMEOUT_MS = 30_000;
31
+ /**
32
+ * Marker file stamped into every store folder; a sync trusts the folder only
33
+ * when it matches. Never itself syncs.
34
+ */
35
+ exports.MARKER_PATH = '.puku-memory-store';
36
+ const MARKER_VERSION = 1;
37
+ function markerSha(memoryStoreId) {
38
+ return node_1.crypto
39
+ .createHash('sha256')
40
+ .update(`version ${MARKER_VERSION}\n${memoryStoreId}`, 'utf-8')
41
+ .digest('hex');
42
+ }
43
+ /** How long a file must stay missing locally before its server delete goes out. */
44
+ exports.DELETE_CORROBORATION_MS = 30_000;
45
+ /**
46
+ * Page sizes for memory listings — the API's maximum per view: `basic` pages
47
+ * carry up to 100 items, `full` pages are capped by the server.
48
+ */
49
+ const LIST_PAGE_SIZE = 100;
50
+ const FULL_LIST_PAGE_SIZE = 20;
51
+ /**
52
+ * How many single-memory content fetches may be in flight at once during one
53
+ * store's pull pass. A sync rarely pulls more than a handful of memories, so
54
+ * a higher cap buys nothing in the common case.
55
+ */
56
+ const FETCH_CONCURRENCY = 16;
57
+ /**
58
+ * How many uploads one store's flush keeps in flight. At ~0.3s per upload,
59
+ * 32 clears the server's 2000-memories-per-store cap inside
60
+ * {@link MEMORY_FLUSH_TIMEOUT_MS}.
61
+ */
62
+ exports.UPLOAD_CONCURRENCY = 32;
63
+ /**
64
+ * Per-sync remote-delete cap bounds. The floor lets a small store's
65
+ * deletes clear in one pass; the ceiling caps damage on large ones.
66
+ */
67
+ const DELETE_CAP_FLOOR = 8;
68
+ const DELETE_CAP_CEILING = 50;
69
+ /**
70
+ * A session's memory stores could not be mounted.
71
+ *
72
+ * Thrown by {@link SessionMemoryStores.download} when a store cannot be
73
+ * materialised on disk, and by the environment worker when a work item for a
74
+ * session that has memory stores carried no sessions token to reach them with.
75
+ */
76
+ class SessionMemoryError extends error_1.PukuError {
77
+ constructor(message, cause) {
78
+ super(message);
79
+ this.name = 'SessionMemoryError';
80
+ // in some environments the 'cause' property is already declared
81
+ // @ts-ignore
82
+ if (cause !== undefined)
83
+ this.cause = cause;
84
+ }
85
+ }
86
+ exports.SessionMemoryError = SessionMemoryError;
87
+ /** One sync's remote-delete gate and counters. */
88
+ class DeletePass {
89
+ mode;
90
+ cap;
91
+ waiveWindow;
92
+ attempted = 0;
93
+ capped = 0;
94
+ suppressed = 0;
95
+ constructor(mode, cap,
96
+ /** Skip the delete wait — set on the session's last sync. */
97
+ waiveWindow) {
98
+ this.mode = mode;
99
+ this.cap = cap;
100
+ this.waiveWindow = waiveWindow;
101
+ }
102
+ takeSlot() {
103
+ if (this.attempted >= this.cap) {
104
+ this.capped++;
105
+ return false;
106
+ }
107
+ this.attempted++;
108
+ return true;
109
+ }
110
+ }
111
+ /**
112
+ * The memory stores attached to one session, materialised on disk.
113
+ *
114
+ * {@link SessionMemoryStores.download} opens a {@link LocalFileStore} at each
115
+ * attached store's directory (its `mount_path`, or a workdir fallback — see
116
+ * {@link SessionMemoryStores.download}), pulls its memories, and records each
117
+ * one's `content_sha256` as the sync baseline. Each sync
118
+ * ({@link SessionMemoryStores.syncIfDue} on the worker's cadence,
119
+ * {@link SessionMemoryStores.finish} once at the end) reconciles disk against
120
+ * server, per store and per path:
121
+ *
122
+ * - a memory changed only remotely is written to disk;
123
+ * - a file changed only locally is uploaded — an update with a
124
+ * `content_sha256` precondition, or a create for a new file;
125
+ * - a file changed on both sides logs a warning and takes the server version;
126
+ * - a file the server refuses (too large, invalid content) is skipped —
127
+ * warned once and retried only after the file changes; other files keep
128
+ * syncing;
129
+ * - a file deleted locally is deleted on the server after a delay and a
130
+ * re-check — never on the first sync that notices, and only up to a
131
+ * per-sync cap. `syncDeletions` gates it;
132
+ * - a memory deleted on the server is deleted on disk — unless the local
133
+ * file holds un-pushed edits: a writable store re-creates the memory
134
+ * from the file, a read-only one keeps the file unsynced;
135
+ * - a store attached read-only pulls but never pushes.
136
+ *
137
+ * A download pulls the whole store, so it lists with content included. The
138
+ * recurring syncs instead run two phases: a content-free listing (paths and
139
+ * shas) drives the merge decisions, then only the memories actually being
140
+ * written to disk are fetched, a bounded number at a time. A sync that finds
141
+ * nothing changed moves no content at all.
142
+ *
143
+ * A file whose write to disk failed is never in the baseline, so its absence
144
+ * reads as a failed download — it is pulled again, never deleted. A write
145
+ * never re-creates a store folder that vanished mid-sync: it fails, and the
146
+ * next sync's scan finds whatever is at the path by then — nothing
147
+ * (re-downloaded) or someone else's files (left alone) — under the rules
148
+ * below.
149
+ *
150
+ * A store folder that loses its {@link MARKER_PATH} marker, is emptied,
151
+ * or vanishes is re-downloaded rather than treated as a mass local
152
+ * delete; a folder whose marker names another store is left as found —
153
+ * nothing pushed, nothing deleted.
154
+ *
155
+ * Two things about the store's directory make
156
+ * {@link SessionMemoryStores.download} refuse the session outright, with
157
+ * {@link SessionMemoryError}: a `mount_path` that is not a clean absolute
158
+ * path, and a directory already sitting at that path.
159
+ *
160
+ * {@link SessionMemoryStores.download} throws on the first store it cannot
161
+ * materialise. The syncs never throw: mid-session, one bad store or one bad
162
+ * file is logged and the rest continue. Instances are not safe for concurrent
163
+ * use. The worker builds one on its token-scoped sub-client (the memory
164
+ * endpoints reject the environment key): `syncIfDue` after each tool call,
165
+ * `finish` once at a clean end, a bounded {@link SessionMemoryStores.flushWrites}
166
+ * in every teardown, `dispose` last.
167
+ */
168
+ class SessionMemoryStores {
169
+ #client;
170
+ #workdir;
171
+ #syncIntervalMs;
172
+ #syncDeletions;
173
+ #log;
174
+ #lastSyncAt;
175
+ #finished = false;
176
+ #stores = [];
177
+ constructor(client, opts) {
178
+ this.#client = client;
179
+ this.#workdir = opts.workdir;
180
+ this.#syncIntervalMs = opts.syncIntervalMs ?? sync_interval_1.DEFAULT_MEMORY_SYNC_INTERVAL_MS;
181
+ (0, sync_interval_1.checkMemorySyncInterval)(this.#syncIntervalMs, 'syncIntervalMs');
182
+ this.#syncDeletions = opts.syncDeletions ?? 'enabled';
183
+ this.#log = (0, log_1.loggerFor)(client);
184
+ this.#lastSyncAt = Date.now();
185
+ }
186
+ /**
187
+ * Every attached store's root directory.
188
+ *
189
+ * The worker lists these as the file tools' allowed roots so a store
190
+ * mounted outside the workdir stays reachable.
191
+ */
192
+ get roots() {
193
+ return this.#stores.map((s) => s.files.root().path);
194
+ }
195
+ /**
196
+ * Root directories of stores attached read-only.
197
+ *
198
+ * The file tools consult this to refuse writes into read-only stores.
199
+ */
200
+ get readOnlyRoots() {
201
+ return this.#stores.filter((s) => s.readOnly).map((s) => s.files.root().path);
202
+ }
203
+ /**
204
+ * Where one store's files land on disk.
205
+ *
206
+ * The store's files land at its `mount_path` — the very location the agent's
207
+ * system prompt tells it to read. A `mount_path` we cannot use verbatim is
208
+ * refused rather than quietly relocated: the agent would read an empty
209
+ * folder at the path it was told about, and write notes somewhere the next
210
+ * session looks for nothing.
211
+ */
212
+ #storeRoot(resource) {
213
+ if (resource.mount_path) {
214
+ if (!(0, file_store_1.isPathLegal)(resource.mount_path)) {
215
+ throw new SessionMemoryError(`memory store mount_path is not a clean absolute path: ${JSON.stringify(resource.mount_path)} ` +
216
+ `(memory_store_id=${resource.memory_store_id})`);
217
+ }
218
+ return resource.mount_path;
219
+ }
220
+ // No mount_path at all: nothing points the agent anywhere, so the workdir
221
+ // is as good a home as any.
222
+ return node_1.path.join(this.#workdir, 'memory', resource.name || resource.memory_store_id);
223
+ }
224
+ /**
225
+ * Download every attached store's memories to disk.
226
+ *
227
+ * `session` arrives already fetched — one snapshot shared with the skills
228
+ * download, so the two cannot disagree about the resources.
229
+ */
230
+ async download(session) {
231
+ for (const resource of session.resources) {
232
+ if (resource.type !== 'memory_store')
233
+ continue;
234
+ const root = this.#storeRoot(resource);
235
+ let store;
236
+ try {
237
+ store = {
238
+ memoryStoreId: resource.memory_store_id,
239
+ // utf8: a binary file is refused at put/get, not mid-sync.
240
+ files: await file_store_1.LocalFileStore.open(root, { utf8: true }),
241
+ readOnly: resource.access === 'read_only',
242
+ baseline: new Map(),
243
+ refusedShas: new Map(),
244
+ pendingDeletes: new Map(),
245
+ };
246
+ // A root `open` did not create is a dead run's leftovers; the first
247
+ // sync would upload them into the customer's store.
248
+ if (!store.files.root().removedOnDispose) {
249
+ // The configured path, not root().path — that one is resolved and
250
+ // would name a symlinked mount's target.
251
+ throw new SessionMemoryError(`something already exists at the memory store's path: ${root} ` +
252
+ `(memory_store_id=${resource.memory_store_id}); ` +
253
+ 'it must not exist when the session starts');
254
+ }
255
+ try {
256
+ await store.files.createRoot();
257
+ }
258
+ catch (e) {
259
+ if (!isErrno(e))
260
+ throw e;
261
+ // An unmountable root fails the item, not just one file.
262
+ throw new SessionMemoryError(`cannot create the memory store's folder: ${root} ` +
263
+ `(memory_store_id=${resource.memory_store_id}): ${e}; ` +
264
+ 'the worker host must make this mount path writable', e);
265
+ }
266
+ await this.#stampAndPull(store);
267
+ this.#log.info('downloaded memories', {
268
+ count: store.baseline.size,
269
+ memory_store_id: store.memoryStoreId,
270
+ dest: store.files.root().path,
271
+ });
272
+ this.#stores.push(store);
273
+ }
274
+ catch (e) {
275
+ // A half-downloaded folder self-destructs; `dispose` leaves a refused
276
+ // pre-existing directory exactly as found.
277
+ if (store)
278
+ await store.files.dispose().catch(() => { });
279
+ // Every store must land: a session missing a folder its system prompt
280
+ // names runs with amnesia and syncs nothing back.
281
+ if (e instanceof SessionMemoryError)
282
+ throw e;
283
+ throw new SessionMemoryError(`failed to download memory store memory_store_id=${resource.memory_store_id}: ${e}`, e);
284
+ }
285
+ }
286
+ this.#lastSyncAt = Date.now();
287
+ }
288
+ /**
289
+ * The session's last sync — skips the delete wait, so calling it twice
290
+ * would undo the protection; it throws instead.
291
+ */
292
+ async finish() {
293
+ if (this.#finished) {
294
+ throw new error_1.PukuError("finish() was already called: it is the session's last sync and runs once");
295
+ }
296
+ this.#finished = true;
297
+ await this.syncAll(true);
298
+ }
299
+ /** @internal — reconcile every store once; the tests' deterministic driver */
300
+ async syncAll(final) {
301
+ await Promise.all(this.#stores.map((store) => this.#syncStore(store, final)));
302
+ this.#lastSyncAt = Date.now();
303
+ }
304
+ async #scanMarker(store) {
305
+ const local = await store.files.hashtree();
306
+ const marker = local[exports.MARKER_PATH];
307
+ delete local[exports.MARKER_PATH];
308
+ if (marker === markerSha(store.memoryStoreId)) {
309
+ return { files: local, markerOk: true, distrustReason: null };
310
+ }
311
+ return {
312
+ files: local,
313
+ markerOk: false,
314
+ distrustReason: marker !== undefined ? 'the marker file does not match this store' : 'the marker file is gone',
315
+ };
316
+ }
317
+ async #syncStore(store, final) {
318
+ try {
319
+ const scan = await this.#scanMarker(store);
320
+ const local = scan.files;
321
+ if (!scan.markerOk) {
322
+ if (Object.keys(local).length > 0) {
323
+ this.#log.warn(`${scan.distrustReason}; leaving the memory store folder as found and not syncing`, {
324
+ root: store.files.root().path,
325
+ memory_store_id: store.memoryStoreId,
326
+ });
327
+ return;
328
+ }
329
+ await this.#recover(store, 'the folder or its marker is gone');
330
+ return;
331
+ }
332
+ // A lone file vanishing is an ordinary deletion; two or more at
333
+ // once with nothing left is a wiped folder.
334
+ if (Object.keys(local).length === 0 && store.baseline.size > 1) {
335
+ await this.#recover(store, 'every memory file is gone at once');
336
+ return;
337
+ }
338
+ const remote = new Map();
339
+ for await (const [rel, item] of this.#listMemories(store.memoryStoreId)) {
340
+ remote.set(rel, item);
341
+ }
342
+ const deletes = new DeletePass(this.#syncDeletions, Math.max(DELETE_CAP_FLOOR, Math.min(DELETE_CAP_CEILING, Math.floor(store.baseline.size / 4))), final);
343
+ const pulls = [];
344
+ const baseline = new Map();
345
+ const paths = [...new Set([...remote.keys(), ...Object.keys(local), ...store.baseline.keys()])].sort();
346
+ for (const rel of paths) {
347
+ const remoteItem = remote.get(rel);
348
+ const localSha = local[rel];
349
+ const baseSha = store.baseline.get(rel);
350
+ let sha;
351
+ if (localSha === undefined &&
352
+ baseSha !== undefined &&
353
+ remoteItem !== undefined &&
354
+ remoteItem.content_sha256 === baseSha &&
355
+ !store.readOnly) {
356
+ sha = await this.#corroboratedDelete(store, rel, remoteItem, baseSha, deletes);
357
+ }
358
+ else {
359
+ sha = await this.#syncPath(store, rel, remoteItem, localSha, pulls);
360
+ }
361
+ if (sha !== undefined)
362
+ baseline.set(rel, sha);
363
+ }
364
+ store.baseline = baseline;
365
+ // The content pass: everything above moved only shas.
366
+ await this.#pullAll(store, pulls);
367
+ if (deletes.suppressed > 0) {
368
+ this.#log.debug('remote deletes are disabled; locally deleted memories stay on the server', {
369
+ count: deletes.suppressed,
370
+ memory_store_id: store.memoryStoreId,
371
+ });
372
+ }
373
+ if (deletes.capped > 0) {
374
+ this.#log.warn(`delete cap reached: ${deletes.mode === 'log_only' ? 'would send' : 'sent'} ` +
375
+ `${deletes.attempted} deletes, held ${deletes.capped} for later syncs`, { memory_store_id: store.memoryStoreId });
376
+ }
377
+ }
378
+ catch (e) {
379
+ this.#log.warn('memory sync failed', { memory_store_id: store.memoryStoreId, error: String(e) });
380
+ }
381
+ }
382
+ /** Sync when `syncIntervalMs` has elapsed since the last one. Never throws. */
383
+ async syncIfDue() {
384
+ if (Date.now() - this.#lastSyncAt < this.#syncIntervalMs)
385
+ return;
386
+ await this.syncAll(false);
387
+ }
388
+ /**
389
+ * Upload new and changed files; send no deletes and pull nothing.
390
+ *
391
+ * The push-only rescue pass for a session ending on an error or
392
+ * cancel — best-effort, bounded by the caller: once `signal` aborts no
393
+ * further upload starts, each store cut off part-way logs how many
394
+ * changed files it had not finished uploading, and this resolves without
395
+ * waiting for requests already in flight. Each store uploads up to
396
+ * {@link UPLOAD_CONCURRENCY} files at a time. Skips read-only stores,
397
+ * refused files, files the server already holds, and folders that fail
398
+ * the marker check. Never throws.
399
+ */
400
+ async flushWrites(signal) {
401
+ await Promise.all(this.#stores.map((store) => this.#flushStore(store, signal)));
402
+ }
403
+ async #flushStore(store, signal) {
404
+ const dirty = new Map();
405
+ const unsent = new Set();
406
+ const push = async () => {
407
+ if (store.readOnly)
408
+ return;
409
+ const scan = await this.#scanMarker(store);
410
+ if (!scan.markerOk) {
411
+ this.#log.warn(`${scan.distrustReason}; not uploading anything from the memory store folder`, {
412
+ root: store.files.root().path,
413
+ memory_store_id: store.memoryStoreId,
414
+ });
415
+ return;
416
+ }
417
+ for (const [rel, sha] of Object.entries(scan.files)) {
418
+ if (sha !== store.baseline.get(rel) && store.refusedShas.get(rel) !== sha) {
419
+ dirty.set(rel, sha);
420
+ unsent.add(rel);
421
+ }
422
+ }
423
+ if (dirty.size === 0 || signal?.aborted)
424
+ return;
425
+ const remote = new Map();
426
+ for await (const [rel, item] of this.#listMemories(store.memoryStoreId)) {
427
+ if (signal?.aborted)
428
+ return;
429
+ remote.set(rel, item);
430
+ }
431
+ const uploads = [];
432
+ for (const rel of [...dirty.keys()].sort()) {
433
+ const localSha = dirty.get(rel);
434
+ const baseSha = store.baseline.get(rel);
435
+ const existing = remote.get(rel);
436
+ if (existing !== undefined && existing.content_sha256 === localSha) {
437
+ store.baseline.set(rel, existing.content_sha256);
438
+ unsent.delete(rel);
439
+ continue;
440
+ }
441
+ if (existing !== undefined && existing.content_sha256 !== baseSha) {
442
+ this.#log.warn('memory changed both locally and remotely; the flush leaves the remote version', {
443
+ path: rel,
444
+ memory_store_id: store.memoryStoreId,
445
+ });
446
+ unsent.delete(rel);
447
+ continue;
448
+ }
449
+ uploads.push([rel, localSha, existing]);
450
+ }
451
+ await this.#uploadAll(store, uploads, unsent, signal);
452
+ };
453
+ try {
454
+ await settledOrAborted(push(), signal);
455
+ if (signal?.aborted && unsent.size > 0) {
456
+ this.#log.warn(`memory flush cut off part-way; ${unsent.size} of ${dirty.size} changed files had not finished uploading`, { memory_store_id: store.memoryStoreId });
457
+ }
458
+ }
459
+ catch (e) {
460
+ this.#log.warn('memory flush failed', { memory_store_id: store.memoryStoreId, error: String(e) });
461
+ }
462
+ }
463
+ /**
464
+ * Remove every store directory that {@link SessionMemoryStores.download}
465
+ * created. Pre-existing directories are left alone — that is
466
+ * {@link FileStore.dispose}'s own rule. A folder that fails the marker
467
+ * check is kept too — sync left it as found, so must dispose.
468
+ */
469
+ async dispose() {
470
+ for (const store of this.#stores) {
471
+ const root = store.files.root();
472
+ try {
473
+ const scan = await this.#scanMarker(store);
474
+ if (!scan.markerOk && Object.keys(scan.files).length > 0) {
475
+ this.#log.warn(`${scan.distrustReason}; leaving the memory store folder on disk`, {
476
+ root: root.path,
477
+ memory_store_id: store.memoryStoreId,
478
+ });
479
+ continue;
480
+ }
481
+ await store.files.dispose();
482
+ }
483
+ catch (e) {
484
+ if (!(e instanceof file_store_1.FileStoreError) && !isErrno(e))
485
+ throw e;
486
+ this.#log.warn('failed to remove the memory store folder', {
487
+ root: store.files.root().path,
488
+ memory_store_id: store.memoryStoreId,
489
+ error: String(e),
490
+ });
491
+ continue;
492
+ }
493
+ if (root.removedOnDispose) {
494
+ this.#log.info('removed memory store dir', {
495
+ dest: root.path,
496
+ memory_store_id: store.memoryStoreId,
497
+ });
498
+ }
499
+ }
500
+ }
501
+ /** Rebuild a destroyed folder from the server; sends no deletes, no uploads. */
502
+ async #recover(store, reason) {
503
+ this.#log.warn(`${reason}; re-downloading the memory store folder instead of syncing`, {
504
+ root: store.files.root().path,
505
+ memory_store_id: store.memoryStoreId,
506
+ });
507
+ await store.files.createRoot();
508
+ await this.#stampAndPull(store);
509
+ }
510
+ /**
511
+ * Write the marker, then pull every remote memory. Baseline is cleared
512
+ * first so a failed write never leaves an entry whose file is not on disk.
513
+ * Every memory is needed here, so the listing carries the content — pages
514
+ * cost far fewer round-trips than a request per memory.
515
+ */
516
+ async #stampAndPull(store) {
517
+ store.baseline = new Map();
518
+ store.pendingDeletes.clear();
519
+ await store.files.put(exports.MARKER_PATH, `version ${MARKER_VERSION}\n${store.memoryStoreId}`);
520
+ for await (const [rel, item] of this.#listMemories(store.memoryStoreId, 'full')) {
521
+ if (await this.#write(store, rel, item.content ?? '')) {
522
+ store.baseline.set(rel, item.content_sha256);
523
+ }
524
+ }
525
+ }
526
+ /**
527
+ * Reconcile one path. Returns the sha to record in the baseline, or
528
+ * `undefined` to drop the path from it.
529
+ *
530
+ * `pulls` is an output: when the remote version should be written to disk,
531
+ * this appends `[rel, remote]` to it instead of writing — `rel` is the
532
+ * file to write, `remote` the listed memory whose content `#pullAll` will
533
+ * fetch and write there.
534
+ */
535
+ async #syncPath(store, rel, remote, localSha, pulls) {
536
+ const baseSha = store.baseline.get(rel);
537
+ if (localSha !== undefined) {
538
+ store.pendingDeletes.delete(rel);
539
+ }
540
+ if (!remote) {
541
+ if (localSha === undefined) {
542
+ store.pendingDeletes.delete(rel);
543
+ return undefined;
544
+ }
545
+ if (baseSha !== undefined) {
546
+ if (localSha === baseSha) {
547
+ const fresh = await this.#removeLocal(store, rel, baseSha);
548
+ if (fresh === undefined)
549
+ return undefined;
550
+ if (fresh === baseSha)
551
+ return baseSha;
552
+ localSha = fresh;
553
+ }
554
+ // The file holds an un-pushed edit — the only copy; falling
555
+ // through re-creates or keeps it.
556
+ if (store.readOnly) {
557
+ this.#log.warn('memory deleted remotely but edited locally; keeping the file, ' +
558
+ 'which a read-only store cannot push', { path: rel, memory_store_id: store.memoryStoreId });
559
+ }
560
+ else if (store.refusedShas.get(rel) !== localSha) {
561
+ this.#log.info('memory deleted remotely but edited locally; re-creating it from the file', {
562
+ path: rel,
563
+ memory_store_id: store.memoryStoreId,
564
+ });
565
+ }
566
+ }
567
+ if (store.readOnly)
568
+ return undefined;
569
+ if (store.refusedShas.get(rel) === localSha)
570
+ return undefined;
571
+ return await this.#upload(store, rel, localSha, undefined);
572
+ }
573
+ const remoteSha = remote.content_sha256;
574
+ const remoteChanged = remoteSha !== baseSha;
575
+ const locallyEdited = localSha !== undefined && localSha !== baseSha && localSha !== remoteSha;
576
+ // Read-only stores never push, so their local edits don't count.
577
+ const localChanged = !store.readOnly && locallyEdited;
578
+ if (localSha === undefined && baseSha !== undefined) {
579
+ // Only successful writes enter the baseline, so this file was verifiably
580
+ // on disk and is now gone: a real local deletion. The unchanged-remote
581
+ // case went to #corroboratedDelete.
582
+ if (remoteChanged) {
583
+ this.#log.warn('memory deleted locally but changed remotely; restoring the remote version', {
584
+ path: rel,
585
+ memory_store_id: store.memoryStoreId,
586
+ });
587
+ store.pendingDeletes.delete(rel);
588
+ pulls.push([rel, remote]);
589
+ }
590
+ return baseSha;
591
+ }
592
+ if (remoteChanged) {
593
+ // The file already holds the remote bytes — adopt without a fetch.
594
+ if (localSha === remoteSha)
595
+ return remoteSha;
596
+ // locallyEdited, not localChanged: warn on read-only overwrites too.
597
+ if (locallyEdited) {
598
+ this.#log.warn('memory changed both locally and remotely; keeping the remote version', {
599
+ path: rel,
600
+ memory_store_id: store.memoryStoreId,
601
+ });
602
+ }
603
+ pulls.push([rel, remote]);
604
+ return baseSha;
605
+ }
606
+ if (localChanged) {
607
+ if (store.refusedShas.get(rel) === localSha)
608
+ return remoteSha;
609
+ return (await this.#upload(store, rel, localSha, remote)) ?? remoteSha;
610
+ }
611
+ return remoteSha;
612
+ }
613
+ /**
614
+ * Remove the file for a memory the server no longer has, if it still holds
615
+ * `expectSha`. Returns `undefined` when the file is gone from disk,
616
+ * `expectSha` when it must stay in the baseline (I/O error), or the file's
617
+ * fresh sha when it was edited since the scan.
618
+ */
619
+ async #removeLocal(store, rel, expectSha) {
620
+ // Re-read: an edit since the scan makes this file the only copy.
621
+ let freshSha;
622
+ try {
623
+ freshSha = await store.files.hashFile(rel);
624
+ }
625
+ catch (e) {
626
+ if (!(e instanceof file_store_1.FileStoreError) && !isErrno(e))
627
+ throw e;
628
+ return expectSha;
629
+ }
630
+ if (freshSha === null)
631
+ return undefined;
632
+ if (freshSha !== expectSha)
633
+ return freshSha;
634
+ try {
635
+ await store.files.remove(rel);
636
+ }
637
+ catch (e) {
638
+ if (!(e instanceof file_store_1.FileStoreError) && !isErrno(e))
639
+ throw e;
640
+ this.#log.warn('failed to remove memory deleted remotely', {
641
+ path: rel,
642
+ memory_store_id: store.memoryStoreId,
643
+ error: String(e),
644
+ });
645
+ return expectSha;
646
+ }
647
+ return undefined;
648
+ }
649
+ /**
650
+ * Write a memory's content to disk; `false` (and a warning) on failure.
651
+ *
652
+ * A `..` component in the wire path reaches here as {@link FileStoreError} —
653
+ * that is the escape guard.
654
+ */
655
+ async #write(store, rel, content) {
656
+ try {
657
+ await store.files.put(rel, content);
658
+ }
659
+ catch (e) {
660
+ if (!(e instanceof file_store_1.FileStoreError) && !isErrno(e))
661
+ throw e;
662
+ this.#log.warn('failed to write memory', {
663
+ path: rel,
664
+ memory_store_id: store.memoryStoreId,
665
+ error: String(e),
666
+ });
667
+ return false;
668
+ }
669
+ return true;
670
+ }
671
+ /**
672
+ * Fetch and write the given memories, {@link FETCH_CONCURRENCY} at a time.
673
+ *
674
+ * The sync's content pass: the listing carried no content, so each memory
675
+ * is fetched individually and written as it arrives. On success the path's
676
+ * baseline advances; on a failed fetch or write the old entry stays and the
677
+ * next sync retries. A 404 means the memory was deleted after the listing —
678
+ * the next sync reconciles it.
679
+ */
680
+ async #pullAll(store, pulls) {
681
+ if (pulls.length === 0)
682
+ return;
683
+ const pullOne = async (rel, listed) => {
684
+ let item;
685
+ try {
686
+ item = await this.#client.beta.memoryStores.memories.retrieve(listed.id, {
687
+ memory_store_id: store.memoryStoreId,
688
+ view: 'full',
689
+ });
690
+ }
691
+ catch (e) {
692
+ if ((0, backoff_1.isStatus)(e, 404))
693
+ return;
694
+ this.#log.warn('failed to fetch memory content', {
695
+ path: rel,
696
+ memory_store_id: store.memoryStoreId,
697
+ error: String(e),
698
+ });
699
+ return;
700
+ }
701
+ if (await this.#write(store, rel, item.content ?? '')) {
702
+ store.baseline.set(rel, item.content_sha256);
703
+ }
704
+ };
705
+ // A fixed pool of workers drains the queue; the write stays inside the
706
+ // worker so a slow disk cannot let fetched bodies pile up beyond the bound.
707
+ const queue = pulls[Symbol.iterator]();
708
+ const worker = async () => {
709
+ for (const [rel, listed] of queue)
710
+ await pullOne(rel, listed);
711
+ };
712
+ await Promise.all(Array.from({ length: Math.min(FETCH_CONCURRENCY, pulls.length) }, worker));
713
+ }
714
+ /**
715
+ * Upload the given files, {@link UPLOAD_CONCURRENCY} at a time, taking each
716
+ * path off `unsent` as its upload returns. No upload starts once `signal`
717
+ * aborts; the ones already in flight run to completion.
718
+ */
719
+ async #uploadAll(store, uploads, unsent, signal) {
720
+ const queue = uploads[Symbol.iterator]();
721
+ const worker = async () => {
722
+ for (const [rel, localSha, existing] of queue) {
723
+ if (signal?.aborted)
724
+ return;
725
+ const sha = await this.#upload(store, rel, localSha, existing);
726
+ unsent.delete(rel);
727
+ if (sha !== undefined)
728
+ store.baseline.set(rel, sha);
729
+ }
730
+ };
731
+ await Promise.all(Array.from({ length: Math.min(exports.UPLOAD_CONCURRENCY, uploads.length) }, worker));
732
+ }
733
+ /**
734
+ * The store's memories keyed by relative path (the wire path's leading `/`
735
+ * stripped — `#upload` re-prefixes it) — `basic` view (shas, no content) at
736
+ * {@link LIST_PAGE_SIZE} per page unless the caller needs `full` pages.
737
+ * `memory_prefix` rollups and the reserved marker path are skipped.
738
+ */
739
+ async *#listMemories(memoryStoreId, view = 'basic') {
740
+ const limit = view === 'basic' ? LIST_PAGE_SIZE : FULL_LIST_PAGE_SIZE;
741
+ for await (const item of this.#client.beta.memoryStores.memories.list(memoryStoreId, { view, limit })) {
742
+ if (item.type !== 'memory')
743
+ continue;
744
+ const rel = item.path.replace(/^\/+/, '');
745
+ if (rel === exports.MARKER_PATH) {
746
+ this.#log.warn('the server listed the reserved marker path; skipping', {
747
+ path: item.path,
748
+ memory_store_id: memoryStoreId,
749
+ });
750
+ continue;
751
+ }
752
+ yield [rel, item];
753
+ }
754
+ }
755
+ /**
756
+ * Push one local file; `undefined` keeps the old baseline so the next pass retries.
757
+ *
758
+ * A refusal the server would repeat (400/413, the utf-8 gate) enters
759
+ * `refusedShas`: warned once, retried only after the file changes.
760
+ */
761
+ async #upload(store, rel, localSha, existing) {
762
+ try {
763
+ const data = await store.files.get(rel);
764
+ if (data === null)
765
+ return undefined;
766
+ const content = (0, bytes_1.decodeUTF8)(data);
767
+ const item = existing ?
768
+ await this.#client.beta.memoryStores.memories.update(existing.id, {
769
+ memory_store_id: store.memoryStoreId,
770
+ content,
771
+ precondition: { type: 'content_sha256', content_sha256: existing.content_sha256 },
772
+ })
773
+ : await this.#client.beta.memoryStores.memories.create(store.memoryStoreId, {
774
+ path: '/' + rel,
775
+ content,
776
+ });
777
+ store.refusedShas.delete(rel);
778
+ return item.content_sha256;
779
+ }
780
+ catch (e) {
781
+ if (existing && (0, backoff_1.isStatus)(e, 404)) {
782
+ // Deleted remotely since the listing, so this file is now the only copy.
783
+ return await this.#upload(store, rel, localSha, undefined);
784
+ }
785
+ const permanent = e instanceof file_store_1.FileStoreError || (0, backoff_1.isStatus)(e, 400) || (0, backoff_1.isStatus)(e, 413);
786
+ if (existing && (0, backoff_1.isStatus)(e, 409)) {
787
+ // The precondition lost a race: the remote moved under us, so the push
788
+ // is dropped. The local file is now stale — the next sync sees
789
+ // remoteChanged and pulls the winner over it.
790
+ this.#log.warn('memory changed both locally and remotely; the upload was refused and the local edit loses', {
791
+ path: rel,
792
+ memory_store_id: store.memoryStoreId,
793
+ });
794
+ }
795
+ else if (permanent && localSha !== undefined) {
796
+ store.refusedShas.set(rel, localSha);
797
+ this.#log.warn('the server rejected this memory file, so it stays un-synced until its content changes', { path: rel, memory_store_id: store.memoryStoreId, rejection: String(e) });
798
+ }
799
+ else {
800
+ this.#log.warn('failed to upload memory', {
801
+ path: rel,
802
+ memory_store_id: store.memoryStoreId,
803
+ error: String(e),
804
+ });
805
+ }
806
+ return undefined;
807
+ }
808
+ }
809
+ /** Send the server delete only after the wait, the cap, and a fresh re-check all clear. */
810
+ async #corroboratedDelete(store, rel, remote, baseSha, deletes) {
811
+ if (deletes.mode === 'disabled') {
812
+ deletes.suppressed++;
813
+ return baseSha;
814
+ }
815
+ let firstAbsent = store.pendingDeletes.get(rel);
816
+ if (firstAbsent === undefined) {
817
+ firstAbsent = Date.now();
818
+ store.pendingDeletes.set(rel, firstAbsent);
819
+ }
820
+ if (!deletes.waiveWindow && Date.now() - firstAbsent < exports.DELETE_CORROBORATION_MS) {
821
+ return baseSha;
822
+ }
823
+ let markerOk;
824
+ let stillAbsent;
825
+ try {
826
+ // Re-check: the folder may have been wiped mid-sync.
827
+ markerOk = (await store.files.hashFile(exports.MARKER_PATH)) === markerSha(store.memoryStoreId);
828
+ stillAbsent = (await store.files.hashFile(rel)) === null;
829
+ }
830
+ catch (e) {
831
+ if (!(e instanceof file_store_1.FileStoreError) && !isErrno(e))
832
+ throw e;
833
+ markerOk = stillAbsent = false;
834
+ }
835
+ if (!markerOk)
836
+ return baseSha;
837
+ if (!stillAbsent) {
838
+ store.pendingDeletes.delete(rel);
839
+ return baseSha;
840
+ }
841
+ if (!deletes.takeSlot())
842
+ return baseSha;
843
+ if (deletes.mode === 'log_only') {
844
+ // Repeats each sync — the log is the dry run.
845
+ this.#log.info('log-only: sync would delete this memory on the server', {
846
+ path: rel,
847
+ memory_store_id: store.memoryStoreId,
848
+ });
849
+ return baseSha;
850
+ }
851
+ const sha = await this.#deleteRemote(store, rel, remote, baseSha);
852
+ if (sha === undefined) {
853
+ store.pendingDeletes.delete(rel);
854
+ }
855
+ return sha;
856
+ }
857
+ async #deleteRemote(store, rel, remote, baseSha) {
858
+ try {
859
+ await this.#client.beta.memoryStores.memories.delete(remote.id, {
860
+ memory_store_id: store.memoryStoreId,
861
+ expected_content_sha256: baseSha,
862
+ });
863
+ }
864
+ catch (e) {
865
+ if ((0, backoff_1.isStatus)(e, 404))
866
+ return undefined; // already gone remotely too
867
+ if ((0, backoff_1.isStatus)(e, 409) || (0, backoff_1.isStatus)(e, 412)) {
868
+ this.#log.warn('memory deleted locally but changed remotely; keeping the remote version', {
869
+ path: rel,
870
+ memory_store_id: store.memoryStoreId,
871
+ });
872
+ }
873
+ else {
874
+ this.#log.warn('failed to delete memory', {
875
+ path: rel,
876
+ memory_store_id: store.memoryStoreId,
877
+ error: String(e),
878
+ });
879
+ }
880
+ return baseSha;
881
+ }
882
+ this.#log.info('propagated local deletion', { path: rel, memory_store_id: store.memoryStoreId });
883
+ return undefined;
884
+ }
885
+ }
886
+ exports.SessionMemoryStores = SessionMemoryStores;
887
+ /**
888
+ * True for a thrown value shaped like a Node filesystem error. Shape-checked,
889
+ * not `instanceof Error` — fs errors can come from another realm.
890
+ */
891
+ function isErrno(e) {
892
+ return typeof e === 'object' && e !== null && typeof e.code === 'string';
893
+ }
894
+ /**
895
+ * Resolve when `p` settles, or as soon as `signal` aborts. A rejection from
896
+ * `p` before the abort propagates; one after it is dropped.
897
+ */
898
+ async function settledOrAborted(p, signal) {
899
+ if (!signal) {
900
+ await p;
901
+ return;
902
+ }
903
+ let onAbort;
904
+ const aborted = new Promise((resolve) => {
905
+ onAbort = resolve;
906
+ if (signal.aborted)
907
+ resolve();
908
+ });
909
+ signal.addEventListener('abort', onAbort, { once: true });
910
+ try {
911
+ await Promise.race([p, aborted]);
912
+ }
913
+ finally {
914
+ signal.removeEventListener('abort', onAbort);
915
+ }
916
+ }