@x12i/memorix-service 3.1.0 → 3.2.1

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 (113) hide show
  1. package/dist/ai-services/associate-prep.d.ts +37 -0
  2. package/dist/ai-services/associate-prep.d.ts.map +1 -0
  3. package/dist/ai-services/associate-prep.js +208 -0
  4. package/dist/ai-services/associate-prep.js.map +1 -0
  5. package/dist/ai-services/chunk-markdown.d.ts +22 -0
  6. package/dist/ai-services/chunk-markdown.d.ts.map +1 -0
  7. package/dist/ai-services/chunk-markdown.js +159 -0
  8. package/dist/ai-services/chunk-markdown.js.map +1 -0
  9. package/dist/ai-services/client.d.ts +1 -1
  10. package/dist/ai-services/client.d.ts.map +1 -1
  11. package/dist/ai-services/client.js +2 -2
  12. package/dist/ai-services/client.js.map +1 -1
  13. package/dist/ai-services/normalize-output.d.ts +7 -1
  14. package/dist/ai-services/normalize-output.d.ts.map +1 -1
  15. package/dist/ai-services/normalize-output.js +340 -60
  16. package/dist/ai-services/normalize-output.js.map +1 -1
  17. package/dist/ai-services/skill-execution.d.ts +3 -2
  18. package/dist/ai-services/skill-execution.d.ts.map +1 -1
  19. package/dist/ai-services/skill-execution.js +24 -4
  20. package/dist/ai-services/skill-execution.js.map +1 -1
  21. package/dist/app.d.ts.map +1 -1
  22. package/dist/app.js +4 -0
  23. package/dist/app.js.map +1 -1
  24. package/dist/connector-catalog.d.ts +79 -0
  25. package/dist/connector-catalog.d.ts.map +1 -0
  26. package/dist/connector-catalog.js +286 -0
  27. package/dist/connector-catalog.js.map +1 -0
  28. package/dist/connector-role.d.ts +22 -0
  29. package/dist/connector-role.d.ts.map +1 -0
  30. package/dist/connector-role.js +71 -0
  31. package/dist/connector-role.js.map +1 -0
  32. package/dist/destination-runtime.d.ts +8 -0
  33. package/dist/destination-runtime.d.ts.map +1 -0
  34. package/dist/destination-runtime.js +11 -0
  35. package/dist/destination-runtime.js.map +1 -0
  36. package/dist/execution-target.d.ts +141 -0
  37. package/dist/execution-target.d.ts.map +1 -0
  38. package/dist/execution-target.js +853 -0
  39. package/dist/execution-target.js.map +1 -0
  40. package/dist/generic-connectors.d.ts +43 -0
  41. package/dist/generic-connectors.d.ts.map +1 -0
  42. package/dist/generic-connectors.js +198 -0
  43. package/dist/generic-connectors.js.map +1 -0
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/jobs-scope.d.ts +21 -0
  49. package/dist/jobs-scope.d.ts.map +1 -0
  50. package/dist/jobs-scope.js +83 -0
  51. package/dist/jobs-scope.js.map +1 -0
  52. package/dist/live.d.ts +11 -0
  53. package/dist/live.d.ts.map +1 -1
  54. package/dist/live.js +700 -73
  55. package/dist/live.js.map +1 -1
  56. package/dist/magit/magit-pack-service.d.ts +80 -0
  57. package/dist/magit/magit-pack-service.d.ts.map +1 -1
  58. package/dist/magit/magit-pack-service.js +233 -11
  59. package/dist/magit/magit-pack-service.js.map +1 -1
  60. package/dist/magit/pack-to-patchset.d.ts +16 -0
  61. package/dist/magit/pack-to-patchset.d.ts.map +1 -1
  62. package/dist/magit/pack-to-patchset.js +46 -0
  63. package/dist/magit/pack-to-patchset.js.map +1 -1
  64. package/dist/magit/package-catalog-bridge.d.ts +5 -5
  65. package/dist/magit/package-catalog-bridge.d.ts.map +1 -1
  66. package/dist/magit/package-catalog-bridge.js +3 -3
  67. package/dist/magit/package-catalog-bridge.js.map +1 -1
  68. package/dist/mgc-packages.d.ts +11 -0
  69. package/dist/mgc-packages.d.ts.map +1 -0
  70. package/dist/mgc-packages.js +11 -0
  71. package/dist/mgc-packages.js.map +1 -0
  72. package/dist/openapi/openapi.json +3445 -791
  73. package/dist/pipeline-compile.d.ts +46 -3
  74. package/dist/pipeline-compile.d.ts.map +1 -1
  75. package/dist/pipeline-compile.js +455 -89
  76. package/dist/pipeline-compile.js.map +1 -1
  77. package/dist/pipeline-invoke.d.ts +63 -0
  78. package/dist/pipeline-invoke.d.ts.map +1 -0
  79. package/dist/pipeline-invoke.js +114 -0
  80. package/dist/pipeline-invoke.js.map +1 -0
  81. package/dist/pipeline-ports.d.ts +13 -0
  82. package/dist/pipeline-ports.d.ts.map +1 -0
  83. package/dist/pipeline-ports.js +104 -0
  84. package/dist/pipeline-ports.js.map +1 -0
  85. package/dist/platform.d.ts +30 -1
  86. package/dist/platform.d.ts.map +1 -1
  87. package/dist/platform.js +239 -8
  88. package/dist/platform.js.map +1 -1
  89. package/dist/routes/connector-invocations.d.ts.map +1 -1
  90. package/dist/routes/connector-invocations.js +18 -21
  91. package/dist/routes/connector-invocations.js.map +1 -1
  92. package/dist/routes/jobs.d.ts +4 -0
  93. package/dist/routes/jobs.d.ts.map +1 -0
  94. package/dist/routes/jobs.js +888 -0
  95. package/dist/routes/jobs.js.map +1 -0
  96. package/dist/routes/memory.d.ts.map +1 -1
  97. package/dist/routes/memory.js +148 -79
  98. package/dist/routes/memory.js.map +1 -1
  99. package/dist/routes/metadata.d.ts.map +1 -1
  100. package/dist/routes/metadata.js +171 -40
  101. package/dist/routes/metadata.js.map +1 -1
  102. package/dist/routes/pipelines.d.ts.map +1 -1
  103. package/dist/routes/pipelines.js +98 -22
  104. package/dist/routes/pipelines.js.map +1 -1
  105. package/dist/routes/relationships.d.ts.map +1 -1
  106. package/dist/routes/relationships.js +80 -2
  107. package/dist/routes/relationships.js.map +1 -1
  108. package/dist/runx/client.d.ts +18 -0
  109. package/dist/runx/client.d.ts.map +1 -0
  110. package/dist/runx/client.js +59 -0
  111. package/dist/runx/client.js.map +1 -0
  112. package/openapi/openapi.json +3445 -791
  113. package/package.json +16 -9
