@pellux/goodvibes-daemon-sdk 0.33.37 → 0.33.38

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.
@@ -1 +1 @@
1
- {"version":3,"file":"integration-route-types.d.ts","sourceRoot":"","sources":["../src/integration-route-types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"integration-route-types.d.ts","sourceRoot":"","sources":["../src/integration-route-types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"integration-routes.d.ts","sourceRoot":"","sources":["../src/integration-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,cAAc,CAAC;AAEnE,OAAO,KAAK,EAAE,6BAA6B,EAAoD,MAAM,8BAA8B,CAAC;AA6DpI,wBAAgB,oCAAoC,CAClD,OAAO,EAAE,6BAA6B,GACrC,8BAA8B,CA4IhC"}
1
+ {"version":3,"file":"integration-routes.d.ts","sourceRoot":"","sources":["../src/integration-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,cAAc,CAAC;AAEnE,OAAO,KAAK,EAAE,6BAA6B,EAAoD,MAAM,8BAA8B,CAAC;AA8DpI,wBAAgB,oCAAoC,CAClD,OAAO,EAAE,6BAA6B,GACrC,8BAA8B,CAqJhC"}
@@ -1,5 +1,5 @@
1
1
  import { jsonErrorResponse } from './error-response.js';
2
- import { createRouteBodySchema, createRouteBodySchemaRegistry, readOptionalStringField, readStringArrayField, } from './route-helpers.js';
2
+ import { createRouteBodySchema, createRouteBodySchemaRegistry, readBoundedPositiveInteger, readOptionalStringField, readStringArrayField, } from './route-helpers.js';
3
3
  const MAX_LOCAL_AUTH_ROLES = 32;
4
4
  const integrationBodySchemas = createRouteBodySchemaRegistry({
5
5
  embeddingDefault: createRouteBodySchema('POST /api/memory/embedding/default', (body) => {
@@ -43,7 +43,7 @@ export function createDaemonIntegrationRouteHandlers(context) {
43
43
  getIntegrationSession: () => withHelpers(context.integrationHelpers, (helpers) => Response.json(helpers.getSessionSnapshot())),
44
44
  getIntegrationTasks: () => withHelpers(context.integrationHelpers, (helpers) => Response.json(helpers.getTaskSnapshot())),
45
45
  getIntegrationAutomation: () => withHelpers(context.integrationHelpers, (helpers) => Response.json(helpers.getAutomationSnapshot())),
46
- getIntegrationSessions: () => withHelpers(context.integrationHelpers, (helpers) => Response.json(helpers.getSessionBrokerSnapshot())),
46
+ getIntegrationSessions: (url) => handleGetIntegrationSessions(context.integrationHelpers, url),
47
47
  getDeliveries: () => withHelpers(context.integrationHelpers, (helpers) => Response.json(helpers.getDeliverySnapshot())),
48
48
  getDelivery: (deliveryId) => {
49
49
  const runtimeStore = context.integrationHelpers?.getRuntimeStore() ?? null;
@@ -84,6 +84,15 @@ export function createDaemonIntegrationRouteHandlers(context) {
84
84
  getIntelligence: () => withHelpers(context.integrationHelpers, (helpers) => Response.json(helpers.getIntelligenceSnapshot())),
85
85
  getMemoryDoctor: async () => Response.json(await context.memoryRegistry.doctor()),
86
86
  getMemoryVectorStats: () => Response.json({ vector: context.memoryRegistry.vectorStats() }),
87
+ getMemoryReviewQueue: (url) => {
88
+ const limit = readBoundedPositiveInteger(url.searchParams.get('limit'), 10, 1_000);
89
+ const rawScope = url.searchParams.get('scope');
90
+ if (rawScope !== null && rawScope !== 'session' && rawScope !== 'project' && rawScope !== 'team') {
91
+ return jsonErrorResponse({ error: `Invalid scope: ${rawScope}. Allowed: session, project, team` }, { status: 400 });
92
+ }
93
+ const scope = rawScope ?? undefined;
94
+ return Response.json({ records: context.memoryRegistry.reviewQueue(limit, scope) });
95
+ },
87
96
  postMemoryVectorRebuild: async (req) => {
88
97
  const admin = context.requireAdmin(req);
89
98
  if (admin)
@@ -204,3 +213,19 @@ function withHelpers(helpers, run) {
204
213
  }
205
214
  return run(helpers);
206
215
  }
216
+ /**
217
+ * Handle GET /api/sessions.
218
+ *
219
+ * Returns the session broker snapshot from the integration helper service.
220
+ * Pagination (`?limit=` / `?cursor=`) is not supported on this endpoint —
221
+ * `IntegrationHelperServiceLike` is a consumer-supplied structural interface
222
+ * whose concrete implementation lives outside daemon-sdk and cannot be
223
+ * range-queried here. Requests containing pagination params receive the
224
+ * same snapshot response as non-paginated requests (backward compatible).
225
+ */
226
+ function handleGetIntegrationSessions(helpers, _url) {
227
+ if (!helpers) {
228
+ return jsonErrorResponse({ error: 'Integration helper service unavailable' }, { status: 503 });
229
+ }
230
+ return Response.json(helpers.getSessionBrokerSnapshot());
231
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"knowledge-routes.d.ts","sourceRoot":"","sources":["../src/knowledge-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,cAAc,CAAC;AAmBjE,OAAO,KAAK,EAEV,2BAA2B,EAM5B,MAAM,4BAA4B,CAAC;AAwCpC,wBAAgB,kCAAkC,CAChD,OAAO,EAAE,2BAA2B,GACnC,4BAA4B,CAyK9B"}
1
+ {"version":3,"file":"knowledge-routes.d.ts","sourceRoot":"","sources":["../src/knowledge-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,cAAc,CAAC;AAoBjE,OAAO,KAAK,EAEV,2BAA2B,EAM5B,MAAM,4BAA4B,CAAC;AAwCpC,wBAAgB,kCAAkC,CAChD,OAAO,EAAE,2BAA2B,GACnC,4BAA4B,CA+J9B"}
@@ -1,6 +1,7 @@
1
1
  import { buildMissingScopeBody, resolvePrivateHostFetchOptions, } from './http-policy.js';
2
2
  import { GoodVibesSdkError, DaemonErrorCategory } from '@pellux/goodvibes-errors';
3
3
  import { jsonErrorResponse } from './error-response.js';
4
+ import { paginateItems, hasPaginationParams } from './pagination.js';
4
5
  import { createArtifactFromUploadRequest, isArtifactUploadRequest } from './artifact-upload.js';
5
6
  import { createRouteBodySchema, createRouteBodySchemaRegistry, readBoundedBodyInteger, readBoundedPositiveInteger, readOptionalBoundedInteger, readOptionalStringField, readStringArrayField, } from './route-helpers.js';
6
7
  import { createDaemonKnowledgeRefinementRouteHandlers } from './knowledge-refinement-routes.js';
@@ -8,18 +9,8 @@ const MAX_KNOWLEDGE_INGEST_TAGS = 64;
8
9
  export function createDaemonKnowledgeRouteHandlers(context) {
9
10
  return {
10
11
  getKnowledgeStatus: async (url) => Response.json(await context.knowledgeService.getStatus(readKnowledgeSpaceQuery(url))),
11
- getKnowledgeSources: async (url) => Response.json({
12
- sources: context.knowledgeService.querySources({
13
- limit: readLimit(url, 100),
14
- ...readKnowledgeSpaceQuery(url),
15
- }).items,
16
- }),
17
- getKnowledgeNodes: async (url) => Response.json({
18
- nodes: context.knowledgeService.queryNodes({
19
- limit: readLimit(url, 100),
20
- ...readKnowledgeSpaceQuery(url),
21
- }).items,
22
- }),
12
+ getKnowledgeSources: async (url) => handleGetKnowledgeSources(context, url),
13
+ getKnowledgeNodes: async (url) => handleGetKnowledgeNodes(context, url),
23
14
  getKnowledgeIssues: async (url) => Response.json({
24
15
  issues: context.knowledgeService.queryIssues({
25
16
  limit: readLimit(url, 100),
@@ -682,8 +673,15 @@ async function handleKnowledgeReviewIssue(context, id, request) {
682
673
  }
683
674
  catch (error) {
684
675
  // map status from error code, not English-prefix match.
685
- const isNotFound = error instanceof Error && (error.code === 'NOT_FOUND'
686
- || error.code === 'KNOWLEDGE_ISSUE_NOT_FOUND');
676
+ const isNotFound = error instanceof Error && (
677
+ // Domain-specific not-found codes are always explicit (never auto-inferred).
678
+ error.code === 'KNOWLEDGE_ISSUE_NOT_FOUND'
679
+ // Bare NOT_FOUND only maps to 404 when the error carries an actual HTTP 404
680
+ // status, preventing auto-inferred NOT_FOUND from category-only errors
681
+ // (which now always have code='NOT_FOUND' via inferCodeFromCategory)
682
+ // from silently becoming 404 responses.
683
+ || (error.code === 'NOT_FOUND'
684
+ && error.status === 404));
687
685
  return jsonErrorResponse(error, { status: isNotFound ? 404 : 400 });
688
686
  }
689
687
  }
@@ -710,8 +708,15 @@ async function handleKnowledgeDecideCandidate(context, id, request) {
710
708
  }
711
709
  catch (error) {
712
710
  // map status from error code, not English-prefix match.
713
- const isNotFound = error instanceof Error && (error.code === 'NOT_FOUND'
714
- || error.code === 'KNOWLEDGE_CANDIDATE_NOT_FOUND');
711
+ const isNotFound = error instanceof Error && (
712
+ // Domain-specific not-found codes are always explicit (never auto-inferred).
713
+ error.code === 'KNOWLEDGE_CANDIDATE_NOT_FOUND'
714
+ // Bare NOT_FOUND only maps to 404 when the error carries an actual HTTP 404
715
+ // status, preventing auto-inferred NOT_FOUND from category-only errors
716
+ // (which now always have code='NOT_FOUND' via inferCodeFromCategory)
717
+ // from silently becoming 404 responses.
718
+ || (error.code === 'NOT_FOUND'
719
+ && error.status === 404));
715
720
  return jsonErrorResponse(error, { status: isNotFound ? 404 : 400 });
716
721
  }
717
722
  }
@@ -732,8 +737,15 @@ async function handleKnowledgeRunJob(context, jobId, request) {
732
737
  }
733
738
  catch (error) {
734
739
  // map status from error code, not English-prefix match.
735
- const isNotFound = error instanceof Error && (error.code === 'NOT_FOUND'
736
- || error.code === 'KNOWLEDGE_JOB_NOT_FOUND');
740
+ const isNotFound = error instanceof Error && (
741
+ // Domain-specific not-found codes are always explicit (never auto-inferred).
742
+ error.code === 'KNOWLEDGE_JOB_NOT_FOUND'
743
+ // Bare NOT_FOUND only maps to 404 when the error carries an actual HTTP 404
744
+ // status, preventing auto-inferred NOT_FOUND from category-only errors
745
+ // (which now always have code='NOT_FOUND' via inferCodeFromCategory)
746
+ // from silently becoming 404 responses.
747
+ || (error.code === 'NOT_FOUND'
748
+ && error.status === 404));
737
749
  return jsonErrorResponse(error, { status: isNotFound ? 404 : 400 });
738
750
  }
739
751
  }
@@ -815,3 +827,98 @@ async function handleKnowledgeMaterializeProjection(context, request) {
815
827
  return jsonErrorResponse(error, { status: 400 });
816
828
  }
817
829
  }
830
+ /** Extract the `id` from an opaque knowledge item (sources/nodes are `unknown` at the SDK boundary). */
831
+ function extractKnowledgeItemId(item) {
832
+ if (typeof item === 'object' && item !== null && 'id' in item) {
833
+ const id = item.id;
834
+ return typeof id === 'string' ? id : undefined;
835
+ }
836
+ return undefined;
837
+ }
838
+ /**
839
+ * Extract the `updatedAt` timestamp from an opaque knowledge item for cursor-based
840
+ * pagination recovery.
841
+ *
842
+ * Knowledge source and node records carry a `readonly updatedAt: number` field
843
+ * (see `KnowledgeSourceRecord` / `KnowledgeNodeRecord` in
844
+ * `platform/knowledge/types.ts`). The production store sorts both collections
845
+ * by `byUpdatedAtDesc` (newest-first by updatedAt), so the cursor's position
846
+ * key MUST encode `updatedAt` — not `createdAt` — to land at the correct
847
+ * insertion point after a mid-walk deletion.
848
+ *
849
+ * Semantic note: `updatedAt` mutates when an item is modified. A mid-walk
850
+ * update moves the item to the front of the list (it gains a new updatedAt
851
+ * value) and its old position disappears — identical in effect to a deletion.
852
+ * The insertion-point scan (findIndex(ts < cursor.value)) handles this
853
+ * correctly: the old position is gone, so the scan skips past it naturally.
854
+ *
855
+ * Returns `undefined` when the field is absent or not a number (tolerant of
856
+ * missing values at the SDK boundary).
857
+ */
858
+ function extractKnowledgeItemUpdatedAt(item) {
859
+ if (typeof item === 'object' && item !== null && 'updatedAt' in item) {
860
+ const ts = item.updatedAt;
861
+ return typeof ts === 'number' ? ts : undefined;
862
+ }
863
+ return undefined;
864
+ }
865
+ /**
866
+ * Handle GET /api/knowledge/sources.
867
+ *
868
+ * Without pagination params (`?limit=` / `?cursor=`): returns `{ sources: [...] }` (backward compat).
869
+ * With pagination params: returns `PaginatedResponse`-shaped `{ items, nextCursor, hasMore }`.
870
+ *
871
+ * ### Fetch cap
872
+ * The knowledge store accepts a `limit` argument but does not support
873
+ * cursor/offset range queries — the full in-memory collection must be loaded to
874
+ * perform cursor-based slicing. We cap the load at 5 000 items. If the store
875
+ * grows beyond this, source-level pagination on the store API should be added
876
+ * and this cap raised accordingly.
877
+ */
878
+ function handleGetKnowledgeSources(context, url) {
879
+ const spaceQuery = readKnowledgeSpaceQuery(url);
880
+ if (!hasPaginationParams(url)) {
881
+ const { items: sources } = context.knowledgeService.querySources({ limit: readLimit(url, 100), ...spaceQuery });
882
+ return Response.json({ sources });
883
+ }
884
+ const limit = readLimit(url, 100);
885
+ const rawCursor = url.searchParams.get('cursor');
886
+ // Load the full in-memory collection (capped at 5_000 — see JSDoc note above).
887
+ const { items: allItems } = context.knowledgeService.querySources({ limit: 5_000, ...spaceQuery });
888
+ const items = allItems;
889
+ const result = paginateItems(items, limit, rawCursor, (item) => extractKnowledgeItemId(item) ?? '',
890
+ // NOTE: cursor position key encodes updatedAt (matches byUpdatedAtDesc store order).
891
+ extractKnowledgeItemUpdatedAt, { descending: true });
892
+ if ('error' in result) {
893
+ return jsonErrorResponse({ error: result.error }, { status: 400 });
894
+ }
895
+ return Response.json(result);
896
+ }
897
+ /**
898
+ * Handle GET /api/knowledge/nodes.
899
+ *
900
+ * Without pagination params (`?limit=` / `?cursor=`): returns `{ nodes: [...] }` (backward compat).
901
+ * With pagination params: returns `PaginatedResponse`-shaped `{ items, nextCursor, hasMore }`.
902
+ *
903
+ * ### Fetch cap
904
+ * Same limitation as `handleGetKnowledgeSources` — capped at 5 000 items.
905
+ */
906
+ function handleGetKnowledgeNodes(context, url) {
907
+ const spaceQuery = readKnowledgeSpaceQuery(url);
908
+ if (!hasPaginationParams(url)) {
909
+ const { items: nodes } = context.knowledgeService.queryNodes({ limit: readLimit(url, 100), ...spaceQuery });
910
+ return Response.json({ nodes });
911
+ }
912
+ const limit = readLimit(url, 100);
913
+ const rawCursor = url.searchParams.get('cursor');
914
+ // Load the full in-memory collection (capped at 5_000 — see JSDoc note above).
915
+ const { items: allItems } = context.knowledgeService.queryNodes({ limit: 5_000, ...spaceQuery });
916
+ const items = allItems;
917
+ const result = paginateItems(items, limit, rawCursor, (item) => extractKnowledgeItemId(item) ?? '',
918
+ // NOTE: cursor position key encodes updatedAt (matches byUpdatedAtDesc store order).
919
+ extractKnowledgeItemUpdatedAt, { descending: true });
920
+ if ('error' in result) {
921
+ return jsonErrorResponse({ error: result.error }, { status: 400 });
922
+ }
923
+ return Response.json(result);
924
+ }
package/dist/operator.js CHANGED
@@ -277,6 +277,8 @@ async function dispatchOperatorRoutesInner(req, handlers) {
277
277
  return handlers.postAutomationHeartbeat(req);
278
278
  if (pathname === '/api/memory/doctor' && method === 'GET')
279
279
  return handlers.getMemoryDoctor();
280
+ if (pathname === '/api/memory/review-queue' && method === 'GET')
281
+ return handlers.getMemoryReviewQueue(url);
280
282
  if (pathname === '/api/memory/vector' && method === 'GET')
281
283
  return handlers.getMemoryVectorStats();
282
284
  if (pathname === '/api/memory/vector/rebuild' && method === 'POST')
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Cursor-based pagination utilities for daemon route handlers.
3
+ *
4
+ * Cursor format: opaque base64url-encoded JSON `{id: string, createdAt: number}`
5
+ * encoding the stable sort position. Cursors remain valid across inserts
6
+ * (no offset-based pagination).
7
+ *
8
+ * @public
9
+ */
10
+ /**
11
+ * Paginated response envelope. Returned by list endpoints that support
12
+ * cursor-based pagination.
13
+ *
14
+ * @public
15
+ */
16
+ export interface PaginatedResponse<T> {
17
+ /** The page of items. */
18
+ readonly items: readonly T[];
19
+ /**
20
+ * Opaque cursor to pass as `?cursor=` to retrieve the next page.
21
+ * Absent when this is the last page.
22
+ */
23
+ readonly nextCursor?: string | undefined;
24
+ /** Whether there are more items beyond this page. */
25
+ readonly hasMore: boolean;
26
+ }
27
+ /** Internal cursor payload. @internal */
28
+ interface CursorPayload {
29
+ readonly id: string;
30
+ readonly createdAt?: number | undefined;
31
+ }
32
+ /**
33
+ * Encode a cursor from a stable sort position.
34
+ *
35
+ * @param id - The item id used as the tiebreak.
36
+ * @param createdAt - Optional creation timestamp for secondary sort key.
37
+ * @returns Opaque base64url cursor string.
38
+ * @public
39
+ */
40
+ export declare function encodeCursor(id: string, createdAt?: number): string;
41
+ /**
42
+ * Decode an opaque cursor string.
43
+ *
44
+ * @param raw - The cursor string from `?cursor=`.
45
+ * @returns The decoded payload, or `null` if the cursor is invalid.
46
+ * @public
47
+ */
48
+ export declare function decodeCursor(raw: string): CursorPayload | null;
49
+ /**
50
+ * Options for `paginateItems`.
51
+ * @public
52
+ */
53
+ export interface PaginateItemsOptions {
54
+ /**
55
+ * Set to `true` when items are sorted **descending** by `createdAt` (newest
56
+ * first). This inverts the insertion-point comparison used during
57
+ * deleted-cursor recovery so that the correct position is found regardless
58
+ * of sort direction. Defaults to `false` (ascending / oldest-first).
59
+ */
60
+ readonly descending?: boolean | undefined;
61
+ }
62
+ /**
63
+ * Apply cursor-based pagination to an already-sorted array of items.
64
+ *
65
+ * Items must be sorted in a stable order. The cursor encodes the last-seen
66
+ * item's `id` (and optionally `createdAt`). Items after the cursor position
67
+ * are returned.
68
+ *
69
+ * ### Deleted-cursor recovery (m3/m4)
70
+ * When the cursor's `id` is not found in the current page (i.e. the item was
71
+ * deleted mid-walk), the implementation falls back to the `createdAt`
72
+ * timestamp in the cursor to locate the insertion point.
73
+ *
74
+ * - **Ascending** (default, `options.descending` falsy): finds the first item
75
+ * whose own `createdAt` is **strictly greater** than the cursor's
76
+ * `createdAt`, then starts the page from that position.
77
+ * - **Descending** (`options.descending: true`): finds the first item whose
78
+ * own `createdAt` is **strictly less** than the cursor's `createdAt`, which
79
+ * is the correct forward position in a newest-first sequence.
80
+ *
81
+ * Without `createdAt` available in the cursor, recovery falls back to the
82
+ * first position (index 0) to avoid returning an empty page.
83
+ *
84
+ * Callers that need stable recovery across deletions should pass `getCreatedAt`
85
+ * and ensure cursors are encoded with `createdAt`. Callers whose store sorts
86
+ * descending must also pass `options.descending: true`.
87
+ *
88
+ * @param items - Sorted array of all items.
89
+ * @param limit - Maximum number of items to return (already clamped by caller).
90
+ * @param rawCursor - Raw cursor string from the request (`?cursor=`).
91
+ * @param getId - Extract the item's id.
92
+ * @param getCreatedAt - Optional: extract the item's creation timestamp (ms since epoch).
93
+ * @param options - Optional pagination options (e.g. sort direction).
94
+ * @returns `PaginatedResponse<T>` or an error string for invalid cursors.
95
+ * @public
96
+ */
97
+ export declare function paginateItems<T>(items: readonly T[], limit: number, rawCursor: string | null, getId: (item: T) => string, getCreatedAt?: (item: T) => number | undefined, options?: PaginateItemsOptions): PaginatedResponse<T> | {
98
+ readonly error: string;
99
+ };
100
+ /**
101
+ * Check whether the request URL contains pagination parameters.
102
+ *
103
+ * Used to distinguish backward-compatible (no params → plain array)
104
+ * from paginated (with params → `PaginatedResponse<T>`) calls.
105
+ *
106
+ * @public
107
+ */
108
+ export declare function hasPaginationParams(url: URL): boolean;
109
+ export {};
110
+ //# sourceMappingURL=pagination.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,yBAAyB;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,yCAAyC;AACzC,UAAU,aAAa;IACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAKnE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAoB9D;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC3C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,EAC1B,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,EAC9C,OAAO,CAAC,EAAE,oBAAoB,GAC7B,iBAAiB,CAAC,CAAC,CAAC,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA8CnD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAErD"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Cursor-based pagination utilities for daemon route handlers.
3
+ *
4
+ * Cursor format: opaque base64url-encoded JSON `{id: string, createdAt: number}`
5
+ * encoding the stable sort position. Cursors remain valid across inserts
6
+ * (no offset-based pagination).
7
+ *
8
+ * @public
9
+ */
10
+ /**
11
+ * Encode a cursor from a stable sort position.
12
+ *
13
+ * @param id - The item id used as the tiebreak.
14
+ * @param createdAt - Optional creation timestamp for secondary sort key.
15
+ * @returns Opaque base64url cursor string.
16
+ * @public
17
+ */
18
+ export function encodeCursor(id, createdAt) {
19
+ const payload = createdAt !== undefined ? { id, createdAt } : { id };
20
+ const json = JSON.stringify(payload);
21
+ // base64url encode (no padding, URL-safe characters)
22
+ return Buffer.from(json, 'utf8').toString('base64url');
23
+ }
24
+ /**
25
+ * Decode an opaque cursor string.
26
+ *
27
+ * @param raw - The cursor string from `?cursor=`.
28
+ * @returns The decoded payload, or `null` if the cursor is invalid.
29
+ * @public
30
+ */
31
+ export function decodeCursor(raw) {
32
+ try {
33
+ const json = Buffer.from(raw, 'base64url').toString('utf8');
34
+ const parsed = JSON.parse(json);
35
+ if (typeof parsed === 'object' &&
36
+ parsed !== null &&
37
+ 'id' in parsed &&
38
+ typeof parsed.id === 'string') {
39
+ const p = parsed;
40
+ return {
41
+ id: p.id,
42
+ createdAt: typeof p.createdAt === 'number' ? p.createdAt : undefined,
43
+ };
44
+ }
45
+ return null;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ /**
52
+ * Apply cursor-based pagination to an already-sorted array of items.
53
+ *
54
+ * Items must be sorted in a stable order. The cursor encodes the last-seen
55
+ * item's `id` (and optionally `createdAt`). Items after the cursor position
56
+ * are returned.
57
+ *
58
+ * ### Deleted-cursor recovery (m3/m4)
59
+ * When the cursor's `id` is not found in the current page (i.e. the item was
60
+ * deleted mid-walk), the implementation falls back to the `createdAt`
61
+ * timestamp in the cursor to locate the insertion point.
62
+ *
63
+ * - **Ascending** (default, `options.descending` falsy): finds the first item
64
+ * whose own `createdAt` is **strictly greater** than the cursor's
65
+ * `createdAt`, then starts the page from that position.
66
+ * - **Descending** (`options.descending: true`): finds the first item whose
67
+ * own `createdAt` is **strictly less** than the cursor's `createdAt`, which
68
+ * is the correct forward position in a newest-first sequence.
69
+ *
70
+ * Without `createdAt` available in the cursor, recovery falls back to the
71
+ * first position (index 0) to avoid returning an empty page.
72
+ *
73
+ * Callers that need stable recovery across deletions should pass `getCreatedAt`
74
+ * and ensure cursors are encoded with `createdAt`. Callers whose store sorts
75
+ * descending must also pass `options.descending: true`.
76
+ *
77
+ * @param items - Sorted array of all items.
78
+ * @param limit - Maximum number of items to return (already clamped by caller).
79
+ * @param rawCursor - Raw cursor string from the request (`?cursor=`).
80
+ * @param getId - Extract the item's id.
81
+ * @param getCreatedAt - Optional: extract the item's creation timestamp (ms since epoch).
82
+ * @param options - Optional pagination options (e.g. sort direction).
83
+ * @returns `PaginatedResponse<T>` or an error string for invalid cursors.
84
+ * @public
85
+ */
86
+ export function paginateItems(items, limit, rawCursor, getId, getCreatedAt, options) {
87
+ let startIndex = 0;
88
+ if (rawCursor !== null) {
89
+ const decoded = decodeCursor(rawCursor);
90
+ if (!decoded) {
91
+ return { error: `Invalid cursor: ${rawCursor}` };
92
+ }
93
+ const afterIndex = items.findIndex((item) => getId(item) === decoded.id);
94
+ if (afterIndex === -1) {
95
+ // Cursor id not found — item was deleted mid-walk.
96
+ // Use createdAt from the cursor to locate the insertion point.
97
+ // For ascending sequences: first item with createdAt > cursor's createdAt.
98
+ // For descending sequences: first item with createdAt < cursor's createdAt.
99
+ // Without createdAt, restart from index 0.
100
+ if (decoded.createdAt !== undefined && getCreatedAt !== undefined) {
101
+ const descending = options?.descending === true;
102
+ const insertionIndex = items.findIndex((item) => {
103
+ const ts = getCreatedAt(item);
104
+ return descending
105
+ ? ts !== undefined && ts < decoded.createdAt
106
+ : ts !== undefined && ts > decoded.createdAt;
107
+ });
108
+ startIndex = insertionIndex === -1 ? items.length : insertionIndex;
109
+ }
110
+ else {
111
+ startIndex = 0;
112
+ }
113
+ }
114
+ else {
115
+ startIndex = afterIndex + 1;
116
+ }
117
+ }
118
+ const page = items.slice(startIndex, startIndex + limit);
119
+ const hasMore = startIndex + limit < items.length;
120
+ const lastItem = page[page.length - 1];
121
+ const nextCursor = hasMore && lastItem !== undefined
122
+ ? encodeCursor(getId(lastItem), getCreatedAt !== undefined ? getCreatedAt(lastItem) : undefined)
123
+ : undefined;
124
+ return { items: page, nextCursor, hasMore };
125
+ }
126
+ /**
127
+ * Check whether the request URL contains pagination parameters.
128
+ *
129
+ * Used to distinguish backward-compatible (no params → plain array)
130
+ * from paginated (with params → `PaginatedResponse<T>`) calls.
131
+ *
132
+ * @public
133
+ */
134
+ export function hasPaginationParams(url) {
135
+ return url.searchParams.has('limit') || url.searchParams.has('cursor');
136
+ }