@pulsarinteractive/geniustasker 0.1.0-beta.4 → 0.1.0-beta.6

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.
@@ -0,0 +1,517 @@
1
+ import * as z from 'zod/v4';
2
+ import { posix } from 'node:path';
3
+ import { DocumentationStore, DocumentationDownloads, digest } from './documentation-store.mjs';
4
+ import { readMemoryTree, readMemoryMarkdown } from './documentation.mjs';
5
+ import { exportGraph } from './documentation-graph.mjs';
6
+ import { fail } from './errors.mjs';
7
+
8
+ const id = z.string().regex(/^[a-zA-Z0-9_-]{1,128}$/);
9
+ const sourceSchema = z
10
+ .object({
11
+ scope: z.string().regex(/^memory:[a-zA-Z0-9_-]{1,128}$/),
12
+ parentId: id.optional(),
13
+ pages: z.boolean().default(true),
14
+ graph: z.boolean().default(false),
15
+ })
16
+ .strict()
17
+ .refine((source) => source.pages || source.graph, { message: 'Select pages or Graph.' });
18
+ export const documentationConfigurationSchema = z
19
+ .object({
20
+ formatVersion: z.literal(1),
21
+ sources: z.array(sourceSchema).min(1).max(50),
22
+ media: z.enum(['references', 'local']).default('references'),
23
+ })
24
+ .strict()
25
+ .refine(
26
+ (value) => new Set(value.sources.map((source) => source.scope)).size === value.sources.length,
27
+ {
28
+ message: 'Choose one subtree per Memory scope.',
29
+ },
30
+ );
31
+ export const documentationSyncSchema = z
32
+ .object({
33
+ directory: z.string().min(1).max(4096),
34
+ configuration: documentationConfigurationSchema.optional(),
35
+ maxFiles: z.number().int().min(1).max(100000).optional(),
36
+ maxBytes: z.number().int().min(1024).max(4294967296).optional(),
37
+ })
38
+ .strict();
39
+ export const documentationDirectorySchema = z
40
+ .object({ directory: z.string().min(1).max(4096) })
41
+ .strict();
42
+ const parse = (schema, value) => {
43
+ const result = schema.safeParse(value);
44
+ if (!result.success) {
45
+ fail('invalid_arguments', 'Use the documented repository documentation configuration.');
46
+ }
47
+ return result.data;
48
+ };
49
+ const denied = (error) =>
50
+ error.status === 403 ||
51
+ ['forbidden', 'agent_permission_denied', 'scope_not_found', 'memory_page_not_found'].includes(
52
+ error.code,
53
+ );
54
+ const slug = (title) =>
55
+ title
56
+ .normalize('NFKD')
57
+ .replace(/[\u0300-\u036f]/g, '')
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9]+/g, '-')
60
+ .replace(/^-|-$/g, '')
61
+ .slice(0, 48) || 'document';
62
+ const escape = (value) =>
63
+ String(value)
64
+ .replace(/[\r\n]/g, ' ')
65
+ .replace(/[\\`*_[\]<>|]/g, '\\$&');
66
+ const contextOf = (tree) => ({ epoch: tree.epoch, checkpoint: tree.checkpoint });
67
+ const equivalentAccess = (before, tree) =>
68
+ before?.epoch === tree.epoch && before.accessRevision === tree.accessRevision;
69
+ const extension = (type) =>
70
+ ({
71
+ 'image/png': 'png',
72
+ 'image/jpeg': 'jpg',
73
+ 'image/webp': 'webp',
74
+ 'image/gif': 'gif',
75
+ 'image/avif': 'avif',
76
+ 'application/pdf': 'pdf',
77
+ 'text/plain': 'txt',
78
+ })[type] ?? 'bin';
79
+
80
+ async function collectPages(client, source, first, previous) {
81
+ if (!source.pages) {
82
+ return {};
83
+ }
84
+ const old = previous?.pages ?? {},
85
+ pages = Object.create(null);
86
+ if (first.notModified) {
87
+ if (!previous || !equivalentAccess(previous, first)) {
88
+ fail('invalid_response', 'An unchanged source has no matching local tree.');
89
+ }
90
+ return structuredClone(old);
91
+ }
92
+ const oldChildren = new Map();
93
+ for (const page of Object.values(old)) {
94
+ const children = oldChildren.get(page.parentId) ?? [];
95
+ children.push(page);
96
+ oldChildren.set(page.parentId, children);
97
+ }
98
+ const retain = (root) => {
99
+ const queue = [root],
100
+ seen = new Set();
101
+ while (queue.length) {
102
+ const page = queue.pop();
103
+ if (seen.has(page.id)) {
104
+ fail('invalid_documentation_manifest', 'A cached branch contains a cycle.');
105
+ }
106
+ seen.add(page.id);
107
+ pages[page.id] = page;
108
+ queue.push(...(oldChildren.get(page.id) ?? []));
109
+ }
110
+ };
111
+ const pending = [first];
112
+ if (first.parent) {
113
+ pages[first.parent.id] = first.parent;
114
+ }
115
+ const visited = new Set();
116
+ while (pending.length) {
117
+ let tree = pending.pop();
118
+ const key = tree.parentId ?? '';
119
+ if (visited.has(key)) {
120
+ fail('invalid_response', 'The remote tree repeats a branch.');
121
+ }
122
+ visited.add(key);
123
+ while (true) {
124
+ if (tree.accessRevision !== first.accessRevision) {
125
+ fail('read_context_changed', 'Memory access changed during tree traversal.');
126
+ }
127
+ for (const page of tree.items) {
128
+ if (Object.hasOwn(pages, page.id)) {
129
+ fail('invalid_response', 'The remote tree repeats a page.');
130
+ }
131
+ const before = old[page.id];
132
+ if (before?.subtreeVersion === page.subtreeVersion && equivalentAccess(previous, first)) {
133
+ retain(page);
134
+ } else {
135
+ pages[page.id] = page;
136
+ if (page.childCount) {
137
+ pending.push(
138
+ await readMemoryTree(client, {
139
+ scope: source.scope,
140
+ parentId: page.id,
141
+ ...contextOf(first),
142
+ }),
143
+ );
144
+ }
145
+ }
146
+ if (Object.keys(pages).length > 10000) {
147
+ fail('documentation_capacity', 'A source exceeds its supported page capacity.');
148
+ }
149
+ }
150
+ if (!tree.nextAfter) {
151
+ break;
152
+ }
153
+ tree = await readMemoryTree(client, {
154
+ scope: source.scope,
155
+ parentId: tree.parentId ?? undefined,
156
+ after: tree.nextAfter,
157
+ ...contextOf(first),
158
+ });
159
+ }
160
+ }
161
+ return pages;
162
+ }
163
+
164
+ function documentPaths(source, pages) {
165
+ const root = source.scope.replace(':', '-'),
166
+ paths = Object.create(null);
167
+ for (const page of Object.values(pages)) {
168
+ const ancestors = [],
169
+ seen = new Set();
170
+ let current = page;
171
+ while (current) {
172
+ if (seen.has(current.id) || seen.size > 12) {
173
+ fail('invalid_response', 'The exported tree contains an invalid hierarchy.');
174
+ }
175
+ seen.add(current.id);
176
+ ancestors.unshift(`${slug(current.title).slice(0, 24)}--${digest(current.id).slice(0, 20)}`);
177
+ if (current.id === source.parentId || current.parentId === null) {
178
+ break;
179
+ }
180
+ current = pages[current.parentId];
181
+ if (!current) {
182
+ fail('invalid_response', 'The exported page has no readable parent.');
183
+ }
184
+ }
185
+ paths[page.id] = `${root}/pages/${ancestors.join('/')}/index.md`;
186
+ }
187
+ return paths;
188
+ }
189
+
190
+ async function localAsset(client, store, source, tree, assetId, previous, downloads) {
191
+ const page = await client.get('resources', {
192
+ scope: source.scope,
193
+ collection: 'assets',
194
+ id: assetId,
195
+ ...contextOf(tree),
196
+ });
197
+ if (
198
+ page.scope !== source.scope ||
199
+ page.collection !== 'assets' ||
200
+ page.epoch !== tree.epoch ||
201
+ page.checkpoint !== tree.checkpoint ||
202
+ page.items?.length !== 1 ||
203
+ page.omittedIds?.length ||
204
+ page.items[0].id !== assetId
205
+ ) {
206
+ fail(
207
+ 'incomplete_documentation',
208
+ 'A referenced media resource is not available in this source context.',
209
+ );
210
+ }
211
+ const asset = page.items[0],
212
+ data = asset.data;
213
+ const url = /^\/api\/v2\/media\/([a-zA-Z0-9_-]{1,128})\/([a-zA-Z0-9_-]{1,128})$/.exec(
214
+ data.url ?? '',
215
+ );
216
+ if (
217
+ !url ||
218
+ url[2] !== assetId ||
219
+ !Number.isSafeInteger(data.sizeBytes) ||
220
+ data.sizeBytes < 1 ||
221
+ data.sizeBytes > 15000000 ||
222
+ (data.sha256 !== undefined && data.sha256 !== null && !/^[a-f0-9]{64}$/.test(data.sha256))
223
+ ) {
224
+ fail('invalid_response', 'The media identity or digest is invalid.');
225
+ }
226
+ const file = `${source.scope.replace(':', '-')}/resources/${assetId}.${extension(data.contentType)}`;
227
+ const key = JSON.stringify([assetId, asset.revision, data.sha256 ?? null]);
228
+ const bucket = downloads.mediaBucket(tree);
229
+ let bytes =
230
+ previous?.revision === asset.revision && previous.file === file
231
+ ? await store.readPrevious(file)
232
+ : null;
233
+ if (!bytes) {
234
+ bytes = await downloads.read(source.scope, bucket, key, 15000000);
235
+ }
236
+ if (!bytes) {
237
+ bytes = await client.transport.request('/api/agents/v1/media/content', {
238
+ query: { scope: source.scope, ownerId: url[1], id: assetId },
239
+ binary: true,
240
+ token: await client.accessToken(),
241
+ });
242
+ }
243
+ if (
244
+ !Buffer.isBuffer(bytes) ||
245
+ bytes.length !== data.sizeBytes ||
246
+ (data.sha256 && digest(bytes) !== data.sha256)
247
+ ) {
248
+ fail('media_digest_mismatch', 'The downloaded resource does not match its verified source.');
249
+ }
250
+ await downloads.put(source.scope, bucket, key, bytes);
251
+ await store.put(file, bytes);
252
+ return { file, revision: asset.revision, sha256: digest(bytes) };
253
+ }
254
+
255
+ /** A sync is explicit and finite. It never writes to GeniusTasker, polls in a
256
+ * daemon, widens grants or publishes the resulting private files to Git. */
257
+ export async function syncDocumentation(client, input) {
258
+ const options = parse(documentationSyncSchema, input);
259
+ const store = new DocumentationStore(options.directory, options);
260
+ return store.run(async () => {
261
+ const identity = await client.get('me'),
262
+ capabilities = await client.get('capabilities');
263
+ if (!capabilities.documentationSync) {
264
+ fail('documentation_unavailable', 'This server has not enabled documentation export.');
265
+ }
266
+ if (typeof identity.actor?.ownerId !== 'string') {
267
+ fail('invalid_response', 'The current account identity is unavailable.');
268
+ }
269
+ const realm = digest(`${client.transport.origin}\n${identity.actor.ownerId}`),
270
+ before = store.previousManifest;
271
+ if (before && before.realm !== realm) {
272
+ fail(
273
+ 'documentation_account_mismatch',
274
+ 'Use a separate destination for another account or environment.',
275
+ );
276
+ }
277
+ const configuration = parse(
278
+ documentationConfigurationSchema,
279
+ options.configuration ?? before?.configuration,
280
+ );
281
+ const downloads = new DocumentationDownloads(
282
+ client.store?.directory,
283
+ store.root,
284
+ realm,
285
+ options,
286
+ );
287
+ await downloads.begin(configuration.sources);
288
+ const states = [],
289
+ fences = [],
290
+ stats = { pagesFetched: 0, pagesReused: 0, pagesResumed: 0, unavailable: [] };
291
+ for (const source of configuration.sources) {
292
+ let previous = before?.sources.find((state) => state.scope === source.scope);
293
+ if (previous?.parentId !== (source.parentId ?? null)) {
294
+ previous = null;
295
+ }
296
+ let tree;
297
+ try {
298
+ tree = await readMemoryTree(client, {
299
+ scope: source.scope,
300
+ parentId: source.parentId,
301
+ ifNoneMatch:
302
+ source.pages &&
303
+ before?.configuration.sources.find((old) => old.scope === source.scope)?.pages === false
304
+ ? undefined
305
+ : previous?.etag,
306
+ });
307
+ } catch (error) {
308
+ if (!denied(error)) {
309
+ throw error;
310
+ }
311
+ states.push({ ...source, parentId: source.parentId ?? null, status: 'unavailable' });
312
+ stats.unavailable.push(source.scope);
313
+ await downloads.clearSource(source.scope);
314
+ continue;
315
+ }
316
+ const pages = await collectPages(client, source, tree, previous),
317
+ paths = documentPaths(source, pages);
318
+ await downloads.prepare(source.scope, pages, tree);
319
+ const state = {
320
+ scope: source.scope,
321
+ parentId: source.parentId ?? null,
322
+ status: 'available',
323
+ epoch: tree.epoch,
324
+ accessRevision: tree.accessRevision,
325
+ etag: tree.etag,
326
+ version: tree.version,
327
+ title: tree.title,
328
+ pages,
329
+ documents: Object.create(null),
330
+ assets: Object.create(null),
331
+ };
332
+ const assets = new Set();
333
+ const ensureAsset = async (asset) => {
334
+ if (configuration.media === 'local' && !state.assets[asset]) {
335
+ state.assets[asset] = await localAsset(
336
+ client,
337
+ store,
338
+ source,
339
+ tree,
340
+ asset,
341
+ previous?.assets?.[asset],
342
+ downloads,
343
+ );
344
+ }
345
+ };
346
+ for (const page of Object.values(pages)) {
347
+ const old = previous?.documents?.[page.id],
348
+ path = paths[page.id];
349
+ let bytes = null;
350
+ if (
351
+ old?.revision === page.revision &&
352
+ old.file === path &&
353
+ before.configuration.media === configuration.media &&
354
+ equivalentAccess(previous, tree)
355
+ ) {
356
+ bytes = await store.readPrevious(old.file);
357
+ }
358
+ if (bytes) {
359
+ await store.put(path, bytes);
360
+ state.documents[page.id] = old;
361
+ stats.pagesReused++;
362
+ for (const asset of old.assetIds) {
363
+ assets.add(asset);
364
+ }
365
+ } else {
366
+ const query = {
367
+ scope: source.scope,
368
+ pageId: page.id,
369
+ revision: page.revision,
370
+ epoch: tree.epoch,
371
+ };
372
+ const key = downloads.pageKey(source.scope, page, tree);
373
+ const cached = await downloads.read(source.scope, 'pages', key);
374
+ let document;
375
+ if (cached) {
376
+ document = await readMemoryMarkdown(
377
+ { get: async () => JSON.parse(cached.toString('utf8')) },
378
+ query,
379
+ );
380
+ stats.pagesResumed++;
381
+ } else {
382
+ document = await readMemoryMarkdown(client, { ...query, checkpoint: tree.checkpoint });
383
+ await downloads.put(source.scope, 'pages', key, Buffer.from(JSON.stringify(document)));
384
+ stats.pagesFetched++;
385
+ }
386
+ if (document.accessRevision !== tree.accessRevision) {
387
+ fail('read_context_changed', 'Access changed while exporting the document.');
388
+ }
389
+ let markdown = document.markdown;
390
+ for (const asset of document.assetIds) {
391
+ await ensureAsset(asset);
392
+ if (configuration.media === 'local') {
393
+ const target = posix.relative(posix.dirname(path), state.assets[asset].file);
394
+ markdown = markdown.split(`(geniustasker-asset:${asset})`).join(`(${target})`);
395
+ }
396
+ }
397
+ await store.put(path, markdown);
398
+ for (const asset of document.assetIds) {
399
+ assets.add(asset);
400
+ }
401
+ state.documents[page.id] = {
402
+ file: path,
403
+ revision: page.revision,
404
+ assetIds: document.assetIds,
405
+ };
406
+ }
407
+ }
408
+ await exportGraph(client, store, source, tree, previous, state, assets, downloads);
409
+ for (const asset of assets) {
410
+ await ensureAsset(asset);
411
+ }
412
+ const index = [
413
+ '> Automatically generated. Edit source documents through GeniusTasker MCP, then sync again.',
414
+ '',
415
+ `# ${escape(tree.title)}`,
416
+ '',
417
+ ];
418
+ for (const page of Object.values(pages).sort((a, b) =>
419
+ paths[a.id].localeCompare(paths[b.id]),
420
+ )) {
421
+ index.push(
422
+ `- [${escape(page.title)}](${paths[page.id].slice(source.scope.replace(':', '-').length + 1)}) · ${escape(page.kind)}`,
423
+ );
424
+ }
425
+ if (source.graph) {
426
+ index.push('- [Graph](graph.md) · [Structured Graph](graph.json)');
427
+ }
428
+ index.push(
429
+ '',
430
+ '---',
431
+ '',
432
+ `Source: \`${source.scope}\` · Epoch: \`${tree.epoch}\` · Tree version: \`${tree.version}\``,
433
+ '',
434
+ );
435
+ await store.put(`${source.scope.replace(':', '-')}/index.md`, index.join('\n'));
436
+ states.push(state);
437
+ fences.push({ source, tree });
438
+ }
439
+ // Every participating scope must still match before publishing the new
440
+ // snapshot. An HTTP/ACL/network failure is never interpreted as an empty tree.
441
+ for (const { source, tree } of fences) {
442
+ try {
443
+ const finalTree = await readMemoryTree(client, {
444
+ scope: source.scope,
445
+ parentId: source.parentId,
446
+ ...contextOf(tree),
447
+ ifNoneMatch: tree.etag,
448
+ });
449
+ if (finalTree.accessRevision !== tree.accessRevision || finalTree.etag !== tree.etag) {
450
+ fail(
451
+ 'read_context_changed',
452
+ 'Memory permissions changed before the snapshot was committed.',
453
+ );
454
+ }
455
+ } catch (error) {
456
+ if (!denied(error)) {
457
+ throw error;
458
+ }
459
+ await store.discardPrefix(source.scope.replace(':', '-'));
460
+ await downloads.clearSource(source.scope);
461
+ states[states.findIndex((state) => state.scope === source.scope)] = {
462
+ scope: source.scope,
463
+ parentId: source.parentId ?? null,
464
+ status: 'unavailable',
465
+ };
466
+ stats.unavailable.push(source.scope);
467
+ }
468
+ }
469
+ const finalIdentity = await client.get('me');
470
+ if (
471
+ finalIdentity.actor?.ownerId !== identity.actor.ownerId ||
472
+ finalIdentity.actor?.profileId !== identity.actor.profileId ||
473
+ finalIdentity.actor?.connectionId !== identity.actor.connectionId
474
+ ) {
475
+ fail(
476
+ 'read_context_changed',
477
+ 'The account or agent profile changed during documentation sync.',
478
+ );
479
+ }
480
+ const checkedAt = new Date().toISOString();
481
+ await store.commit({ realm, configuration, sources: states, checkedAt });
482
+ await downloads.finish();
483
+ return {
484
+ directory: store.root,
485
+ contentDirectory: store.current,
486
+ checkedAt,
487
+ files: Object.keys(store.files).length,
488
+ bytes: store.size,
489
+ ...stats,
490
+ };
491
+ });
492
+ }
493
+
494
+ export async function documentationStatus(input) {
495
+ const { directory } = parse(documentationDirectorySchema, input);
496
+ const status = await new DocumentationStore(directory).status();
497
+ if (!status.formatVersion) {
498
+ return status;
499
+ }
500
+ return {
501
+ directory,
502
+ checkedAt: status.checkedAt,
503
+ checkRecommended: new Date(status.checkedAt).toDateString() !== new Date().toDateString(),
504
+ files: Object.keys(status.files).length,
505
+ sources: status.sources.map((source) => ({
506
+ scope: source.scope,
507
+ status: source.status,
508
+ title: source.title,
509
+ })),
510
+ authorization:
511
+ 'Saved snapshot; current access has not been checked by this local status command.',
512
+ };
513
+ }
514
+
515
+ export async function unlockDocumentation(input) {
516
+ return new DocumentationStore(parse(documentationDirectorySchema, input).directory).unlock();
517
+ }
@@ -0,0 +1,136 @@
1
+ import * as z from 'zod/v4';
2
+ import { fail } from './errors.mjs';
3
+
4
+ const id = z.string().regex(/^[a-zA-Z0-9_-]{1,128}$/);
5
+ const revision = z
6
+ .string()
7
+ .regex(/^(0|[1-9][0-9]*)$/)
8
+ .max(32);
9
+ const digest = z.string().regex(/^[a-f0-9]{64}$/);
10
+ const scope = z.string().regex(/^memory:[a-zA-Z0-9_-]{1,128}$/);
11
+ const context = {
12
+ epoch: z.string().min(1).max(128).optional(),
13
+ checkpoint: revision.optional(),
14
+ };
15
+ export const memoryTreeSchema = z
16
+ .object({
17
+ scope,
18
+ parentId: id.optional(),
19
+ after: id.optional(),
20
+ ...context,
21
+ ifNoneMatch: digest.optional(),
22
+ })
23
+ .strict()
24
+ .refine((value) => !value.after || !!(value.epoch && value.checkpoint), {
25
+ message: 'Continuation needs epoch and checkpoint.',
26
+ });
27
+ export const memoryMarkdownSchema = z
28
+ .object({
29
+ scope,
30
+ pageId: id,
31
+ revision: revision.optional(),
32
+ ...context,
33
+ })
34
+ .strict();
35
+ const entry = z.object({
36
+ id,
37
+ revision,
38
+ parentId: id.nullable(),
39
+ title: z.string().max(160),
40
+ path: z.string().max(512),
41
+ kind: z.string().max(60),
42
+ section: z.string().max(80),
43
+ directory: z.boolean(),
44
+ order: z.number().int(),
45
+ updated: z.string().nullable(),
46
+ childCount: z.number().int().min(0).max(10000),
47
+ subtreeVersion: digest,
48
+ });
49
+ const treeResult = z.object({
50
+ scope,
51
+ epoch: z.string(),
52
+ checkpoint: revision,
53
+ accessRevision: z.string(),
54
+ parentId: id.nullable(),
55
+ title: z.string().max(160),
56
+ parent: entry.nullable(),
57
+ version: digest,
58
+ etag: digest,
59
+ notModified: z.boolean(),
60
+ items: z.array(entry).max(50),
61
+ nextAfter: id.nullable(),
62
+ totalChildren: z.number().int().min(0).max(10000),
63
+ });
64
+ const markdownResult = z.object({
65
+ scope,
66
+ epoch: z.string(),
67
+ checkpoint: revision,
68
+ accessRevision: z.string(),
69
+ id,
70
+ revision,
71
+ updated: z.string().nullable(),
72
+ creator: z.string().nullable(),
73
+ lastEditor: z.string().nullable(),
74
+ markdown: z.string(),
75
+ assetIds: z.array(id).max(64),
76
+ });
77
+ function parsed(schema, value, code = 'invalid_arguments') {
78
+ const result = schema.safeParse(value);
79
+ if (!result.success) {
80
+ fail(code, 'Use the documented Memory tree or Markdown format.');
81
+ }
82
+ return result.data;
83
+ }
84
+ function checkContext(query, result) {
85
+ if (
86
+ result.scope !== query.scope ||
87
+ (query.epoch !== undefined && result.epoch !== query.epoch) ||
88
+ (query.checkpoint !== undefined && result.checkpoint !== query.checkpoint)
89
+ ) {
90
+ fail('invalid_response', 'Documentation does not match the requested source context.');
91
+ }
92
+ }
93
+
94
+ /** One bounded tree request. The server rechecks the grant and page roles even
95
+ * with a validator. Folder digests describe readable descendants only. */
96
+ export async function readMemoryTree(client, value) {
97
+ const query = parsed(memoryTreeSchema, value);
98
+ const result = parsed(treeResult, await client.get('memory/tree', query), 'invalid_response');
99
+ checkContext(query, result);
100
+ if (
101
+ result.parentId !== (query.parentId ?? null) ||
102
+ result.items.some((item) => item.parentId !== result.parentId) ||
103
+ new Set(result.items.map((item) => item.id)).size !== result.items.length ||
104
+ (result.notModified &&
105
+ (result.etag !== query.ifNoneMatch || result.items.length || result.nextAfter !== null)) ||
106
+ (!result.notModified && query.parentId && result.parent?.id !== query.parentId) ||
107
+ (result.nextAfter && result.nextAfter !== result.items.at(-1)?.id) ||
108
+ result.items.some(
109
+ (item, index) => item.id <= (index ? result.items[index - 1].id : (query.after ?? '')),
110
+ )
111
+ ) {
112
+ fail('invalid_response', 'The tree response has an inconsistent parent, cursor or validator.');
113
+ }
114
+ return result;
115
+ }
116
+
117
+ /** Fetch exactly one page. The Markdown remains untrusted user-authored data;
118
+ * it never authorizes changing grants, credentials or repository instructions. */
119
+ export async function readMemoryMarkdown(client, value) {
120
+ const query = parsed(memoryMarkdownSchema, value);
121
+ const result = parsed(
122
+ markdownResult,
123
+ await client.get('memory/markdown', query),
124
+ 'invalid_response',
125
+ );
126
+ checkContext(query, result);
127
+ if (
128
+ result.id !== query.pageId ||
129
+ (query.revision !== undefined && result.revision !== query.revision) ||
130
+ Buffer.byteLength(result.markdown) > 196608 ||
131
+ new Set(result.assetIds).size !== result.assetIds.length
132
+ ) {
133
+ fail('invalid_response', 'The Markdown response does not match the requested page or limits.');
134
+ }
135
+ return result;
136
+ }