package/dist/live.js CHANGED
@@ -8,8 +8,41 @@ import { createTrafficRing, createInstrumentedFetch, } from "@x12i/api-live-view
8
8
  import { createServiceInvokePorts, } from "@x12i/memorix-pipeline/v2";
9
9
  import { issueCallbackGrant } from "./callback-grant.js";
10
10
  import { createAiServicesClient } from "./ai-services/client.js";
11
+ import { chunkMarkdown, markdownApproxTokens, } from "./ai-services/chunk-markdown.js";
11
12
  import { normalizeAiServiceStepOutput } from "./ai-services/normalize-output.js";
12
13
  import { defaultSkillExecutionForItem } from "./ai-services/skill-execution.js";
14
+ import { associateDraftChunkSize, associateExistingCap, chunkArray, draftKey as associateDraftKey, mergeAssociateOutputs, prepareExistingKnowxForAssociate, slimDraftsForAssociate, slimKnowxForAssociate, } from "./ai-services/associate-prep.js";
15
+ import { deliverViaPlatform } from "./destination-runtime.js";
16
+ const DEFAULT_MAX_DOC_TOKENS = 200_000;
17
+ /** Serial by default — OpenRouter cheap/default often 408s under parallel load. */
18
+ const DEFAULT_CHUNK_CONCURRENCY = 1;
19
+ const DEFAULT_CHUNK_RETRIES = 4;
20
+ const DEFAULT_CHUNK_TARGET_TOKENS = 1500;
21
+ /** Wired by platform so chunked AI extract can patch the running step attempt. */
22
+ let stepProgressReporter = null;
23
+ export function setStepProgressReporter(reporter) {
24
+ stepProgressReporter = reporter;
25
+ }
26
+ async function reportStepProgress(req, progress) {
27
+ const attemptId = req.attemptId?.trim();
28
+ if (!attemptId || !stepProgressReporter)
29
+ return;
30
+ try {
31
+ await stepProgressReporter(req.scope.orgId, attemptId, {
32
+ ...progress,
33
+ });
34
+ }
35
+ catch {
36
+ /* progress is best-effort */
37
+ }
38
+ }
39
+ function envInt(name, fallback) {
40
+ const raw = process.env[name]?.trim();
41
+ if (!raw)
42
+ return fallback;
43
+ const n = Number(raw);
44
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
45
+ }
13
46
  export const memorixLiveRing = createTrafficRing();
14
47
  /** Outbound Memory connector pulls (opx-source, REST fixtures). */
