maka-agent 0.2.0-dev.31.20260913 → 0.2.0-dev.32.20260914

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 (71) hide show
  1. package/dist/pi-transcript.js +3 -4
  2. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  3. package/node_modules/@maka/core/dist/agent-graph-schedule.js +15 -4
  4. package/node_modules/@maka/core/dist/executor-id.js +22 -0
  5. package/node_modules/@maka/core/dist/external-session.js +18 -0
  6. package/node_modules/@maka/core/dist/model-call-usage-projection.js +0 -13
  7. package/node_modules/@maka/core/dist/runtime-event.js +19 -3
  8. package/node_modules/@maka/core/dist/session-send-projection.js +6 -3
  9. package/node_modules/@maka/core/dist/session.js +8 -2
  10. package/node_modules/@maka/core/dist/shell-run-result.js +1 -0
  11. package/node_modules/@maka/core/dist/shell-run.js +4 -0
  12. package/node_modules/@maka/core/dist/work-board.js +94 -1
  13. package/node_modules/@maka/core/package.json +1 -0
  14. package/node_modules/@maka/eval/dist/fleet-simulation.js +270 -0
  15. package/node_modules/@maka/eval/dist/fleet-store.js +243 -0
  16. package/node_modules/@maka/eval/dist/fleet-worker.js +125 -0
  17. package/node_modules/@maka/eval/dist/fleet.js +329 -0
  18. package/node_modules/@maka/eval/dist/index.js +3 -0
  19. package/node_modules/@maka/runtime/dist/agent-run.js +2 -17
  20. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +6 -2
  21. package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +36 -24
  22. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +257 -330
  23. package/node_modules/@maka/runtime/dist/background-task-health-tool.js +104 -0
  24. package/node_modules/@maka/runtime/dist/history-compaction.js +3 -1
  25. package/node_modules/@maka/runtime/dist/local-web-fetch.js +1 -1
  26. package/node_modules/@maka/runtime/dist/model-adapter.js +62 -53
  27. package/node_modules/@maka/runtime/dist/model-history.js +1 -5
  28. package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +311 -0
  29. package/node_modules/@maka/runtime/dist/plugin-executor-service.js +344 -0
  30. package/node_modules/@maka/runtime/dist/provider-error-classification.js +7 -1
  31. package/node_modules/@maka/runtime/dist/provider-request-telemetry.js +35 -8
  32. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +7 -1
  33. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +1 -0
  34. package/node_modules/@maka/runtime/dist/runtime-invocation-route.js +49 -0
  35. package/node_modules/@maka/runtime/dist/runtime-kernel.js +72 -150
  36. package/node_modules/@maka/runtime/dist/runtime-read-model.js +0 -2
  37. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -0
  38. package/node_modules/@maka/runtime/dist/session-manager.js +100 -58
  39. package/node_modules/@maka/runtime/dist/shell-run-manager.js +9 -3
  40. package/node_modules/@maka/runtime/dist/shell-run-tool-result.js +1 -0
  41. package/node_modules/@maka/runtime/dist/stream-graph-schedule-reconcile.js +1 -0
  42. package/node_modules/@maka/runtime/dist/stream-graph-supervisor-tools.js +26 -4
  43. package/node_modules/@maka/runtime/dist/subagent-tools.js +7 -0
  44. package/node_modules/@maka/runtime/dist/tool-runtime.js +45 -466
  45. package/node_modules/@maka/runtime/package.json +3 -0
  46. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +7 -2
  47. package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +1 -0
  48. package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +24 -3
  49. package/node_modules/@maka/runtime-host/dist/protocol/index.js +4 -1
  50. package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +42 -5
  51. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +31 -3
  52. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +6 -0
  53. package/node_modules/@maka/runtime-host/dist/server/child-agent-composition.js +1 -0
  54. package/node_modules/@maka/runtime-host/dist/server/execution-artifacts.js +1 -22
  55. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +54 -8
  56. package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +14 -1
  57. package/node_modules/@maka/runtime-host/dist/server/host-session-availability.js +10 -2
  58. package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +6 -0
  59. package/node_modules/@maka/runtime-host/dist/server/plugin-platform.js +6 -0
  60. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +56 -16
  61. package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +4 -3
  62. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +4 -0
  63. package/node_modules/@maka/runtime-host/dist/server/web-fetch-tool.js +37 -1
  64. package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +450 -234
  65. package/node_modules/@maka/storage/dist/claude-code-transcript-lineage.js +106 -77
  66. package/node_modules/@maka/storage/dist/legacy-run-header.js +2 -2
  67. package/node_modules/@maka/storage/dist/model-call-usage-sql.js +1 -1
  68. package/node_modules/@maka/storage/dist/session-store.js +12 -2
  69. package/node_modules/@maka/storage/dist/sqlite-long-term-memory-store.js +86 -12
  70. package/node_modules/@maka/storage/dist/work-board-store.js +40 -1
  71. package/package.json +1 -1
