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.
- package/dist/pi-transcript.js +3 -4
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/agent-graph-schedule.js +15 -4
- package/node_modules/@maka/core/dist/executor-id.js +22 -0
- package/node_modules/@maka/core/dist/external-session.js +18 -0
- package/node_modules/@maka/core/dist/model-call-usage-projection.js +0 -13
- package/node_modules/@maka/core/dist/runtime-event.js +19 -3
- package/node_modules/@maka/core/dist/session-send-projection.js +6 -3
- package/node_modules/@maka/core/dist/session.js +8 -2
- package/node_modules/@maka/core/dist/shell-run-result.js +1 -0
- package/node_modules/@maka/core/dist/shell-run.js +4 -0
- package/node_modules/@maka/core/dist/work-board.js +94 -1
- package/node_modules/@maka/core/package.json +1 -0
- package/node_modules/@maka/eval/dist/fleet-simulation.js +270 -0
- package/node_modules/@maka/eval/dist/fleet-store.js +243 -0
- package/node_modules/@maka/eval/dist/fleet-worker.js +125 -0
- package/node_modules/@maka/eval/dist/fleet.js +329 -0
- package/node_modules/@maka/eval/dist/index.js +3 -0
- package/node_modules/@maka/runtime/dist/agent-run.js +2 -17
- package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +6 -2
- package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +36 -24
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +257 -330
- package/node_modules/@maka/runtime/dist/background-task-health-tool.js +104 -0
- package/node_modules/@maka/runtime/dist/history-compaction.js +3 -1
- package/node_modules/@maka/runtime/dist/local-web-fetch.js +1 -1
- package/node_modules/@maka/runtime/dist/model-adapter.js +62 -53
- package/node_modules/@maka/runtime/dist/model-history.js +1 -5
- package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +311 -0
- package/node_modules/@maka/runtime/dist/plugin-executor-service.js +344 -0
- package/node_modules/@maka/runtime/dist/provider-error-classification.js +7 -1
- package/node_modules/@maka/runtime/dist/provider-request-telemetry.js +35 -8
- package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +7 -1
- package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +1 -0
- package/node_modules/@maka/runtime/dist/runtime-invocation-route.js +49 -0
- package/node_modules/@maka/runtime/dist/runtime-kernel.js +72 -150
- package/node_modules/@maka/runtime/dist/runtime-read-model.js +0 -2
- package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -0
- package/node_modules/@maka/runtime/dist/session-manager.js +100 -58
- package/node_modules/@maka/runtime/dist/shell-run-manager.js +9 -3
- package/node_modules/@maka/runtime/dist/shell-run-tool-result.js +1 -0
- package/node_modules/@maka/runtime/dist/stream-graph-schedule-reconcile.js +1 -0
- package/node_modules/@maka/runtime/dist/stream-graph-supervisor-tools.js +26 -4
- package/node_modules/@maka/runtime/dist/subagent-tools.js +7 -0
- package/node_modules/@maka/runtime/dist/tool-runtime.js +45 -466
- package/node_modules/@maka/runtime/package.json +3 -0
- package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +7 -2
- package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +24 -3
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +4 -1
- package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +42 -5
- package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +31 -3
- package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +6 -0
- package/node_modules/@maka/runtime-host/dist/server/child-agent-composition.js +1 -0
- package/node_modules/@maka/runtime-host/dist/server/execution-artifacts.js +1 -22
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +54 -8
- package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +14 -1
- package/node_modules/@maka/runtime-host/dist/server/host-session-availability.js +10 -2
- package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +6 -0
- package/node_modules/@maka/runtime-host/dist/server/plugin-platform.js +6 -0
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +56 -16
- package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +4 -3
- package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +4 -0
- package/node_modules/@maka/runtime-host/dist/server/web-fetch-tool.js +37 -1
- package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +450 -234
- package/node_modules/@maka/storage/dist/claude-code-transcript-lineage.js +106 -77
- package/node_modules/@maka/storage/dist/legacy-run-header.js +2 -2
- package/node_modules/@maka/storage/dist/model-call-usage-sql.js +1 -1
- package/node_modules/@maka/storage/dist/session-store.js +12 -2
- package/node_modules/@maka/storage/dist/sqlite-long-term-memory-store.js +86 -12
- package/node_modules/@maka/storage/dist/work-board-store.js +40 -1
- 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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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:
|
|
133
|
-
withdrawnPrompts:
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
+
}
|