15
48
  export const memoryPullFetch = createInstrumentedFetch(memorixLiveRing, {
@@ -33,27 +66,56 @@ function resolveAccess(raw) {
33
66
  return raw.filter((a) => a === "explore" || a === "checkpoints" || a === "raw");
34
67
  }
35
68
  const aiClient = createAiServicesClient({ fetchImpl: pipelineWorkerFetch });
69
+ /**
70
+ * Paginate + collect knowx catalog for associate.
71
+ * Slim/cap applied later via prepareExistingKnowxForAssociate (same knobs as
72
+ * memorix-knowx-pack associate-ai.ts).
73
+ */
36
74
  async function fetchExistingKnowxForAssociate(memorix) {
37
75
  const objectTypes = ["content", "people", "work-items"];
38
76
  const out = [];
77
+ const seen = new Set();
39
78
  const headers = {
40
79
  "x-memorix-org-id": memorix.orgId,
41
80
  "x-memorix-agent-ids": memorix.agentIds.join(","),
42
81
  };
43
82
  if (memorix.grant)
44
83
  headers["x-memorix-invocation-grant"] = memorix.grant;
84
+ // Soft upper bound before slim/cap — avoid unbounded Explore walks.
85
+ const hardCap = Math.max(associateExistingCap() * 3, 360);
45
86
  for (const objectType of objectTypes) {
46
- try {
47
- const url = `${memorix.baseUrl}/api/data/content?objectType=${encodeURIComponent(objectType)}&contentType=knowx&limit=200`;
48
- const res = await pipelineWorkerFetch(url, { headers });
49
- if (!res.ok)
50
- continue;
51
- const body = (await res.json());
52
- if (Array.isArray(body.records))
53
- out.push(...body.records);
54
- }
55
- catch {
56
- /* best-effort */
87
+ let cursor;
88
+ for (let page = 0; page < 20 && out.length < hardCap; page++) {
89
+ try {
90
+ const q = new URLSearchParams({
91
+ objectType,
92
+ contentType: "knowx",
93
+ limit: "200",
94
+ });
95
+ if (cursor)
96
+ q.set("cursor", cursor);
97
+ const url = `${memorix.baseUrl}/api/data/content?${q}`;
98
+ const res = await pipelineWorkerFetch(url, { headers });
99
+ if (!res.ok)
100
+ break;
101
+ const body = (await res.json());
102
+ for (const raw of body.records ?? []) {
103
+ const slim = slimKnowxForAssociate(raw);
104
+ const key = associateDraftKey(slim);
105
+ if (!key || seen.has(key))
106
+ continue;
107
+ seen.add(key);
108
+ out.push(slim ?? raw);
109
+ if (out.length >= hardCap)
110
+ break;
111
+ }
112
+ cursor = body.pageInfo?.nextCursor ?? undefined;
113
+ if (!cursor)
114
+ break;
115
+ }
116
+ catch {
117
+ break;
118
+ }
57
119
  }
58
120
  }
59
121
  return out;
@@ -92,49 +154,407 @@ function collectKnowxDrafts(inputs) {
92
154
  }
93
155
  return out;
94
156
  }
95
- /** Shape KnowX skill input + stamp templateTokens.input for Rendrix {{input}}. */
96
- function prepareKnowxSkillInvoke(itemId, serviceId, inputs, execution) {
157
+ /**
158
+ * Shape KnowX FR `input` for skill invoke.
159
+ * ai-skills@0.3+ maps FR input to root `{{input}}` — do not stamp templateTokens.input.
160
+ */
161
+ function prepareKnowxSkillInvoke(itemId, serviceId, inputs) {
97
162
  const isExtract = serviceId === "knowx-extract" || itemId === "knowx.extract";
98
163
  const isAssociate = serviceId === "knowx-associate-knowledge" ||
99
164
  itemId === "knowx.associate-knowledge";
100
- let skillInput = inputs;
101
165
  if (isAssociate) {
102
- const drafts = collectKnowxDrafts(inputs);
103
- skillInput = {
104
- knowxDrafts: drafts.length ? drafts : inputs.knowxDrafts,
105
- existingKnowx: inputs.existingKnowx ?? [],
106
- };
166
+ const collected = collectKnowxDrafts(inputs);
167
+ const drafts = slimDraftsForAssociate(collected.length
168
+ ? collected
169
+ : Array.isArray(inputs.knowxDrafts)
170
+ ? inputs.knowxDrafts
171
+ : []);
172
+ const draftKeys = new Set(drafts
173
+ .map((d) => associateDraftKey(d))
174
+ .filter((k) => Boolean(k)));
175
+ const existingRaw = Array.isArray(inputs.existingKnowx)
176
+ ? inputs.existingKnowx
177
+ : [];
178
+ const existingKnowx = prepareExistingKnowxForAssociate({
179
+ existing: existingRaw,
180
+ draftKeys,
181
+ cap: associateExistingCap(),
182
+ });
183
+ return { knowxDrafts: drafts, existingKnowx };
107
184
  }
108
- else if (isExtract) {
109
- skillInput = {
185
+ if (isExtract) {
186
+ // Chunk fan-out builds a per-chunk bag; keep legacy fields for short docs / fixtures.
187
+ return {
110
188
  text: inputs.text,
111
189
  knowledgeSource: inputs.knowledgeSource,
112
190
  contentId: inputs.contentId,
113
191
  contentIds: inputs.contentIds,
114
192
  contentDrafts: inputs.contentDrafts,
193
+ chunkText: inputs.chunkText,
194
+ chunkIndex: inputs.chunkIndex,
195
+ chunkCount: inputs.chunkCount,
196
+ contentHash: inputs.contentHash,
197
+ contentRevision: inputs.contentRevision,
198
+ pageStart: inputs.pageStart,
199
+ pageEnd: inputs.pageEnd,
115
200
  };
116
201
  }
117
- let inputJson;
118
- try {
119
- inputJson = JSON.stringify(skillInput);
202
+ return inputs;
203
+ }
204
+ function asRecord(v) {
205
+ return v && typeof v === "object" && !Array.isArray(v)
206
+ ? v
207
+ : null;
208
+ }
209
+ /** Pull markdown sources for knowx.extract (contentDrafts, promote envelope, or inline text). */
210
+ function collectExtractSources(inputs) {
211
+ const out = [];
212
+ const drafts = [];
213
+ const top = inputs.contentDrafts;
214
+ if (Array.isArray(top))
215
+ drafts.push(...top);
216
+ for (const value of Object.values(inputs)) {
217
+ const o = asRecord(value);
218
+ if (!o)
219
+ continue;
220
+ for (const key of ["value", "body", "data", "output", "result"]) {
221
+ const nested = asRecord(o[key]);
222
+ if (nested && Array.isArray(nested.contentDrafts)) {
223
+ drafts.push(...nested.contentDrafts);
224
+ }
225
+ }
226
+ if (Array.isArray(o.contentDrafts)) {
227
+ drafts.push(...o.contentDrafts);
228
+ }
120
229
  }
121
- catch {
122
- inputJson = "{}";
230
+ const seen = new Set();
231
+ for (const d of drafts) {
232
+ const md = typeof d.markdown === "string" ? d.markdown.trim() : "";
233
+ if (!md)
234
+ continue;
235
+ const key = `${d.contentId ?? ""}:${d.contentHash ?? md.slice(0, 40)}`;
236
+ if (seen.has(key))
237
+ continue;
238
+ seen.add(key);
239
+ out.push({
240
+ contentId: typeof d.contentId === "string" ? d.contentId : undefined,
241
+ contentHash: typeof d.contentHash === "string" ? d.contentHash : undefined,
242
+ contentRevision: typeof d.revision === "number" ? d.revision : undefined,
243
+ title: typeof d.title === "string" ? d.title : undefined,
244
+ markdown: md,
245
+ });
246
+ }
247
+ if (!out.length) {
248
+ const text = typeof inputs.text === "string" ? inputs.text.trim() : "";
249
+ const ks = asRecord(inputs.knowledgeSource);
250
+ const ksBytes = typeof ks?.bytes === "string" ? String(ks.bytes).trim() : "";
251
+ const markdown = text || ksBytes;
252
+ if (markdown) {
253
+ out.push({
254
+ contentId: typeof inputs.contentId === "string" ? inputs.contentId : undefined,
255
+ markdown,
256
+ });
257
+ }
258
+ }
259
+ return out;
260
+ }
261
+ function draftKey(draft) {
262
+ if (!draft || typeof draft !== "object")
263
+ return "";
264
+ const d = draft;
265
+ const ot = String(d.objectType ?? "").trim();
266
+ const id = String(d.recordId ?? "").trim();
267
+ if (!ot || !id)
268
+ return "";
269
+ return `${ot}::${id}`;
270
+ }
271
+ function draftConfidence(draft) {
272
+ if (!draft || typeof draft !== "object")
273
+ return 0;
274
+ const data = asRecord(draft.data);
275
+ const epistemic = asRecord(data?.epistemic);
276
+ const score = Number(epistemic?.confidenceScore ?? 0);
277
+ return Number.isFinite(score) ? score : 0;
278
+ }
279
+ /** Merge chunk outputs: dedupe knowxDrafts by objectType::recordId (keep highest confidence). */
280
+ export function mergeKnowxExtractChunkOutputs(parts) {
281
+ const byKey = new Map();
282
+ const untitled = [];
283
+ let docHash = "";
284
+ for (const part of parts) {
285
+ if (typeof part.docHash === "string" && part.docHash && !docHash) {
286
+ docHash = part.docHash;
287
+ }
288
+ const drafts = Array.isArray(part.knowxDrafts) ? part.knowxDrafts : [];
289
+ for (const draft of drafts) {
290
+ const key = draftKey(draft);
291
+ if (!key) {
292
+ untitled.push(draft);
293
+ continue;
294
+ }
295
+ const prev = byKey.get(key);
296
+ if (!prev || draftConfidence(draft) >= draftConfidence(prev)) {
297
+ byKey.set(key, draft);
298
+ }
299
+ }
123
300
  }
124
- const priorTokens = execution.templateTokens && typeof execution.templateTokens === "object"
125
- ? execution.templateTokens
126
- : {};
127
301
  return {
128
- input: skillInput,
129
- execution: {
130
- ...execution,
131
- templateTokens: {
132
- ...priorTokens,
133
- input: inputJson,
134
- },
135
- },
302
+ knowxDrafts: [...byKey.values(), ...untitled],
303
+ ...(docHash ? { docHash } : {}),
304
+ kind: "ai-services",
305
+ live: true,
306
+ chunkCount: parts.length,
136
307
  };
137
308
  }
309
+ async function mapPool(items, concurrency, fn) {
310
+ const out = new Array(items.length);
311
+ let next = 0;
312
+ const workers = Array.from({ length: Math.min(concurrency, Math.max(1, items.length)) }, async () => {
313
+ while (next < items.length) {
314
+ const i = next++;
315
+ out[i] = await fn(items[i], i);
316
+ }
317
+ });
318
+ await Promise.all(workers);
319
+ return out;
320
+ }
321
+ function isAbortError(err) {
322
+ if (!err || typeof err !== "object")
323
+ return false;
324
+ const e = err;
325
+ return (e.name === "AbortError" ||
326
+ e.code === "ABORT_ERR" ||
327
+ /aborted|AbortError/i.test(String(e.message ?? "")));
328
+ }
329
+ async function invokeKnowxExtractChunked(req, args) {
330
+ const sources = collectExtractSources(args.inputs);
331
+ if (!sources.length) {
332
+ // No markdown bag — fall through to a single invoke with the prepared bag
333
+ // (activity-log / knowledgeSource fixtures).
334
+ return invokeAiServiceOnce(req, {
335
+ ...args,
336
+ invokeInput: prepareKnowxSkillInvoke(args.itemId, args.serviceId, args.inputs),
337
+ correlationSuffix: "extract",
338
+ });
339
+ }
340
+ const maxDocTokens = envInt("KNOWX_AI_MAX_DOC_TOKENS", DEFAULT_MAX_DOC_TOKENS);
341
+ const targetTokens = envInt("KNOWX_AI_CHUNK_TOKENS", DEFAULT_CHUNK_TARGET_TOKENS);
342
+ const concurrency = envInt("KNOWX_AI_CHUNK_CONCURRENCY", DEFAULT_CHUNK_CONCURRENCY);
343
+ const work = [];
344
+ for (const source of sources) {
345
+ const tokens = markdownApproxTokens(source.markdown);
346
+ if (tokens > maxDocTokens) {
347
+ return {
348
+ status: "failed",
349
+ retryable: false,
350
+ error: {
351
+ code: "AI_DOC_TOO_LARGE",
352
+ message: `Document ~${tokens} tokens exceeds KNOWX_AI_MAX_DOC_TOKENS=${maxDocTokens}${source.contentId ? ` (contentId=${source.contentId})` : ""}`,
353
+ },
354
+ };
355
+ }
356
+ const chunks = chunkMarkdown(source.markdown, {
357
+ targetTokens,
358
+ overlapRatio: 0.1,
359
+ });
360
+ if (!chunks.length)
361
+ continue;
362
+ for (const chunk of chunks) {
363
+ work.push({ source, chunk });
364
+ }
365
+ }
366
+ if (!work.length) {
367
+ return {
368
+ status: "failed",
369
+ retryable: false,
370
+ error: {
371
+ code: "AI_EXTRACT_EMPTY",
372
+ message: "knowx.extract found no extractable markdown chunks",
373
+ },
374
+ };
375
+ }
376
+ // Short single-chunk docs: one invoke (preserve fixture behaviour).
377
+ if (work.length === 1 && work[0].chunk.count === 1) {
378
+ const w = work[0];
379
+ const bag = {
380
+ text: w.chunk.text,
381
+ contentId: w.source.contentId ?? args.inputs.contentId,
382
+ contentIds: w.source.contentId
383
+ ? [w.source.contentId]
384
+ : args.inputs.contentIds,
385
+ contentHash: w.source.contentHash,
386
+ contentRevision: w.source.contentRevision,
387
+ chunkText: w.chunk.text,
388
+ chunkIndex: 0,
389
+ chunkCount: 1,
390
+ pageStart: w.chunk.pageStart,
391
+ pageEnd: w.chunk.pageEnd,
392
+ knowledgeSource: args.inputs.knowledgeSource,
393
+ };
394
+ return invokeAiServiceOnce(req, {
395
+ ...args,
396
+ invokeInput: prepareKnowxSkillInvoke(args.itemId, args.serviceId, bag),
397
+ correlationSuffix: "c0",
398
+ });
399
+ }
400
+ const retries = envInt("KNOWX_AI_CHUNK_RETRIES", DEFAULT_CHUNK_RETRIES);
401
+ let draftsSoFar = 0;
402
+ let chunksDone = 0;
403
+ await reportStepProgress(req, {
404
+ chunkIndex: 0,
405
+ chunkCount: work.length,
406
+ draftsSoFar: 0,
407
+ message: `Extracting chunk 0/${work.length}`,
408
+ });
409
+ const results = await mapPool(work, concurrency, async (w, i) => {
410
+ const bag = {
411
+ chunkText: w.chunk.text,
412
+ chunkIndex: w.chunk.index,
413
+ chunkCount: w.chunk.count,
414
+ contentId: w.source.contentId,
415
+ contentHash: w.source.contentHash,
416
+ contentRevision: w.source.contentRevision,
417
+ pageStart: w.chunk.pageStart,
418
+ pageEnd: w.chunk.pageEnd,
419
+ title: w.source.title,
420
+ // Prefer chunkText in the prompt; keep text as the chunk for {{input}} templates.
421
+ text: w.chunk.text,
422
+ };
423
+ let last;
424
+ for (let attempt = 0; attempt <= retries; attempt++) {
425
+ last = await invokeAiServiceOnce(req, {
426
+ ...args,
427
+ invokeInput: prepareKnowxSkillInvoke(args.itemId, args.serviceId, bag),
428
+ correlationSuffix: `c${w.chunk.index}-${i}:a${attempt}`,
429
+ timeoutMs: 180_000,
430
+ });
431
+ if (last.status === "succeeded") {
432
+ const part = (last.output ?? {});
433
+ const drafts = Array.isArray(part.knowxDrafts) ? part.knowxDrafts.length : 0;
434
+ chunksDone += 1;
435
+ draftsSoFar += drafts;
436
+ await reportStepProgress(req, {
437
+ chunkIndex: chunksDone,
438
+ chunkCount: work.length,
439
+ draftsSoFar,
440
+ message: `Extracting chunk ${chunksDone}/${work.length}`,
441
+ });
442
+ return last;
443
+ }
444
+ const msg = `${last.error?.code ?? ""} ${last.error?.message ?? ""}`;
445
+ const retryable = last.retryable || /timeout|408|429|REMOTE_UNREACHABLE|INVOKE_FAILED/i.test(msg);
446
+ if (!retryable || attempt >= retries)
447
+ return last;
448
+ await new Promise((r) => setTimeout(r, 1500 * (attempt + 1)));
449
+ }
450
+ return last;
451
+ });
452
+ const succeeded = [];
453
+ for (const r of results) {
454
+ if (r.status !== "succeeded" || !r.output) {
455
+ return {
456
+ status: "failed",
457
+ retryable: r.retryable ?? false,
458
+ error: r.error ?? {
459
+ code: "AI_EXTRACT_CHUNK_FAILED",
460
+ message: "knowx.extract chunk invoke failed",
461
+ },
462
+ };
463
+ }
464
+ succeeded.push(r.output);
465
+ }
466
+ const merged = mergeKnowxExtractChunkOutputs(succeeded);
467
+ if (!Array.isArray(merged.knowxDrafts) || merged.knowxDrafts.length === 0) {
468
+ return {
469
+ status: "failed",
470
+ retryable: false,
471
+ error: {
472
+ code: "AI_OUTPUT_MISSING_DRAFTS",
473
+ message: "knowx.extract chunk fan-out produced no knowxDrafts after merge",
474
+ },
475
+ };
476
+ }
477
+ await reportStepProgress(req, {
478
+ chunkIndex: work.length,
479
+ chunkCount: work.length,
480
+ draftsSoFar: Array.isArray(merged.knowxDrafts)
481
+ ? merged.knowxDrafts.length
482
+ : draftsSoFar,
483
+ message: "Extract complete",
484
+ });
485
+ return { status: "succeeded", output: merged };
486
+ }
487
+ async function invokeAiServiceOnce(req, args) {
488
+ const correlationId = `${req.runId}:${req.stepId}:${req.attempt}:${args.correlationSuffix}`;
489
+ try {
490
+ const { status, body } = await aiClient.invoke(args.endpoint, {
491
+ itemId: args.itemId,
492
+ input: args.invokeInput,
493
+ execution: Object.keys(args.execution).length
494
+ ? args.execution
495
+ : undefined,
496
+ correlationId,
497
+ }, correlationId, args.timeoutMs);
498
+ if (status < 200 || status >= 300 || body.ok === false) {
499
+ return {
500
+ status: "failed",
501
+ retryable: Boolean(body.error?.retryable),
502
+ error: {
503
+ code: body.error?.code ?? "AI_SERVICE_INVOKE_FAILED",
504
+ message: body.error?.message ?? `HTTP ${status}`,
505
+ },
506
+ };
507
+ }
508
+ const normalized = normalizeAiServiceStepOutput(body, {
509
+ itemId: args.itemId,
510
+ serviceId: args.serviceId,
511
+ });
512
+ if (!normalized.ok) {
513
+ try {
514
+ const dumpPath = `/tmp/knowx-ai-unparseable-${args.correlationSuffix}-${Date.now()}.json`;
515
+ const { writeFileSync } = await import("node:fs");
516
+ writeFileSync(dumpPath, JSON.stringify({
517
+ itemId: args.itemId,
518
+ serviceId: args.serviceId,
519
+ correlationSuffix: args.correlationSuffix,
520
+ code: normalized.code,
521
+ message: normalized.message,
522
+ body,
523
+ }, null, 2));
524
+ console.error(`[ai-services] unparseable dump ${dumpPath}`);
525
+ }
526
+ catch {
527
+ /* ignore dump errors */
528
+ }
529
+ return {
530
+ status: "failed",
531
+ retryable: normalized.code === "AI_OUTPUT_EMPTY",
532
+ error: { code: normalized.code, message: normalized.message },
533
+ };
534
+ }
535
+ return { status: "succeeded", output: normalized.output };
536
+ }
537
+ catch (err) {
538
+ if (isAbortError(err)) {
539
+ return {
540
+ status: "failed",
541
+ retryable: true,
542
+ error: {
543
+ code: "AI_EXTRACT_TIMEOUT",
544
+ message: err instanceof Error ? err.message : "AI extract timed out",
545
+ },
546
+ };
547
+ }
548
+ return {
549
+ status: "failed",
550
+ retryable: true,
551
+ error: {
552
+ code: "REMOTE_UNREACHABLE",
553
+ message: err instanceof Error ? err.message : String(err),
554
+ },
555
+ };
556
+ }
557
+ }
138
558
  async function invokeAiServiceRemote(req, memorix) {
139
559
  const c = (req.config ?? {});
140
560
  const endpoint = String(c.endpoint ?? "").replace(/\/+$/, "");
@@ -155,7 +575,6 @@ async function invokeAiServiceRemote(req, memorix) {
155
575
  error: { code: "AI_ITEM_MISSING", message: "itemId required" },
156
576
  };
157
577
  }
158
- const correlationId = `${req.runId}:${req.stepId}:${req.attempt}`;
159
578
  const inputs = {
160
579
  ...(req.inputs && typeof req.inputs === "object"
161
580
  ? req.inputs
@@ -165,7 +584,8 @@ async function invokeAiServiceRemote(req, memorix) {
165
584
  if ((serviceId === "knowx-associate-knowledge" ||
166
585
  itemId === "knowx.associate-knowledge") &&
167
586
  memorix &&
168
- inputs.existingKnowx === undefined) {
587
+ (inputs.existingKnowx === undefined ||
588
+ (Array.isArray(inputs.existingKnowx) && inputs.existingKnowx.length === 0))) {
169
589
  const existing = await fetchExistingKnowxForAssociate(memorix);
170
590
  if (existing.length)
171
591
  inputs.existingKnowx = existing;
@@ -179,57 +599,156 @@ async function invokeAiServiceRemote(req, memorix) {
179
599
  stepId: req.stepId,
180
600
  })
181
601
  : undefined;
182
- let execution = {
602
+ const execution = {
183
603
  ...skillDefaults,
184
604
  ...configExec,
185
605
  };
186
- let invokeInput = inputs;
606
+ const isKnowxExtract = serviceId === "knowx-extract" || itemId === "knowx.extract";
607
+ const isKnowxAssociate = serviceId === "knowx-associate-knowledge" ||
608
+ itemId === "knowx.associate-knowledge";
187
609
  if (aiServiceType === "skill" &&
610
+ isKnowxExtract &&
188
611
  (itemId.startsWith("knowx.") || serviceId.startsWith("knowx-"))) {
189
- const prepared = prepareKnowxSkillInvoke(itemId, serviceId, inputs, execution);
190
- invokeInput = prepared.input;
191
- execution = prepared.execution;
612
+ return invokeKnowxExtractChunked(req, {
613
+ endpoint,
614
+ itemId,
615
+ serviceId,
616
+ execution,
617
+ inputs,
618
+ });
192
619
  }
193
- try {
194
- const { status, body } = await aiClient.invoke(endpoint, {
620
+ if (aiServiceType === "skill" &&
621
+ isKnowxAssociate &&
622
+ (itemId.startsWith("knowx.") || serviceId.startsWith("knowx-"))) {
623
+ return invokeKnowxAssociateChunked(req, {
624
+ endpoint,
195
625
  itemId,
196
- input: invokeInput,
197
- execution: Object.keys(execution).length ? execution : undefined,
198
- correlationId,
199
- }, correlationId);
200
- if (status < 200 || status >= 300 || body.ok === false) {
201
- return {
202
- status: "failed",
203
- retryable: Boolean(body.error?.retryable),
204
- error: {
205
- code: body.error?.code ?? "AI_SERVICE_INVOKE_FAILED",
206
- message: body.error?.message ?? `HTTP ${status}`,
626
+ serviceId,
627
+ execution,
628
+ inputs,
629
+ });
630
+ }
631
+ let invokeInput = inputs;
632
+ if (aiServiceType === "skill" &&
633
+ (itemId.startsWith("knowx.") || serviceId.startsWith("knowx-"))) {
634
+ invokeInput = prepareKnowxSkillInvoke(itemId, serviceId, inputs);
635
+ }
636
+ return invokeAiServiceOnce(req, {
637
+ endpoint,
638
+ itemId,
639
+ serviceId,
640
+ execution,
641
+ invokeInput,
642
+ correlationSuffix: "once",
643
+ });
644
+ }
645
+ /**
646
+ * Fan-out knowx.associate-knowledge when draft count exceeds chunk size.
647
+ * Mirrors memorix-knowx-pack associate-ai draft chunking.
648
+ */
649
+ async function invokeKnowxAssociateChunked(req, args) {
650
+ const prepared = prepareKnowxSkillInvoke(args.itemId, args.serviceId, args.inputs);
651
+ const drafts = Array.isArray(prepared.knowxDrafts)
652
+ ? prepared.knowxDrafts
653
+ : [];
654
+ const existingKnowx = Array.isArray(prepared.existingKnowx)
655
+ ? prepared.existingKnowx
656
+ : [];
657
+ const chunks = chunkArray(drafts, associateDraftChunkSize());
658
+ const outputs = [];
659
+ for (let i = 0; i < chunks.length; i++) {
660
+ const chunkDrafts = chunks[i];
661
+ await reportStepProgress(req, {
662
+ chunkIndex: i + 1,
663
+ chunkCount: chunks.length,
664
+ message: `Associate chunk ${i + 1}/${chunks.length}`,
665
+ });
666
+ let once = await invokeAiServiceOnce(req, {
667
+ endpoint: args.endpoint,
668
+ itemId: args.itemId,
669
+ serviceId: args.serviceId,
670
+ execution: args.execution,
671
+ invokeInput: {
672
+ knowxDrafts: chunkDrafts,
673
+ existingKnowx,
674
+ },
675
+ correlationSuffix: `assoc-${i + 1}-of-${chunks.length}`,
676
+ });
677
+ // Retries: empty companion responses are flaky on deepseek-v4-flash.
678
+ const retriable = (code) => code === "AI_OUTPUT_UNPARSEABLE" ||
679
+ code === "AI_OUTPUT_MISSING_ASSOCIATE" ||
680
+ code === "AI_OUTPUT_EMPTY";
681
+ for (let attempt = 0; attempt < 2 && once.status !== "succeeded"; attempt++) {
682
+ if (!retriable(once.error?.code))
683
+ break;
684
+ once = await invokeAiServiceOnce(req, {
685
+ endpoint: args.endpoint,
686
+ itemId: args.itemId,
687
+ serviceId: args.serviceId,
688
+ execution: args.execution,
689
+ invokeInput: {
690
+ knowxDrafts: chunkDrafts,
691
+ existingKnowx,
207
692
  },
208
- };
693
+ correlationSuffix: `assoc-${i + 1}-of-${chunks.length}-retry${attempt + 1}`,
694
+ });
209
695
  }
210
- const normalized = normalizeAiServiceStepOutput(body, { itemId, serviceId });
211
- if (!normalized.ok) {
212
- return {
213
- status: "failed",
214
- retryable: false,
215
- error: { code: normalized.code, message: normalized.message },
216
- };
696
+ if (once.status !== "succeeded" && retriable(once.error?.code)) {
697
+ const shrunk = existingKnowx.length > 4
698
+ ? existingKnowx.slice(0, Math.max(4, Math.floor(existingKnowx.length / 2)))
699
+ : existingKnowx;
700
+ once = await invokeAiServiceOnce(req, {
701
+ endpoint: args.endpoint,
702
+ itemId: args.itemId,
703
+ serviceId: args.serviceId,
704
+ execution: args.execution,
705
+ invokeInput: {
706
+ knowxDrafts: chunkDrafts,
707
+ existingKnowx: shrunk,
708
+ },
709
+ correlationSuffix: `assoc-${i + 1}-of-${chunks.length}-shrink`,
710
+ });
217
711
  }
218
- return {
219
- status: "succeeded",
220
- output: normalized.output,
221
- };
712
+ // Extra retries with full catalog — empty responses are intermittent.
713
+ for (let attempt = 0; attempt < 3 && once.status !== "succeeded" && retriable(once.error?.code); attempt++) {
714
+ once = await invokeAiServiceOnce(req, {
715
+ endpoint: args.endpoint,
716
+ itemId: args.itemId,
717
+ serviceId: args.serviceId,
718
+ execution: args.execution,
719
+ invokeInput: {
720
+ knowxDrafts: chunkDrafts,
721
+ existingKnowx,
722
+ },
723
+ correlationSuffix: `assoc-${i + 1}-of-${chunks.length}-persist${attempt + 1}`,
724
+ });
725
+ }
726
+ if (once.status !== "succeeded") {
727
+ return once;
728
+ }
729
+ const out = once.output && typeof once.output === "object"
730
+ ? once.output
731
+ : {};
732
+ outputs.push(out);
222
733
  }
223
- catch (err) {
734
+ const merged = mergeAssociateOutputs(outputs);
735
+ if (!asRecord(merged.classification) &&
736
+ !Array.isArray(merged.relationshipPlans)) {
224
737
  return {
225
738
  status: "failed",
226
- retryable: true,
739
+ retryable: false,
227
740
  error: {
228
- code: "REMOTE_UNREACHABLE",
229
- message: err instanceof Error ? err.message : String(err),
741
+ code: "AI_OUTPUT_MISSING_ASSOCIATE",
742
+ message: "knowx.associate-knowledge output missing classification / relationshipPlans",
230
743
  },
231
744
  };
232
745
  }
746
+ await reportStepProgress(req, {
747
+ chunkIndex: chunks.length,
748
+ chunkCount: chunks.length,
749
+ message: "Associate complete",
750
+ });
751
+ return { status: "succeeded", output: merged };
233
752
  }
234
753
  const baseServiceInvokePorts = createServiceInvokePorts({
235
754
  remoteFetch: pipelineWorkerFetch,
@@ -262,6 +781,9 @@ export const serviceInvokePorts = {
262
781
  invokeInProcess: baseServiceInvokePorts.invokeInProcess,
263
782
  invokeRemote: async (req) => {
264
783
  const c = (req.config ?? {});
784
+ if (c.invokeProtocol === "memorix-destination/1") {
785
+ return invokeDestinationDeliver(req);
786
+ }
265
787
  if (c.invokeProtocol === "ai-services/1" ||
266
788
  (typeof c.aiServiceType === "string" &&
267
789
  c.aiServiceType &&
@@ -292,4 +814,109 @@ export const serviceInvokePorts = {
292
814
  return baseServiceInvokePorts.invokeRemote(req);
293
815
  },
294
816
  };
817
+ function inventDeliverItems(inputs) {
818
+ if (!inputs || typeof inputs !== "object")
819
+ return [];
820
+ const o = inputs;
821
+ if (Array.isArray(o.items)) {
822
+ return o.items.filter((x) => Boolean(x) && typeof x === "object");
823
+ }
824
+ if (Array.isArray(o.records)) {
825
+ return o.records
826
+ .filter((x) => Boolean(x) && typeof x === "object")
827
+ .map((r) => ({
828
+ recordId: typeof r.recordId === "string" ? r.recordId : typeof r.id === "string" ? r.id : undefined,
829
+ objectType: typeof r.objectType === "string" ? r.objectType : undefined,
830
+ contentType: typeof r.contentType === "string" ? r.contentType : undefined,
831
+ payload: (r.payload && typeof r.payload === "object"
832
+ ? r.payload
833
+ : r),
834
+ identity: typeof r.identity === "string" ? r.identity : undefined,
835
+ }));
836
+ }
837
+ // Single-item envelope from work selector.
838
+ if (typeof o.recordId === "string" || typeof o.id === "string" || o.payload) {
839
+ return [
840
+ {
841
+ recordId: typeof o.recordId === "string"
842
+ ? o.recordId
843
+ : typeof o.id === "string"
844
+ ? o.id
845
+ : undefined,
846
+ objectType: typeof o.objectType === "string" ? o.objectType : undefined,
847
+ contentType: typeof o.contentType === "string" ? o.contentType : undefined,
848
+ payload: o.payload && typeof o.payload === "object"
849
+ ? o.payload
850
+ : o,
851
+ identity: typeof o.identity === "string" ? o.identity : undefined,
852
+ },
853
+ ];
854
+ }
855
+ return [];
856
+ }
857
+ async function invokeDestinationDeliver(req) {
858
+ const c = (req.config ?? {});
859
+ const destinationId = String(c.destinationId ?? "").trim();
860
+ if (!destinationId) {
861
+ return {
862
+ status: "failed",
863
+ retryable: false,
864
+ error: {
865
+ code: "DESTINATION_ID_MISSING",
866
+ message: "destinationId required for memorix-destination/1",
867
+ },
868
+ };
869
+ }
870
+ const items = inventDeliverItems(req.inputs);
871
+ if (!items.length) {
872
+ return {
873
+ status: "failed",
874
+ retryable: false,
875
+ error: {
876
+ code: "DELIVER_ITEMS_MISSING",
877
+ message: "items, records, or a single item payload required for destination deliver",
878
+ },
879
+ };
880
+ }
881
+ try {
882
+ const receipt = await deliverViaPlatform({
883
+ orgId: req.scope.orgId,
884
+ destinationId,
885
+ items,
886
+ workRef: `${req.runId}:${req.stepId}`,
887
+ mode: c.mode === "context" || c.mode === "knowledge"
888
+ ? c.mode
889
+ : undefined,
890
+ });
891
+ if (receipt.status === "failed") {
892
+ return {
893
+ status: "failed",
894
+ retryable: false,
895
+ error: {
896
+ code: receipt.failureSummary?.code ?? "MEMORY_DELIVER_FAILED",
897
+ message: receipt.failureSummary?.message ??
898
+ `deliver to ${destinationId} failed`,
899
+ },
900
+ output: receipt,
901
+ };
902
+ }
903
+ return {
904
+ status: "succeeded",
905
+ output: receipt,
906
+ };
907
+ }
908
+ catch (err) {
909
+ const message = err instanceof Error ? err.message : String(err);
910
+ return {
911
+ status: "failed",
912
+ retryable: false,
913
+ error: {
914
+ code: message.includes("MEMORY_DELIVERER_UNAVAILABLE")
915
+ ? "MEMORY_DELIVERER_UNAVAILABLE"
916
+ : "MEMORY_DELIVER_ERROR",
917
+ message,
918
+ },
919
+ };
920
+ }
921
+ }
295
922
  //# sourceMappingURL=live.js.map