@@ -20,6 +20,102 @@ function stringField(record, key) {
20
20
  const value = record[key];
21
21
  return typeof value === 'string' && value.length > 0 ? value : undefined;
22
22
  }
23
+ /**
24
+ * Incremental lineage index used by large transcript imports.
25
+ *
26
+ * It retains only graph identity, never complete transcript records. Once the
27
+ * first pass is complete, independent filters can replay the fixed source
28
+ * snapshot while applying the same de-duplication and rewind selection as
29
+ * {@link resolveTranscriptLineage}.
30
+ */
31
+ export class TranscriptLineageIndexer {
32
+ #nodes = new Map();
33
+ #duplicates = 0;
34
+ #compactBoundaries = 0;
35
+ accept(record) {
36
+ const uuid = stringField(record, 'uuid');
37
+ if (uuid !== undefined && this.#nodes.has(uuid)) {
38
+ this.#duplicates += 1;
39
+ return;
40
+ }
41
+ if (record.subtype === 'compact_boundary')
42
+ this.#compactBoundaries += 1;
43
+ if (uuid === undefined)
44
+ return;
45
+ const parentUuid = transcriptParentUuid(record);
46
+ this.#nodes.set(uuid, {
47
+ uuid,
48
+ ...(parentUuid !== undefined ? { parentUuid } : {}),
49
+ isPrompt: isPromptRecord(record),
50
+ isSidechain: record.isSidechain === true,
51
+ });
52
+ }
53
+ finish() {
54
+ const main = [...this.#nodes.values()].filter((node) => !node.isSidechain);
55
+ const present = new Set(main.map((node) => node.uuid));
56
+ const childrenOf = new Map();
57
+ const promptsOf = new Map();
58
+ for (const node of main) {
59
+ const key = node.parentUuid !== undefined && present.has(node.parentUuid) ? node.parentUuid : ROOT_KEY;
60
+ const children = childrenOf.get(key);
61
+ if (children)
62
+ children.push(node.uuid);
63
+ else
64
+ childrenOf.set(key, [node.uuid]);
65
+ if (node.isPrompt) {
66
+ const prompts = promptsOf.get(key);
67
+ if (prompts)
68
+ prompts.push(node.uuid);
69
+ else
70
+ promptsOf.set(key, [node.uuid]);
71
+ }
72
+ }
73
+ const withdrawn = [];
74
+ for (const prompts of promptsOf.values()) {
75
+ if (prompts.length > 1)
76
+ withdrawn.push(...prompts.slice(0, -1));
77
+ }
78
+ const dropped = new Set();
79
+ const stack = [...withdrawn];
80
+ while (stack.length > 0) {
81
+ const uuid = stack.pop();
82
+ if (dropped.has(uuid))
83
+ continue;
84
+ dropped.add(uuid);
85
+ for (const child of childrenOf.get(uuid) ?? [])
86
+ stack.push(child);
87
+ }
88
+ return new FinishedTranscriptLineageIndex(dropped, withdrawn.length, this.#duplicates, this.#compactBoundaries);
89
+ }
90
+ }
91
+ class FinishedTranscriptLineageIndex {
92
+ withdrawnPrompts;
93
+ duplicates;
94
+ compactBoundaries;
95
+ #dropped;
96
+ abandoned;
97
+ constructor(dropped, withdrawnPrompts, duplicates, compactBoundaries) {
98
+ this.withdrawnPrompts = withdrawnPrompts;
99
+ this.duplicates = duplicates;
100
+ this.compactBoundaries = compactBoundaries;
101
+ this.#dropped = dropped;
102
+ this.abandoned = this.#dropped.size;
103
+ }
104
+ createFilter() {
105
+ const seen = new Set();
106
+ return {
107
+ keep: (record) => {
108
+ const uuid = stringField(record, 'uuid');
109
+ if (uuid === undefined)
110
+ return true;
111
+ if (seen.has(uuid))
112
+ return false;
113
+ seen.add(uuid);
114
+ return !this.#dropped.has(uuid);
115
+ },
116
+ };
117
+ }
118
+ }
23
119
  /**
24
120
  * The parent a record hangs from: `parentUuid`, and nothing else.
25
121
  *
@@ -52,86 +148,19 @@ export function isPromptRecord(record) {
52
148
  block !== null &&
53
149
  block.type === 'tool_result');
54
150
  }
55
- /**
56
- * Drop repeats of a `uuid` already seen, keeping the first.
57
- *
58
- * A record written twice replays whatever identity it carries — a prompt, a
59
- * turn boundary, a tool call. Measured: 3 repeats across 1130 local
60
- * transcripts. Rare enough to be invisible in testing, permanent once it is
61
- * persisted as canonical history.
62
- */
63
- function deduplicate(records) {
64
- const seen = new Set();
65
- const kept = [];
66
- let duplicates = 0;
67
- for (const record of records) {
68
- const uuid = stringField(record, 'uuid');
69
- if (uuid !== undefined) {
70
- if (seen.has(uuid)) {
71
- duplicates += 1;
72
- continue;
73
- }
74
- seen.add(uuid);
75
- }
76
- kept.push(record);
77
- }
78
- return { kept, duplicates };
79
- }
80
151
  const ROOT_KEY = ' root';
81
152
  export function resolveTranscriptLineage(rawRecords) {
82
- const { kept: records, duplicates } = deduplicate(rawRecords);
83
- const compactBoundaries = records.filter((r) => r.subtype === 'compact_boundary').length;
84
- const main = records.filter((r) => r.isSidechain !== true && stringField(r, 'uuid') !== undefined);
85
- if (main.length === 0) {
86
- return { records, abandoned: 0, withdrawnPrompts: 0, duplicates, compactBoundaries };
87
- }
88
- const present = new Set(main.map((r) => stringField(r, 'uuid')));
89
- const childrenOf = new Map();
90
- for (const record of main) {
91
- const parent = transcriptParentUuid(record);
92
- // A parent outside this file is no parent: the record roots its own
93
- // segment rather than being orphaned into nothing.
94
- const key = parent !== undefined && present.has(parent) ? parent : ROOT_KEY;
95
- const siblings = childrenOf.get(key);
96
- if (siblings)
97
- siblings.push(record);
98
- else
99
- childrenOf.set(key, [record]);
100
- }
101
- // Among sibling prompts the last written is the one that was asked; the
102
- // earlier ones were withdrawn by the edit that replaced them.
103
- const withdrawn = [];
104
- for (const [, siblings] of childrenOf) {
105
- const prompts = siblings.filter(isPromptRecord);
106
- if (prompts.length < 2)
107
- continue;
108
- withdrawn.push(...prompts.slice(0, -1));
109
- }
110
- if (withdrawn.length === 0) {
111
- return { records, abandoned: 0, withdrawnPrompts: 0, duplicates, compactBoundaries };
112
- }
113
- // A withdrawn prompt takes its subtree: the answer to a question that was
114
- // never asked is not conversation either.
115
- const dropped = new Set();
116
- const stack = [...withdrawn];
117
- while (stack.length > 0) {
118
- const record = stack.pop();
119
- const uuid = stringField(record, 'uuid');
120
- if (dropped.has(uuid))
121
- continue;
122
- dropped.add(uuid);
123
- for (const child of childrenOf.get(uuid) ?? [])
124
- stack.push(child);
125
- }
126
- const resolved = records.filter((record) => {
127
- const uuid = stringField(record, 'uuid');
128
- return uuid === undefined || !dropped.has(uuid);
129
- });
153
+ const indexer = new TranscriptLineageIndexer();
154
+ for (const record of rawRecords)
155
+ indexer.accept(record);
156
+ const index = indexer.finish();
157
+ const filter = index.createFilter();
158
+ const resolved = rawRecords.filter((record) => filter.keep(record));
130
159
  return {
131
160
  records: resolved,
132
- abandoned: dropped.size,
133
- withdrawnPrompts: withdrawn.length,
134
- duplicates,
135
- compactBoundaries,
161
+ abandoned: index.abandoned,
162
+ withdrawnPrompts: index.withdrawnPrompts,
163
+ duplicates: index.duplicates,
164
+ compactBoundaries: index.compactBoundaries,
136
165
  };
137
166
  }
@@ -232,7 +232,7 @@ export function invocationOpeningFromLegacyRunHeader(header) {
232
232
  };
233
233
  }
234
234
  function invocationRouteFromLegacyRunHeader(header) {
235
- if (header.llmConnectionId === undefined) {
235
+ if (header.llmConnectionId === undefined || header.backendKind === 'plugin-executor') {
236
236
  return {
237
237
  provenance: 'unknown',
238
238
  backendKind: header.backendKind,
@@ -320,7 +320,7 @@ function isLegacyContinuationSource(value) {
320
320
  }
321
321
  /** `'fake'` stays accepted: runs written by builds that shipped FakeBackend must keep decoding (#3211). */
322
322
  function isPersistedBackendKind(value) {
323
- return value === 'ai-sdk' || value === 'fake';
323
+ return value === 'ai-sdk' || value === 'plugin-executor' || value === 'fake';
324
324
  }
325
325
  function isSha256Digest(value) {
326
326
  return typeof value === 'string' && /^sha256:[0-9a-f]{64}$/.test(value);
@@ -34,7 +34,7 @@ export const CACHE_READ_TOKENS = `
34
34
  * when the price was simply never resolved.
35
35
  */
36
36
  export const PRICED_COST = `CASE WHEN cost_basis = 'priced' THEN COALESCE(cost_usd, 0) ELSE 0 END`;
37
- /** Mirrors `usageStatusForAttempt`: only a provider failure is an error. */
37
+ /** Only a provider failure is an error. */
38
38
  const ERROR_ROW = `status = 'failed'`;
39
39
  export const TOKEN_SUMS = `
40
40
  SUM(COALESCE(input_tokens, 0)) AS input,
@@ -22,6 +22,7 @@ import { createSqliteSessionMetadataStore, SessionMetadataVersionConflictError,
22
22
  import { isDiscardableConversationCopy } from './session-conversation-copy.js';
23
23
  import { acquireOperationalStateDatabase, OPERATIONAL_STATE_DATABASE_NAME, } from './operational-state-store.js';
24
24
  import { DEFAULT_SESSION_NAME, normalizeUserSessionName } from '@maka/core/session-name';
25
+ import { isExecutorId } from '@maka/core/executor-id';
25
26
  import { decodeCanonicalMessage, deriveTurnRecords, isSessionBlockedReason, isSessionConversationCopy, isSubagentSessionParent, isSubagentSessionRuntime, isSubagentSessionSpawn, isSessionStatus, isWorkHubCoordinationSessionId, subagentSessionRuntimeSummary, WORKHUB_COORDINATION_SESSION_ID, WORKHUB_COORDINATION_SESSION_ROLE, } from '@maka/core/session';
26
27
  import { isCollaborationMode } from '@maka/core/collaboration';
27
28
  import { isOrchestrationMode } from '@maka/core/orchestration';
@@ -609,7 +610,8 @@ function buildSessionHeader(workspaceRoot, input, sessionId = randomUUID(), conv
609
610
  ...(input.revisionIndex !== undefined ? { revisionIndex: input.revisionIndex } : {}),
610
611
  ...(input.revisionState ? { revisionState: input.revisionState } : {}),
611
612
  hasUnread: false,
612
- backend: 'ai-sdk',
613
+ backend: input.executorId ? 'plugin-executor' : 'ai-sdk',
614
+ ...(input.executorId ? { executorId: input.executorId } : {}),
613
615
  ...(input.llmConnectionId === undefined ? {} : { llmConnectionId: input.llmConnectionId }),
614
616
  llmConnectionSlug: input.llmConnectionSlug,
615
617
  // A subagent Session's route is chosen by the spawn that created it and is
@@ -669,6 +671,7 @@ export function normalizeSessionHeader(header, sessionId = header.id) {
669
671
  (header.lastReadMessageId === undefined || typeof header.lastReadMessageId === 'string') &&
670
672
  typeof header.hasUnread === 'boolean' &&
671
673
  isPersistedBackendKind(header.backend) &&
674
+ isValidExecutorSelection(header) &&
672
675
  (header.llmConnectionId === undefined ||
673
676
  (typeof header.llmConnectionId === 'string' && header.llmConnectionId.length > 0)) &&
674
677
  typeof header.llmConnectionSlug === 'string' &&
@@ -805,7 +808,13 @@ function isValidSubagentSessionLineage(header) {
805
808
  * FakeBackend fail `normalizeSessionHeader` and read back as malformed (#3211).
806
809
  */
807
810
  function isPersistedBackendKind(value) {
808
- return value === 'ai-sdk' || value === 'fake';
811
+ return value === 'ai-sdk' || value === 'plugin-executor' || value === 'fake';
812
+ }
813
+ function isValidExecutorSelection(header) {
814
+ if (header.backend === 'plugin-executor') {
815
+ return isExecutorId(header.executorId);
816
+ }
817
+ return header.executorId === undefined;
809
818
  }
810
819
  function isFiniteNumber(value) {
811
820
  return typeof value === 'number' && Number.isFinite(value);
@@ -871,6 +880,7 @@ function toSummary(header) {
871
880
  ...(header.revisionIndex !== undefined ? { revisionIndex: header.revisionIndex } : {}),
872
881
  ...(header.revisionState ? { revisionState: header.revisionState } : {}),
873
882
  backend: header.backend,
883
+ ...(header.executorId ? { executorId: header.executorId } : {}),
874
884
  ...(header.llmConnectionId === undefined ? {} : { llmConnectionId: header.llmConnectionId }),
875
885
  llmConnectionSlug: header.llmConnectionSlug,
876
886
  connectionLocked: header.connectionLocked,
@@ -555,14 +555,12 @@ export class SqliteMemoryItemStore {
555
555
  });
556
556
  return this.#readSnapshot(() => {
557
557
  const rows = this.#database.prepare(query.sql).all(...query.parameters);
558
- return rows.map((row) => {
558
+ const itemIds = rows.map((row) => {
559
559
  if (typeof row.item_id !== 'string')
560
560
  throw new Error('Invalid Memory Item search result');
561
- const record = this.#readItemRecord(row.item_id);
562
- if (!record)
563
- throw new Error(`Memory Item ${row.item_id} disappeared during read`);
564
- return record;
561
+ return row.item_id;
565
562
  });
563
+ return this.#readItemRecords(itemIds);
566
564
  });
567
565
  }
568
566
  async readOperation(operationId) {
@@ -685,13 +683,55 @@ export class SqliteMemoryItemStore {
685
683
  FROM memory_item_sources WHERE item_id = ? ORDER BY event_id ASC
686
684
  LIMIT ${MAX_SOURCES_PER_ITEM + 1}`)
687
685
  .all(itemId);
688
- assertChildCardinality('keys', keys.length, MAX_KEYS_PER_ITEM);
689
- assertChildCardinality('sources', sources.length, MAX_SOURCES_PER_ITEM);
690
- return {
691
- item: decodeItem(row),
692
- keys: keys.map(decodeKey),
693
- sources: sources.map(decodeSource),
694
- };
686
+ return decodeItemRecord(row, keys, sources);
687
+ }
688
+ #readItemRecords(itemIds) {
689
+ if (itemIds.length === 0)
690
+ return [];
691
+ if (itemIds.length === 1) {
692
+ const record = this.#readItemRecord(itemIds[0]);
693
+ if (!record)
694
+ throw new Error(`Memory Item ${itemIds[0]} disappeared during read`);
695
+ return [record];
696
+ }
697
+ const rows = this.#database
698
+ .prepare(`SELECT * FROM memory_items WHERE item_id IN (${placeholders(itemIds.length)})`)
699
+ .all(...itemIds);
700
+ const items = new Map(rows.map((row) => [row.item_id, row]));
701
+ const keys = this.#readItemChildren('keys', itemIds, ([, key_text, normalized_key, key_type, key_origin]) => ({
702
+ key_text,
703
+ normalized_key,
704
+ key_type,
705
+ key_origin,
706
+ }));
707
+ const sources = this.#readItemChildren('sources', itemIds, ([, session_id, run_id, turn_id, event_id]) => ({ session_id, run_id, turn_id, event_id }));
708
+ // IN queries need not return search order; preserve the ranked ID sequence.
709
+ return itemIds.map((itemId) => {
710
+ const row = items.get(itemId);
711
+ if (!row)
712
+ throw new Error(`Memory Item ${itemId} disappeared during read`);
713
+ return decodeItemRecord(row, keys.get(itemId) ?? [], sources.get(itemId) ?? []);
714
+ });
715
+ }
716
+ #readItemChildren(child, itemIds, toRow) {
717
+ const statement = this.#database.prepare(buildMemoryChildBatchQuery(child, itemIds.length));
718
+ // Native array results avoid SQLite's per-column object property construction
719
+ // for as many as 25,700 sources. Column validation still happens in search order.
720
+ statement.setReturnArrays(true);
721
+ const rows = statement.all(...itemIds);
722
+ const grouped = new Map();
723
+ let previousItemId;
724
+ let children;
725
+ for (const row of rows) {
726
+ const itemId = row[0];
727
+ if (!children || itemId !== previousItemId) {
728
+ children = [];
729
+ grouped.set(itemId, children);
730
+ previousItemId = itemId;
731
+ }
732
+ children.push(toRow(row));
733
+ }
734
+ return grouped;
695
735
  }
696
736
  #requireItemRecord(itemId) {
697
737
  const record = this.#readItemRecord(itemId);
@@ -796,6 +836,31 @@ export function buildSqliteMemoryKeySearchQuery(input) {
796
836
  parameters,
797
837
  };
798
838
  }
839
+ function buildMemoryChildBatchQuery(child, itemCount) {
840
+ const table = child === 'keys' ? 'memory_item_keys' : 'memory_item_sources';
841
+ const orderColumn = child === 'keys' ? 'normalized_key' : 'event_id';
842
+ const columns = child === 'keys'
843
+ ? 'c.key_text, c.normalized_key, c.key_type, c.key_origin'
844
+ : 'c.session_id, c.run_id, c.turn_id, c.event_id';
845
+ const maximum = child === 'keys' ? MAX_KEYS_PER_ITEM : MAX_SOURCES_PER_ITEM;
846
+ // Each composite primary key supplies at most maximum + 1 children per Item,
847
+ // including the overflow sentinel used by cardinality validation. A missing
848
+ // sentinel falls back to the last child, keeping corrupt child sets bounded.
849
+ // Fix Items as the outer loop so both indexes supply order without a temp sort.
850
+ return `
851
+ SELECT i.item_id, ${columns}
852
+ FROM memory_items i
853
+ CROSS JOIN ${table} c
854
+ ON c.item_id = i.item_id
855
+ AND c.${orderColumn} <= COALESCE(
856
+ (SELECT ${orderColumn} FROM ${table}
857
+ WHERE item_id = i.item_id ORDER BY ${orderColumn} ASC LIMIT 1 OFFSET ${maximum}),
858
+ (SELECT ${orderColumn} FROM ${table}
859
+ WHERE item_id = i.item_id ORDER BY ${orderColumn} DESC LIMIT 1)
860
+ )
861
+ WHERE i.item_id IN (${placeholders(itemCount)})
862
+ ORDER BY i.item_id ASC, c.${orderColumn} ASC`;
863
+ }
799
864
  function normalizeMutations(mutations) {
800
865
  if (!Array.isArray(mutations) || mutations.length === 0) {
801
866
  throw new Error('Memory operation requires at least one mutation');
@@ -1108,6 +1173,15 @@ function mutationResult(mutationIndex, mutationType, item, outcome) {
1108
1173
  outcome,
1109
1174
  };
1110
1175
  }
1176
+ function decodeItemRecord(row, keys, sources) {
1177
+ assertChildCardinality('keys', keys.length, MAX_KEYS_PER_ITEM);
1178
+ assertChildCardinality('sources', sources.length, MAX_SOURCES_PER_ITEM);
1179
+ return {
1180
+ item: decodeItem(row),
1181
+ keys: keys.map(decodeKey),
1182
+ sources: sources.map(decodeSource),
1183
+ };
1184
+ }
1111
1185
  function decodeItem(row) {
1112
1186
  const kind = requiredString(row.kind, 'kind');
1113
1187
  const statementType = requiredString(row.statement_type, 'statement_type');
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import { randomUUID } from 'node:crypto';
20
20
  import { resolve } from 'node:path';
21
- import { WORK_BOARD_DEFAULT_PAGE_SIZE, applyWorkBoardItemPatch, archiveWorkBoardItem, decodeWorkBoardItem, isSafeWorkBoardId, normalizeCreateWorkBoardItemInput, normalizeUpdateWorkBoardItemInput, normalizeWorkBoardListQuery, unarchiveWorkBoardItem, } from '@maka/core/work-board';
21
+ import { WORK_BOARD_DEFAULT_PAGE_SIZE, applyWorkBoardItemPatch, archiveWorkBoardItem, decodeWorkBoardItem, isSafeWorkBoardId, normalizeCreateWorkBoardItemInput, normalizeWorkBoardLinkedSession, normalizeUpdateWorkBoardItemInput, normalizeWorkBoardListQuery, unarchiveWorkBoardItem, } from '@maka/core/work-board';
22
22
  import { buildWorkBoardListStatement, encodeWorkBoardCursor, workBoardFilterFingerprint, } from './work-board-list-query.js';
23
23
  import { acquireOperationalStateDatabase, } from './operational-state-store.js';
24
24
  import { chainWrite } from './write-queue.js';
@@ -78,6 +78,7 @@ class SqliteWorkBoardStore {
78
78
  archived: false,
79
79
  creator: value.creator,
80
80
  provenance: value.provenance,
81
+ linkedSessions: [],
81
82
  createdAt: now,
82
83
  updatedAt: now,
83
84
  };
@@ -172,6 +173,41 @@ class SqliteWorkBoardStore {
172
173
  });
173
174
  });
174
175
  }
176
+ async linkSession(id, link, options = {}, now = Date.now()) {
177
+ const normalized = normalizeWorkBoardLinkedSession(link);
178
+ if (!normalized.ok)
179
+ throw storeError('invalid_input', normalized.message);
180
+ const expectedRevision = normalizeExpectedRevision(options);
181
+ assertValidNow(now);
182
+ let result;
183
+ await chainWrite(this.writeQueues, WORK_BOARD_WRITE_KEY, async () => {
184
+ this.#lease.transaction('write', () => {
185
+ const current = this.#requireItem(id);
186
+ assertExpectedRevision(expectedRevision, current);
187
+ const key = linkedSessionKey(normalized.value);
188
+ const linkedSessions = current.linkedSessions ?? [];
189
+ if (linkedSessions.some((entry) => linkedSessionKey(entry) === key)) {
190
+ result = current;
191
+ return;
192
+ }
193
+ const effectiveNow = Math.max(now, current.updatedAt);
194
+ // Single-link contract ({@link WORK_BOARD_MAX_LINKED_SESSIONS}): a
195
+ // board item owns at most one started Session at a time, so a freshly
196
+ // started Session replaces any previous link. This keeps
197
+ // `linkedSessions` bounded instead of growing without limit on
198
+ // repeated starts.
199
+ const next = {
200
+ ...current,
201
+ linkedSessions: [normalized.value],
202
+ updatedAt: effectiveNow,
203
+ revision: current.revision + 1,
204
+ };
205
+ this.#writeItem(next);
206
+ result = next;
207
+ });
208
+ });
209
+ return result;
210
+ }
175
211
  #readRow(id) {
176
212
  if (!isSafeWorkBoardId(id)) {
177
213
  throw storeError('invalid_input', 'Work Board item id is invalid');
@@ -261,3 +297,6 @@ function assertExpectedRevision(expected, item) {
261
297
  function storeError(code, message) {
262
298
  return new WorkBoardStoreError(code, message);
263
299
  }
300
+ function linkedSessionKey(link) {
301
+ return `${link.profileId}\u0000${link.hostId}\u0000${link.sessionId}`;
302
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maka-agent",
3
- "version": "0.2.0-dev.31.20260913",
3
+ "version": "0.2.0-dev.32.20260914",
4
4
  "description": "Apache Maka (Incubating) developer snapshot; not an Apache release.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",