maka-agent 0.2.0-dev.22.20260905 → 0.2.0-dev.24.20260907
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/acp/maka-acp-agent.js +2 -1
- package/dist/acp/session-configuration.js +143 -0
- package/dist/acp/session-registry.js +90 -4
- package/dist/cli-core.js +2 -0
- package/dist/pi-transcript.js +5 -12
- package/dist/pi-tui-runner.js +74 -4
- package/dist/runtime-host-cli-context.js +167 -78
- package/dist/runtime-host-cli-installation.js +8 -2
- package/dist/runtime-host-cli.js +31 -1
- package/dist/runtime-host-handoff-surface.js +81 -0
- package/dist/runtime-host-installed-update-bootstrap.js +13 -0
- package/dist/runtime-host-installed-update-coordinator.js +33 -0
- package/dist/runtime-host-lifecycle-transaction.js +9 -1
- package/dist/runtime-host-local-handoff.js +37 -4
- package/dist/runtime-host-local-managed-activation.js +84 -0
- package/dist/runtime-host-local-source-retirement.js +18 -2
- package/dist/runtime-host-managed-lifecycle-manager.js +1 -0
- package/dist/runtime-host-operator-retirement.js +50 -0
- package/dist/runtime-host-service-manager.js +3 -0
- package/dist/runtime-host-session-driver.js +4 -25
- package/dist/runtime-host-session-update.js +62 -0
- package/dist/runtime-host-tui-command.js +38 -73
- package/dist/runtime-host-tui-context.js +1 -0
- package/dist/runtime-host-tui-owner.js +226 -0
- package/dist/tui-copy-catalog.js +140 -3
- package/dist/tui-host-owner-copy.js +24 -0
- package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/bot-events.js +18 -63
- package/node_modules/@maka/core/dist/canonical-runtime-event.js +3 -1
- package/node_modules/@maka/core/dist/model-call-attempt.js +0 -82
- package/node_modules/@maka/core/dist/model-call-usage-projection.js +0 -186
- package/node_modules/@maka/core/dist/model-failure.js +40 -0
- package/node_modules/@maka/core/dist/model-metadata.generated.js +4 -4
- package/node_modules/@maka/core/dist/model-metadata.js +3 -3
- package/node_modules/@maka/core/dist/model-web-search.js +5 -4
- package/node_modules/@maka/core/dist/provider-registry.js +45 -4
- package/node_modules/@maka/core/dist/record-schema.js +22 -2
- package/node_modules/@maka/core/dist/redaction.js +41 -58
- package/node_modules/@maka/core/dist/runtime-boundary.js +31 -11
- package/node_modules/@maka/core/dist/runtime-event.js +37 -6
- package/node_modules/@maka/core/dist/runtime-handoff.js +98 -0
- package/node_modules/@maka/core/dist/runtime-invocation.js +1 -1
- package/node_modules/@maka/core/dist/runtime-logical-execution.js +112 -0
- package/node_modules/@maka/core/dist/session-trace.js +5 -1
- package/node_modules/@maka/core/dist/session.js +54 -52
- package/node_modules/@maka/core/dist/slash-command-catalog.js +2 -0
- package/node_modules/@maka/core/dist/tool-result-archive-evidence.js +20 -0
- package/node_modules/@maka/core/dist/usage-ledger-merge.js +7 -9
- package/node_modules/@maka/core/dist/usage-record-schema.js +26 -35
- package/node_modules/@maka/core/dist/workhub-creation-intent.js +34 -20
- package/node_modules/@maka/core/package.json +4 -0
- package/node_modules/@maka/eval/harbor/egress-proxy/entrypoint.sh +2 -1
- package/node_modules/@maka/mcp/dist/index.js +131 -1
- package/node_modules/@maka/runtime/dist/agent-run.js +272 -105
- package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +13 -5
- package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +109 -34
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +155 -134
- package/node_modules/@maka/runtime/dist/bots/base-adapter.js +17 -0
- package/node_modules/@maka/runtime/dist/bots/bot-test.js +38 -22
- package/node_modules/@maka/runtime/dist/bots/dingtalk-bridge.js +7 -5
- package/node_modules/@maka/runtime/dist/bots/discord-bridge.js +4 -4
- package/node_modules/@maka/runtime/dist/bots/feishu-bridge.js +4 -5
- package/node_modules/@maka/runtime/dist/bots/qq-bridge.js +6 -4
- package/node_modules/@maka/runtime/dist/bots/slack-bridge.js +3 -4
- package/node_modules/@maka/runtime/dist/bots/telegram-bridge.js +4 -5
- package/node_modules/@maka/runtime/dist/bots/wechat-bridge.js +17 -19
- package/node_modules/@maka/runtime/dist/bots/wecom-bridge.js +5 -6
- package/node_modules/@maka/runtime/dist/bots/ws-bridge-base.js +2 -3
- package/node_modules/@maka/runtime/dist/context-budget.js +14 -9
- package/node_modules/@maka/runtime/dist/conversation-copy.js +5 -4
- package/node_modules/@maka/runtime/dist/goal-continuation.js +36 -4
- package/node_modules/@maka/runtime/dist/history-compact-checkpoint.js +18 -9
- package/node_modules/@maka/runtime/dist/history-compact-summarizer.js +30 -22
- package/node_modules/@maka/runtime/dist/history-compaction.js +29 -4
- package/node_modules/@maka/runtime/dist/ledger-tool-result-archive-reader.js +106 -0
- package/node_modules/@maka/runtime/dist/message-authority.js +9 -0
- package/node_modules/@maka/runtime/dist/model-adapter.js +19 -98
- package/node_modules/@maka/runtime/dist/model-factory.js +19 -81
- package/node_modules/@maka/runtime/dist/model-fetcher.js +7 -0
- package/node_modules/@maka/runtime/dist/model-runtime.js +85 -111
- package/node_modules/@maka/runtime/dist/native-web-search-tool.js +2 -1
- package/node_modules/@maka/runtime/dist/openai-codex-history-compactor.js +2 -2
- package/node_modules/@maka/runtime/dist/opencode-session-header.js +4 -2
- package/node_modules/@maka/runtime/dist/provider-error-classification.js +84 -123
- package/node_modules/@maka/runtime/dist/provider-request-telemetry.js +68 -108
- package/node_modules/@maka/runtime/dist/run-handoff-gate.js +97 -0
- package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +82 -54
- package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +117 -159
- package/node_modules/@maka/runtime/dist/runtime-kernel.js +150 -112
- package/node_modules/@maka/runtime/dist/runtime-ledger-repair.js +160 -79
- package/node_modules/@maka/runtime/dist/runtime-read-model.js +30 -89
- package/node_modules/@maka/runtime/dist/runtime-resume.js +80 -4
- package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +5 -0
- package/node_modules/@maka/runtime/dist/session-manager.js +171 -288
- package/node_modules/@maka/runtime/dist/session-projection-helpers.js +0 -27
- package/node_modules/@maka/runtime/dist/session-trace-projection.js +3 -30
- package/node_modules/@maka/runtime/dist/subscription-model-fetch.js +3 -4
- package/node_modules/@maka/runtime/dist/test-connection.js +16 -16
- package/node_modules/@maka/runtime/dist/tool-result-archive-encoding.js +38 -0
- package/node_modules/@maka/runtime/dist/tool-runtime.js +3 -61
- package/node_modules/@maka/runtime/package.json +1 -0
- package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +35 -0
- package/node_modules/@maka/runtime-host/dist/candidate-entry.js +4 -1
- package/node_modules/@maka/runtime-host/dist/client/connect-or-spawn.js +9 -1
- package/node_modules/@maka/runtime-host/dist/client/connection.js +3 -0
- package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +156 -0
- package/node_modules/@maka/runtime-host/dist/client/host-handoff.js +273 -0
- package/node_modules/@maka/runtime-host/dist/client/host-retirement.js +26 -5
- package/node_modules/@maka/runtime-host/dist/client/index.js +3 -1
- package/node_modules/@maka/runtime-host/dist/client/launcher.js +3 -0
- package/node_modules/@maka/runtime-host/dist/client/peer-client.js +13 -0
- package/node_modules/@maka/runtime-host/dist/client/registered-host-termination.js +0 -59
- package/node_modules/@maka/runtime-host/dist/client/remote-compatibility-error.js +2 -0
- package/node_modules/@maka/runtime-host/dist/client/session-subscription.js +27 -5
- package/node_modules/@maka/runtime-host/dist/operator/index.js +2 -1
- package/node_modules/@maka/runtime-host/dist/operator/retirement-cancellation.js +20 -0
- package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +28 -16
- package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +4 -0
- package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +29 -12
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +37 -1
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +2 -0
- package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +28 -1
- package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +40 -20
- package/node_modules/@maka/runtime-host/dist/protocol/session-transcript.js +0 -5
- package/node_modules/@maka/runtime-host/dist/protocol/session-turns.js +29 -58
- package/node_modules/@maka/runtime-host/dist/protocol/turn.js +11 -1
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +33 -0
- package/node_modules/@maka/runtime-host/dist/server/access-authority.js +26 -1
- package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +6 -1
- package/node_modules/@maka/runtime-host/dist/server/canonical-turn-snapshot.js +19 -4
- package/node_modules/@maka/runtime-host/dist/server/canonical-usage-reader.js +24 -11
- package/node_modules/@maka/runtime-host/dist/server/connection-session.js +7 -6
- package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +7 -4
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +168 -28
- package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +5 -5
- package/node_modules/@maka/runtime-host/dist/server/execution-model-composition.js +3 -2
- package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +6 -3
- package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +40 -1
- package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +108 -23
- package/node_modules/@maka/runtime-host/dist/server/host-residency-registry.js +13 -7
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-admission-registry.js +3 -1
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +124 -62
- package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +85 -8
- package/node_modules/@maka/runtime-host/dist/server/root-admission-owner.js +7 -0
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +344 -71
- package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +37 -2
- package/node_modules/@maka/runtime-host/dist/server/serial-outbound-writer.js +45 -3
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +201 -12
- package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +87 -25
- package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +8 -55
- package/node_modules/@maka/runtime-host/dist/server/session-sidecar-purge.js +1 -16
- package/node_modules/@maka/runtime-host/dist/server/session-transcript-pager.js +0 -1
- package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +460 -70
- package/node_modules/@maka/runtime-host/dist/server/shared-session-transcript.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/storage-maintenance.js +109 -0
- package/node_modules/@maka/runtime-host/dist/server/usage-pricing-coordinator.js +4 -11
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +44 -15
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +6 -2
- package/node_modules/@maka/storage/dist/agent-run-store.js +44 -0
- package/node_modules/@maka/storage/dist/artifact-store.js +21 -10
- package/node_modules/@maka/storage/dist/artifact-stores.js +1 -1
- package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +0 -4
- package/node_modules/@maka/storage/dist/codex-session-adapter.js +0 -2
- package/node_modules/@maka/storage/dist/context-offload-snapshot.js +325 -0
- package/node_modules/@maka/storage/dist/conversation-operational-state.js +4 -0
- package/node_modules/@maka/storage/dist/execution-stores.js +11 -7
- package/node_modules/@maka/storage/dist/file-session-repository.js +1012 -0
- package/node_modules/@maka/storage/dist/model-call-ledger.js +199 -32
- package/node_modules/@maka/storage/dist/model-call-usage-sql.js +136 -0
- package/node_modules/@maka/storage/dist/opencode-session-adapter.js +0 -4
- package/node_modules/@maka/storage/dist/operational-state-backup.js +9 -5
- package/node_modules/@maka/storage/dist/operational-state-store-public.js +1 -1
- package/node_modules/@maka/storage/dist/operational-state-store.js +5 -3
- package/node_modules/@maka/storage/dist/runtime-event-persistence.js +1 -0
- package/node_modules/@maka/storage/dist/runtime-transcript-query.js +308 -0
- package/node_modules/@maka/storage/dist/session-bundle-policy.js +24 -4
- package/node_modules/@maka/storage/dist/session-message-projection.js +15 -0
- package/node_modules/@maka/storage/dist/session-repository.js +767 -0
- package/node_modules/@maka/storage/dist/session-store.js +25 -99
- package/node_modules/@maka/storage/dist/sqlite-artifact-metadata.js +10 -3
- package/node_modules/@maka/storage/dist/sqlite-context-offload-store.js +24 -8
- package/node_modules/@maka/storage/dist/sqlite-core-execution-schema.js +17 -1
- package/node_modules/@maka/storage/dist/sqlite-runtime-schema.js +44 -1
- package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +129 -25
- package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +109 -892
- package/node_modules/@maka/storage/dist/sqlite-usage-schema.js +200 -20
- package/node_modules/@maka/storage/dist/tool-result-archive-evidence.js +111 -0
- package/node_modules/@maka/storage/dist/usage-stores.js +6 -2
- package/node_modules/@maka/storage/package.json +2 -0
- package/package.json +1 -1
|
@@ -36,15 +36,12 @@ import { messageContentDigest, messageContentsEqual, normalizeMessageContent, }
|
|
|
36
36
|
import { AGENT_GRAPH_SUPERVISOR_WAKE_SCHEMA_VERSION, } from '@maka/core/agent-graph-supervisor-wake';
|
|
37
37
|
import { assertSafeSessionId, decodePersistedSessionHeader, normalizeSessionHeader, SessionNotFoundError, } from './session-store.js';
|
|
38
38
|
import { isDiscardableConversationCopy, isValidConversationCopyTransition, } from './session-conversation-copy.js';
|
|
39
|
-
import {
|
|
39
|
+
import { projectSessionCatalogMessages } from './session-message-projection.js';
|
|
40
40
|
import { configureSqliteSessionMetadataDatabase, migrateSqliteSessionMetadataDatabase, readSqliteSessionMetadataSchemaVersion, SQLITE_AGENT_GRAPH_CONTROL_TABLES, SQLITE_SESSION_MESSAGE_CHUNK_BYTES, SQLITE_SESSION_MESSAGE_CHUNK_MARKER, } from './sqlite-session-metadata-schema.js';
|
|
41
41
|
import { buildSqliteSessionCatalogPageQuery, } from './sqlite-session-catalog-query.js';
|
|
42
42
|
import { sqliteOrdinarySessionRolePredicate, sqliteRecoverableSessionRolePredicate, } from './sqlite-session-role-scope.js';
|
|
43
43
|
export { SQLITE_SESSION_METADATA_SCHEMA_VERSION } from './sqlite-session-metadata-schema.js';
|
|
44
44
|
const SQLITE_TRANSCRIPT_MESSAGE_LOOKUP_BATCH_SIZE = 256;
|
|
45
|
-
const SQLITE_TURN_CONTRIBUTION_MAX_SOURCE_MESSAGES = 1_024;
|
|
46
|
-
const SQLITE_TURN_CONTRIBUTION_MAX_SOURCE_BYTES = 4 * 1024 * 1024;
|
|
47
|
-
const SQLITE_TURN_LANDMARK_LEGACY_NEIGHBOR_MESSAGES = 32;
|
|
48
45
|
// Each target Session binds three parameters in the linkage query. Stay well
|
|
49
46
|
// inside SQLite's bound-parameter limit.
|
|
50
47
|
const WORKHUB_TARGET_LINKAGE_MAX_SESSIONS = 256;
|
|
@@ -351,6 +348,32 @@ export class SqliteSessionMetadataStore {
|
|
|
351
348
|
return rows.map(decodeSandboxBoundaryRequestRow);
|
|
352
349
|
});
|
|
353
350
|
}
|
|
351
|
+
async hasExplicitSandboxBoundaryDenial(identities) {
|
|
352
|
+
this.assertOpen();
|
|
353
|
+
const query = this.db.prepare(`
|
|
354
|
+
SELECT
|
|
355
|
+
MAX(CASE WHEN outcome_reason = 'client_denied' THEN 1 ELSE 0 END) AS explicit,
|
|
356
|
+
MAX(CASE WHEN outcome_reason IN ('client_denied', 'turn_stopped', 'turn_terminal', 'host_restarted')
|
|
357
|
+
THEN 0 ELSE 1 END) AS ambiguous
|
|
358
|
+
FROM sandbox_boundary_log
|
|
359
|
+
WHERE session_id = ? AND run_id = ? AND turn_id = ?
|
|
360
|
+
AND entry_kind = 'expansion_request' AND status = 'denied'
|
|
361
|
+
`);
|
|
362
|
+
let denied = false;
|
|
363
|
+
for (const identity of identities) {
|
|
364
|
+
assertSafeSessionId(identity.sessionId);
|
|
365
|
+
assertSandboxBoundaryProvenanceId(identity.runId, 'run id');
|
|
366
|
+
assertSandboxBoundaryProvenanceId(identity.turnId, 'turn id');
|
|
367
|
+
const evidence = query.get(identity.sessionId, identity.runId, identity.turnId);
|
|
368
|
+
// Legacy NULL also meant internal cleanup. Do not invent a user decision
|
|
369
|
+
// or erase a possible denial; ambiguous provenance blocks this continuation.
|
|
370
|
+
if (evidence.ambiguous === 1) {
|
|
371
|
+
throw new Error('Historical sandbox denial cannot be attributed safely; start a new user Turn.');
|
|
372
|
+
}
|
|
373
|
+
denied ||= evidence.explicit === 1;
|
|
374
|
+
}
|
|
375
|
+
return denied;
|
|
376
|
+
}
|
|
354
377
|
async settleSandboxBoundaryRequest(input) {
|
|
355
378
|
this.assertOpen();
|
|
356
379
|
assertSafeSessionId(input.sessionId);
|
|
@@ -378,7 +401,7 @@ export class SqliteSessionMetadataStore {
|
|
|
378
401
|
sessionId: input.sessionId,
|
|
379
402
|
requestId: input.requestId,
|
|
380
403
|
status: 'denied',
|
|
381
|
-
|
|
404
|
+
outcomeReason: input.closureReason ?? 'client_denied',
|
|
382
405
|
settledAt,
|
|
383
406
|
});
|
|
384
407
|
return {
|
|
@@ -908,6 +931,10 @@ export class SqliteSessionMetadataStore {
|
|
|
908
931
|
.run(sessionId);
|
|
909
932
|
});
|
|
910
933
|
}
|
|
934
|
+
/**
|
|
935
|
+
* Session records in catalog order, each carrying the projection the Session
|
|
936
|
+
* list shows.
|
|
937
|
+
*/
|
|
911
938
|
async list(filter, roleScope) {
|
|
912
939
|
this.assertOpen();
|
|
913
940
|
const { where, parameters } = buildSessionListPredicate(filter ?? {});
|
|
@@ -923,17 +950,21 @@ export class SqliteSessionMetadataStore {
|
|
|
923
950
|
}
|
|
924
951
|
const rows = this.db
|
|
925
952
|
.prepare(`
|
|
926
|
-
SELECT
|
|
953
|
+
SELECT
|
|
954
|
+
metadata.session_id,
|
|
955
|
+
metadata.payload_json,
|
|
956
|
+
metadata.metadata_version,
|
|
957
|
+
metadata.committed_at,
|
|
958
|
+
COALESCE(projection.activity_at, 0) AS activity_at,
|
|
959
|
+
projection.last_message_preview
|
|
927
960
|
FROM session_metadata metadata
|
|
961
|
+
LEFT JOIN session_catalog_projection projection
|
|
962
|
+
ON projection.session_id = metadata.session_id
|
|
928
963
|
${where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''}
|
|
929
|
-
ORDER BY
|
|
930
|
-
SELECT activity_at
|
|
931
|
-
FROM session_catalog_projection projection
|
|
932
|
-
WHERE projection.session_id = metadata.session_id
|
|
933
|
-
) DESC, session_id ASC
|
|
964
|
+
ORDER BY activity_at DESC, metadata.session_id ASC
|
|
934
965
|
`)
|
|
935
966
|
.all(...parameters);
|
|
936
|
-
return rows.map(
|
|
967
|
+
return rows.map(decodeCatalogRecord);
|
|
937
968
|
}
|
|
938
969
|
async listCatalogPage(filter, cursor, limit) {
|
|
939
970
|
this.assertOpen();
|
|
@@ -1495,11 +1526,12 @@ export class SqliteSessionMetadataStore {
|
|
|
1495
1526
|
return [];
|
|
1496
1527
|
return this.readTransaction(() => {
|
|
1497
1528
|
const list = targets.map(() => '?').join(', ');
|
|
1498
|
-
// One Message moves between these lifecycle tables
|
|
1499
|
-
//
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
// durable
|
|
1529
|
+
// One Message moves between these lifecycle tables — pending, admitted
|
|
1530
|
+
// into a Turn, cancelled. Combine every target's identities once, then
|
|
1531
|
+
// resolve activity from the canonical Coordination ledger in this same
|
|
1532
|
+
// read transaction. That avoids rebuilding the target set once per page
|
|
1533
|
+
// or once per candidate, without introducing another durable
|
|
1534
|
+
// representation.
|
|
1503
1535
|
const rows = this.db
|
|
1504
1536
|
.prepare(`
|
|
1505
1537
|
WITH target_messages(session_id, message_id) AS (
|
|
@@ -1510,7 +1542,7 @@ export class SqliteSessionMetadataStore {
|
|
|
1510
1542
|
AND length(message_id) = 52
|
|
1511
1543
|
UNION
|
|
1512
1544
|
SELECT session_id, message_id
|
|
1513
|
-
FROM
|
|
1545
|
+
FROM core_root_source_message_proofs
|
|
1514
1546
|
WHERE session_id IN (${list})
|
|
1515
1547
|
AND message_id GLOB 'whm_*'
|
|
1516
1548
|
AND length(message_id) = 52
|
|
@@ -1613,19 +1645,6 @@ export class SqliteSessionMetadataStore {
|
|
|
1613
1645
|
provenSteeringMessages.set(normalized.messageId, normalized);
|
|
1614
1646
|
}
|
|
1615
1647
|
this.transaction(() => {
|
|
1616
|
-
const lastSequenceRow = this.db
|
|
1617
|
-
.prepare('SELECT COALESCE(MAX(sequence), -1) AS last_sequence FROM session_messages WHERE session_id = ?')
|
|
1618
|
-
.get(input.sessionId);
|
|
1619
|
-
if (typeof lastSequenceRow.last_sequence !== 'number' ||
|
|
1620
|
-
!Number.isSafeInteger(lastSequenceRow.last_sequence) ||
|
|
1621
|
-
lastSequenceRow.last_sequence < -1) {
|
|
1622
|
-
throw new SessionMetadataConflictError('Invalid Session message sequence');
|
|
1623
|
-
}
|
|
1624
|
-
const lastSequence = lastSequenceRow.last_sequence;
|
|
1625
|
-
const historicalMissingMessages = new Map();
|
|
1626
|
-
const ordinaryMissingMessages = new Map();
|
|
1627
|
-
const historicalMessageIdSet = new Set();
|
|
1628
|
-
const existingSequences = new Map();
|
|
1629
1648
|
for (const messageId of unique) {
|
|
1630
1649
|
const fallback = provenRootMessages.get(messageId);
|
|
1631
1650
|
const steeringProof = provenSteeringMessages.get(messageId);
|
|
@@ -1662,69 +1681,14 @@ export class SqliteSessionMetadataStore {
|
|
|
1662
1681
|
!messageContentsEqual(admission.content, fallback.content)) {
|
|
1663
1682
|
throw new SessionMetadataConflictError('Message admission fallback content conflict');
|
|
1664
1683
|
}
|
|
1665
|
-
if (admission === undefined && fallback !== undefined) {
|
|
1666
|
-
historicalMessageIdSet.add(messageId);
|
|
1667
|
-
}
|
|
1668
1684
|
if (!admission &&
|
|
1669
1685
|
this.db
|
|
1670
1686
|
.prepare('SELECT 1 AS present FROM cancelled_message_admissions WHERE session_id = ? AND message_id = ?')
|
|
1671
1687
|
.get(input.sessionId, messageId)) {
|
|
1672
1688
|
throw new SessionMetadataConflictError('Message admission is already cancelled');
|
|
1673
1689
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
SELECT message.sequence, message.record_json, payload.record_bytes, payload.sha256
|
|
1677
|
-
FROM session_messages AS message
|
|
1678
|
-
LEFT JOIN session_message_payloads AS payload
|
|
1679
|
-
ON payload.session_id = message.session_id AND payload.sequence = message.sequence
|
|
1680
|
-
WHERE message.session_id = ? AND message.message_id = ?
|
|
1681
|
-
`)
|
|
1682
|
-
.all(input.sessionId, messageId);
|
|
1683
|
-
if (rows.length > 1) {
|
|
1684
|
-
throw new SessionMetadataConflictError('Message admission transcript identity is ambiguous');
|
|
1685
|
-
}
|
|
1686
|
-
if (rows.length === 0) {
|
|
1687
|
-
const source = admission ?? fallback;
|
|
1688
|
-
if (!source)
|
|
1689
|
-
throw new SessionMetadataConflictError('Message admission does not exist');
|
|
1690
|
-
const message = decodeCanonicalMessage({
|
|
1691
|
-
type: 'user',
|
|
1692
|
-
id: messageId,
|
|
1693
|
-
turnId: input.turnId,
|
|
1694
|
-
ts: steeringProof?.eventTs ?? source.admittedAt,
|
|
1695
|
-
...source.content,
|
|
1696
|
-
steeringEventId: steeringProof?.eventId ?? messageId,
|
|
1697
|
-
});
|
|
1698
|
-
const json = JSON.stringify(message);
|
|
1699
|
-
(historicalMessageIdSet.has(messageId)
|
|
1700
|
-
? historicalMissingMessages
|
|
1701
|
-
: ordinaryMissingMessages).set(messageId, { message, json });
|
|
1702
|
-
}
|
|
1703
|
-
else {
|
|
1704
|
-
const sequence = rows[0]?.sequence;
|
|
1705
|
-
if (typeof sequence !== 'number' || !Number.isSafeInteger(sequence)) {
|
|
1706
|
-
throw new SessionMetadataConflictError('Invalid Message transcript sequence');
|
|
1707
|
-
}
|
|
1708
|
-
const row = rows[0];
|
|
1709
|
-
const recordJson = readStoredMessageRecordJson(this.db, input.sessionId, sequence, row);
|
|
1710
|
-
const message = decodeStoredMessage(JSON.parse(recordJson));
|
|
1711
|
-
const expectedSource = admission ?? fallback ?? steeringProof;
|
|
1712
|
-
if (message.type !== 'user' ||
|
|
1713
|
-
message.id !== messageId ||
|
|
1714
|
-
(expectedSource !== undefined &&
|
|
1715
|
-
!messageContentsEqual(normalizeMessageContent(message), expectedSource.content))) {
|
|
1716
|
-
throw new SessionMetadataConflictError('Message admission transcript identity conflict');
|
|
1717
|
-
}
|
|
1718
|
-
if (message.turnId !== input.turnId) {
|
|
1719
|
-
throw new SessionMetadataConflictError('Message admission transcript Turn conflict');
|
|
1720
|
-
}
|
|
1721
|
-
if (steeringProof !== undefined &&
|
|
1722
|
-
(message.ts !== steeringProof.eventTs ||
|
|
1723
|
-
message.turnId !== steeringProof.executionTurnId ||
|
|
1724
|
-
message.steeringEventId !== steeringProof.eventId)) {
|
|
1725
|
-
throw new SessionMetadataConflictError('Proven steering transcript identity conflict');
|
|
1726
|
-
}
|
|
1727
|
-
existingSequences.set(messageId, sequence);
|
|
1690
|
+
if (admission === undefined && fallback === undefined && steeringProof === undefined) {
|
|
1691
|
+
throw new SessionMetadataConflictError('Message admission does not exist');
|
|
1728
1692
|
}
|
|
1729
1693
|
if (admission) {
|
|
1730
1694
|
const deleted = this.db
|
|
@@ -1735,94 +1699,21 @@ export class SqliteSessionMetadataStore {
|
|
|
1735
1699
|
}
|
|
1736
1700
|
}
|
|
1737
1701
|
}
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
throw new SessionMetadataConflictError('Message admission transcript source order conflict');
|
|
1754
|
-
}
|
|
1755
|
-
previousExistingSequence = sequence;
|
|
1756
|
-
}
|
|
1757
|
-
const insertionGroups = [];
|
|
1758
|
-
let pending = [];
|
|
1759
|
-
let previousAnchor = -1;
|
|
1760
|
-
for (const messageId of historicalMessageIds) {
|
|
1761
|
-
const missing = historicalMissingMessages.get(messageId);
|
|
1762
|
-
if (missing) {
|
|
1763
|
-
pending.push(missing);
|
|
1764
|
-
continue;
|
|
1765
|
-
}
|
|
1766
|
-
const boundary = existingSequences.get(messageId);
|
|
1767
|
-
if (pending.length > 0 && boundary !== undefined) {
|
|
1768
|
-
const admittedAt = Math.min(...pending.map(({ message }) => message.ts));
|
|
1769
|
-
const earlierBoundary = transcript.find(({ sequence, message }) => sequence > previousAnchor &&
|
|
1770
|
-
sequence < boundary &&
|
|
1771
|
-
!historicalMessageIdSet.has(message.id) &&
|
|
1772
|
-
(message.turnId === input.turnId || message.ts >= admittedAt));
|
|
1773
|
-
if (earlierBoundary) {
|
|
1774
|
-
throw new SessionMetadataConflictError('Message admission transcript source order conflict');
|
|
1775
|
-
}
|
|
1776
|
-
insertionGroups.push({ boundary, entries: pending });
|
|
1777
|
-
pending = [];
|
|
1778
|
-
}
|
|
1779
|
-
if (boundary !== undefined)
|
|
1780
|
-
previousAnchor = boundary;
|
|
1781
|
-
}
|
|
1782
|
-
if (pending.length > 0) {
|
|
1783
|
-
const admittedAt = Math.min(...pending.map(({ message }) => message.ts));
|
|
1784
|
-
const repairBoundary = transcript.find(({ sequence, message }) => sequence > previousAnchor &&
|
|
1785
|
-
!historicalMessageIdSet.has(message.id) &&
|
|
1786
|
-
(message.turnId === input.turnId || message.ts >= admittedAt))?.sequence;
|
|
1787
|
-
insertionGroups.push({
|
|
1788
|
-
boundary: repairBoundary ?? lastSequence + 1,
|
|
1789
|
-
entries: pending,
|
|
1790
|
-
});
|
|
1791
|
-
}
|
|
1792
|
-
for (const group of insertionGroups.reverse()) {
|
|
1793
|
-
this.shiftSessionMessageSuffixSync(input.sessionId, group.boundary, group.entries.length);
|
|
1794
|
-
this.insertSessionMessagesSync(input.sessionId, group.boundary, group.entries);
|
|
1795
|
-
if (group.boundary === lastSequence + 1) {
|
|
1796
|
-
tailLatest = group.entries.at(-1)?.message;
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
if (ordinaryMissingMessages.size > 0) {
|
|
1801
|
-
const ordinaryEntries = unique.flatMap((messageId) => {
|
|
1802
|
-
const entry = ordinaryMissingMessages.get(messageId);
|
|
1803
|
-
return entry ? [entry] : [];
|
|
1804
|
-
});
|
|
1805
|
-
const currentLastSequenceRow = this.db
|
|
1806
|
-
.prepare('SELECT COALESCE(MAX(sequence), -1) AS last_sequence FROM session_messages WHERE session_id = ?')
|
|
1807
|
-
.get(input.sessionId);
|
|
1808
|
-
const currentLastSequence = currentLastSequenceRow.last_sequence;
|
|
1809
|
-
if (typeof currentLastSequence !== 'number' ||
|
|
1810
|
-
!Number.isSafeInteger(currentLastSequence) ||
|
|
1811
|
-
currentLastSequence < -1) {
|
|
1812
|
-
throw new SessionMetadataConflictError('Invalid Session message sequence');
|
|
1813
|
-
}
|
|
1814
|
-
this.insertSessionMessagesSync(input.sessionId, currentLastSequence + 1, ordinaryEntries);
|
|
1815
|
-
tailLatest = ordinaryEntries.at(-1)?.message;
|
|
1816
|
-
}
|
|
1817
|
-
if (tailLatest?.type === 'user') {
|
|
1818
|
-
this.updateCatalogProjectionSync(input.sessionId, {
|
|
1819
|
-
lastMessageAt: tailLatest.ts,
|
|
1820
|
-
lastMessagePreview: catalogPreviewForUserMessage(tailLatest),
|
|
1821
|
-
}, false, true);
|
|
1822
|
-
}
|
|
1823
|
-
else if (historicalMissingMessages.size > 0 || ordinaryMissingMessages.size > 0) {
|
|
1824
|
-
this.updateCatalogProjectionSync(input.sessionId, {}, false, true);
|
|
1825
|
-
}
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* The catalog facts a durable message carries, committed without a transcript
|
|
1706
|
+
* row to carry them: the Session list's preview line, its time, and the
|
|
1707
|
+
* connection lock a Session takes on its first user message.
|
|
1708
|
+
*/
|
|
1709
|
+
async commitMessageCatalogProjection(sessionId, message) {
|
|
1710
|
+
this.assertOpen();
|
|
1711
|
+
assertSafeSessionId(sessionId);
|
|
1712
|
+
this.transaction(() => {
|
|
1713
|
+
const record = this.readRecordSync(sessionId);
|
|
1714
|
+
if (!record)
|
|
1715
|
+
throw new SessionNotFoundError(sessionId);
|
|
1716
|
+
this.updateCatalogProjectionSync(sessionId, projectSessionCatalogMessages([message]), false, message.type === 'user' && !record.header.connectionLocked);
|
|
1826
1717
|
});
|
|
1827
1718
|
}
|
|
1828
1719
|
async updateMessageAdmission(admission) {
|
|
@@ -1937,112 +1828,57 @@ export class SqliteSessionMetadataStore {
|
|
|
1937
1828
|
async readMessages(sessionId) {
|
|
1938
1829
|
return this.readMessagesWith(sessionId, decodeStoredMessage);
|
|
1939
1830
|
}
|
|
1940
|
-
async
|
|
1831
|
+
async readMessagesAfter(sessionId, request) {
|
|
1941
1832
|
this.assertOpen();
|
|
1942
1833
|
assertSafeSessionId(sessionId);
|
|
1943
|
-
|
|
1834
|
+
if (!Number.isSafeInteger(request.maxMessages) || request.maxMessages < 1) {
|
|
1835
|
+
throw new Error('Invalid Session message count limit');
|
|
1836
|
+
}
|
|
1837
|
+
if (!Number.isSafeInteger(request.maxStoredBytes) || request.maxStoredBytes < 1) {
|
|
1838
|
+
throw new Error('Invalid Session message byte limit');
|
|
1839
|
+
}
|
|
1840
|
+
if (request.afterSequence !== undefined && request.beforeSequence !== undefined) {
|
|
1841
|
+
throw new Error('Invalid Session message scan bounds');
|
|
1842
|
+
}
|
|
1843
|
+
const backward = request.beforeSequence !== undefined;
|
|
1944
1844
|
return this.readTransaction(() => {
|
|
1945
1845
|
if (!this.readRecordSync(sessionId))
|
|
1946
1846
|
throw new SessionNotFoundError(sessionId);
|
|
1947
|
-
const highWaterRow = this.db
|
|
1948
|
-
.prepare('SELECT MAX(sequence) AS high_water FROM session_messages WHERE session_id = ?')
|
|
1949
|
-
.get(sessionId);
|
|
1950
|
-
const actualHighWater = nullableStoredMessageSequence(highWaterRow.high_water, sessionId);
|
|
1951
|
-
const throughSequence = request.throughSequence === undefined ? actualHighWater : request.throughSequence;
|
|
1952
|
-
if (throughSequence === null) {
|
|
1953
|
-
return {
|
|
1954
|
-
throughSequence: null,
|
|
1955
|
-
fragments: [],
|
|
1956
|
-
rawBytes: 0,
|
|
1957
|
-
next: null,
|
|
1958
|
-
};
|
|
1959
|
-
}
|
|
1960
|
-
if (actualHighWater === null || throughSequence > actualHighWater) {
|
|
1961
|
-
throw new Error(`Session transcript watermark is ahead of durable storage: ${sessionId}`);
|
|
1962
|
-
}
|
|
1963
|
-
const position = request.position ?? (request.direction === 'older' ? throughSequence : 0);
|
|
1964
|
-
const comparison = request.direction === 'older' ? '<=' : '>=';
|
|
1965
|
-
const order = request.direction === 'older' ? 'DESC' : 'ASC';
|
|
1966
1847
|
const rows = this.db
|
|
1967
1848
|
.prepare(`
|
|
1968
|
-
SELECT message.sequence,
|
|
1969
|
-
coalesce(payload.record_bytes, length(CAST(message.record_json AS BLOB))) AS total_bytes,
|
|
1970
|
-
payload.record_bytes IS NOT NULL AS chunked,
|
|
1971
|
-
payload.sha256 AS payload_sha256
|
|
1849
|
+
SELECT message.sequence, message.record_json, payload.record_bytes, payload.sha256
|
|
1972
1850
|
FROM session_messages AS message
|
|
1973
1851
|
LEFT JOIN session_message_payloads AS payload
|
|
1974
1852
|
ON payload.session_id = message.session_id AND payload.sequence = message.sequence
|
|
1975
|
-
WHERE message.session_id = ?
|
|
1976
|
-
|
|
1977
|
-
AND message.sequence ${comparison} ?
|
|
1978
|
-
ORDER BY message.sequence ${order}
|
|
1853
|
+
WHERE message.session_id = ? AND message.sequence ${backward ? '<' : '>'} ?
|
|
1854
|
+
ORDER BY message.sequence ${backward ? 'DESC' : 'ASC'}
|
|
1979
1855
|
LIMIT ?
|
|
1980
1856
|
`)
|
|
1981
|
-
.all(sessionId,
|
|
1982
|
-
const
|
|
1983
|
-
let
|
|
1984
|
-
let next = null;
|
|
1857
|
+
.all(sessionId, backward ? request.beforeSequence : (request.afterSequence ?? -1), request.maxMessages);
|
|
1858
|
+
const records = [];
|
|
1859
|
+
let storedBytes = 0;
|
|
1985
1860
|
for (const row of rows) {
|
|
1986
|
-
if (slices.length >= request.maxMessages || rawBytes >= request.maxBytes)
|
|
1987
|
-
break;
|
|
1988
1861
|
const sequence = requireStoredMessageSequence(row.sequence, sessionId);
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
const
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
? totalBytes
|
|
1999
|
-
: 0;
|
|
2000
|
-
if ((request.direction === 'older' && (edge < 1 || edge > totalBytes)) ||
|
|
2001
|
-
(request.direction === 'newer' && (edge < 0 || edge >= totalBytes))) {
|
|
2002
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
|
|
2003
|
-
}
|
|
2004
|
-
const available = request.maxBytes - rawBytes;
|
|
2005
|
-
const byteOffset = request.direction === 'older' ? Math.max(0, edge - available) : edge;
|
|
2006
|
-
const byteLength = request.direction === 'older'
|
|
2007
|
-
? edge - byteOffset
|
|
2008
|
-
: Math.min(totalBytes - edge, available);
|
|
2009
|
-
const complete = request.direction === 'older' ? byteOffset === 0 : byteOffset + byteLength === totalBytes;
|
|
2010
|
-
slices.push({
|
|
1862
|
+
// A record too large for one row is stored in chunks, with only a
|
|
1863
|
+
// marker inline; its size is the chunk total, not the marker's.
|
|
1864
|
+
const recordBytes = typeof row.record_bytes === 'number' ? row.record_bytes : String(row.record_json).length;
|
|
1865
|
+
// The first record of a page is always taken, so a single row larger
|
|
1866
|
+
// than the budget still makes progress instead of stalling the scan.
|
|
1867
|
+
if (records.length > 0 && storedBytes + recordBytes > request.maxStoredBytes)
|
|
1868
|
+
break;
|
|
1869
|
+
storedBytes += recordBytes;
|
|
1870
|
+
records.push({
|
|
2011
1871
|
sequence,
|
|
2012
|
-
|
|
2013
|
-
totalBytes,
|
|
2014
|
-
byteLength,
|
|
2015
|
-
chunked,
|
|
2016
|
-
payloadDigest,
|
|
1872
|
+
message: decodeStoredMessageRecordRow(this.db, sessionId, row),
|
|
2017
1873
|
});
|
|
2018
|
-
rawBytes += byteLength;
|
|
2019
|
-
if (!complete) {
|
|
2020
|
-
next = {
|
|
2021
|
-
position: sequence,
|
|
2022
|
-
byteOffset: request.direction === 'older' ? byteOffset : byteOffset + byteLength,
|
|
2023
|
-
};
|
|
2024
|
-
break;
|
|
2025
|
-
}
|
|
2026
1874
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
}
|
|
2034
|
-
const dataBySequence = readTranscriptSlices(this.db, sessionId, slices);
|
|
2035
|
-
const fragments = slices.map(({ sequence, byteOffset, totalBytes, byteLength, payloadDigest }) => {
|
|
2036
|
-
const data = dataBySequence.get(sequence);
|
|
2037
|
-
if (!data || data.byteLength !== byteLength) {
|
|
2038
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
|
|
2039
|
-
}
|
|
2040
|
-
if (byteOffset === 0 && byteLength === totalBytes) {
|
|
2041
|
-
validateTranscriptRecord(data, sessionId, sequence);
|
|
2042
|
-
}
|
|
2043
|
-
return { sequence, byteOffset, totalBytes, payloadDigest, data };
|
|
2044
|
-
});
|
|
2045
|
-
return { throughSequence, fragments, rawBytes, next };
|
|
1875
|
+
const highWater = this.db
|
|
1876
|
+
.prepare('SELECT MAX(sequence) AS high_water FROM session_messages WHERE session_id = ?')
|
|
1877
|
+
.get(sessionId);
|
|
1878
|
+
return {
|
|
1879
|
+
records,
|
|
1880
|
+
highWaterSequence: nullableStoredMessageSequence(highWater.high_water, sessionId),
|
|
1881
|
+
};
|
|
2046
1882
|
});
|
|
2047
1883
|
}
|
|
2048
1884
|
async readTranscriptMessages(sessionId, request) {
|
|
@@ -2129,80 +1965,6 @@ export class SqliteSessionMetadataStore {
|
|
|
2129
1965
|
return messages;
|
|
2130
1966
|
});
|
|
2131
1967
|
}
|
|
2132
|
-
async readTranscriptRecords(sessionId, request) {
|
|
2133
|
-
this.assertOpen();
|
|
2134
|
-
assertSafeSessionId(sessionId);
|
|
2135
|
-
assertTranscriptRecordScanRequest(request);
|
|
2136
|
-
return this.readTransaction(() => {
|
|
2137
|
-
if (!this.readRecordSync(sessionId))
|
|
2138
|
-
throw new SessionNotFoundError(sessionId);
|
|
2139
|
-
const highWaterRow = this.db
|
|
2140
|
-
.prepare('SELECT MAX(sequence) AS high_water FROM session_messages WHERE session_id = ?')
|
|
2141
|
-
.get(sessionId);
|
|
2142
|
-
const actualHighWater = nullableStoredMessageSequence(highWaterRow.high_water, sessionId);
|
|
2143
|
-
const throughSequence = request.throughSequence === undefined ? actualHighWater : request.throughSequence;
|
|
2144
|
-
if (throughSequence === null) {
|
|
2145
|
-
return { throughSequence: null, records: [], nextPosition: null };
|
|
2146
|
-
}
|
|
2147
|
-
if (actualHighWater === null || throughSequence > actualHighWater) {
|
|
2148
|
-
throw new Error(`Session transcript watermark is ahead of durable storage: ${sessionId}`);
|
|
2149
|
-
}
|
|
2150
|
-
const position = request.position ?? (request.direction === 'older' ? throughSequence : 0);
|
|
2151
|
-
const comparison = request.direction === 'older' ? '<=' : '>=';
|
|
2152
|
-
const order = request.direction === 'older' ? 'DESC' : 'ASC';
|
|
2153
|
-
const rows = this.db
|
|
2154
|
-
.prepare(`
|
|
2155
|
-
SELECT message.sequence,
|
|
2156
|
-
coalesce(payload.record_bytes, length(CAST(message.record_json AS BLOB))) AS stored_bytes
|
|
2157
|
-
FROM session_messages AS message
|
|
2158
|
-
LEFT JOIN session_message_payloads AS payload
|
|
2159
|
-
ON payload.session_id = message.session_id AND payload.sequence = message.sequence
|
|
2160
|
-
WHERE message.session_id = ?
|
|
2161
|
-
AND message.sequence <= ?
|
|
2162
|
-
AND message.sequence ${comparison} ?
|
|
2163
|
-
ORDER BY message.sequence ${order}
|
|
2164
|
-
LIMIT ?
|
|
2165
|
-
`)
|
|
2166
|
-
.all(sessionId, throughSequence, position, request.maxMessages + 1);
|
|
2167
|
-
const selected = [];
|
|
2168
|
-
let storedBytes = 0;
|
|
2169
|
-
for (const row of rows) {
|
|
2170
|
-
if (selected.length >= request.maxMessages)
|
|
2171
|
-
break;
|
|
2172
|
-
const sequence = requireStoredMessageSequence(row.sequence, sessionId);
|
|
2173
|
-
const bytes = requireTranscriptRecordByteLength(row.stored_bytes, sessionId, sequence);
|
|
2174
|
-
if (selected.length > 0 && storedBytes + bytes > request.maxStoredBytes)
|
|
2175
|
-
break;
|
|
2176
|
-
selected.push(sequence);
|
|
2177
|
-
storedBytes += bytes;
|
|
2178
|
-
}
|
|
2179
|
-
const decoded = new Map();
|
|
2180
|
-
for (const row of readStoredMessageRows(this.db, sessionId, selected)) {
|
|
2181
|
-
try {
|
|
2182
|
-
decoded.set(row.sequence, decodeStoredMessage(JSON.parse(row.recordJson)));
|
|
2183
|
-
}
|
|
2184
|
-
catch (error) {
|
|
2185
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, row.sequence, {
|
|
2186
|
-
cause: error,
|
|
2187
|
-
});
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
|
-
const records = selected.map((sequence) => {
|
|
2191
|
-
const message = decoded.get(sequence);
|
|
2192
|
-
if (!message)
|
|
2193
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
|
|
2194
|
-
return { sequence, message };
|
|
2195
|
-
});
|
|
2196
|
-
const last = selected.at(-1);
|
|
2197
|
-
return {
|
|
2198
|
-
throughSequence,
|
|
2199
|
-
records,
|
|
2200
|
-
nextPosition: last !== undefined && rows.length > selected.length
|
|
2201
|
-
? last + (request.direction === 'older' ? -1 : 1)
|
|
2202
|
-
: null,
|
|
2203
|
-
};
|
|
2204
|
-
});
|
|
2205
|
-
}
|
|
2206
1968
|
async readTranscriptHighWater(sessionId) {
|
|
2207
1969
|
this.assertOpen();
|
|
2208
1970
|
assertSafeSessionId(sessionId);
|
|
@@ -2213,295 +1975,6 @@ export class SqliteSessionMetadataStore {
|
|
|
2213
1975
|
.get(sessionId);
|
|
2214
1976
|
return nullableStoredMessageSequence(row.high_water, sessionId);
|
|
2215
1977
|
}
|
|
2216
|
-
async readTurnContributions(sessionId, throughSequence, position, maxContributions) {
|
|
2217
|
-
this.assertOpen();
|
|
2218
|
-
assertSafeSessionId(sessionId);
|
|
2219
|
-
if ((throughSequence !== null &&
|
|
2220
|
-
(!Number.isSafeInteger(throughSequence) || throughSequence < 0)) ||
|
|
2221
|
-
!Number.isSafeInteger(position) ||
|
|
2222
|
-
position < 0 ||
|
|
2223
|
-
!Number.isSafeInteger(maxContributions) ||
|
|
2224
|
-
maxContributions < 1 ||
|
|
2225
|
-
maxContributions > 128) {
|
|
2226
|
-
throw new Error('Invalid Session turn contribution request');
|
|
2227
|
-
}
|
|
2228
|
-
return this.readTransaction(() => {
|
|
2229
|
-
if (!this.readRecordSync(sessionId))
|
|
2230
|
-
throw new SessionNotFoundError(sessionId);
|
|
2231
|
-
const highWaterRow = this.db
|
|
2232
|
-
.prepare('SELECT MAX(sequence) AS high_water FROM session_messages WHERE session_id = ?')
|
|
2233
|
-
.get(sessionId);
|
|
2234
|
-
const actualHighWater = nullableStoredMessageSequence(highWaterRow.high_water, sessionId);
|
|
2235
|
-
const fixedThrough = throughSequence ?? actualHighWater;
|
|
2236
|
-
if (fixedThrough === null) {
|
|
2237
|
-
return { throughSequence: null, contributions: [], nextPosition: null };
|
|
2238
|
-
}
|
|
2239
|
-
if (actualHighWater === null || fixedThrough > actualHighWater) {
|
|
2240
|
-
throw new Error(`Session turn watermark is ahead of durable storage: ${sessionId}`);
|
|
2241
|
-
}
|
|
2242
|
-
const contributions = new Map();
|
|
2243
|
-
let nextPosition = position;
|
|
2244
|
-
let sourceMessages = 0;
|
|
2245
|
-
let sourceBytes = 0;
|
|
2246
|
-
while (nextPosition <= fixedThrough) {
|
|
2247
|
-
const rows = this.db
|
|
2248
|
-
.prepare(`
|
|
2249
|
-
SELECT message.sequence, message.record_json, payload.record_bytes, payload.sha256
|
|
2250
|
-
FROM session_messages AS message
|
|
2251
|
-
LEFT JOIN session_message_payloads AS payload
|
|
2252
|
-
ON payload.session_id = message.session_id AND payload.sequence = message.sequence
|
|
2253
|
-
WHERE message.session_id = ?
|
|
2254
|
-
AND message.sequence >= ?
|
|
2255
|
-
AND message.sequence <= ?
|
|
2256
|
-
ORDER BY message.sequence ASC
|
|
2257
|
-
LIMIT 128
|
|
2258
|
-
`)
|
|
2259
|
-
.all(sessionId, nextPosition, fixedThrough);
|
|
2260
|
-
if (rows.length === 0) {
|
|
2261
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, nextPosition);
|
|
2262
|
-
}
|
|
2263
|
-
for (const row of rows) {
|
|
2264
|
-
const sequence = requireStoredMessageSequence(row.sequence, sessionId);
|
|
2265
|
-
const recordBytes = storedMessageRecordBytes(row, sessionId, sequence);
|
|
2266
|
-
if (sourceMessages > 0 &&
|
|
2267
|
-
(sourceMessages >= SQLITE_TURN_CONTRIBUTION_MAX_SOURCE_MESSAGES ||
|
|
2268
|
-
sourceBytes + recordBytes > SQLITE_TURN_CONTRIBUTION_MAX_SOURCE_BYTES)) {
|
|
2269
|
-
return {
|
|
2270
|
-
throughSequence: fixedThrough,
|
|
2271
|
-
contributions: [...contributions.values()],
|
|
2272
|
-
nextPosition: sequence,
|
|
2273
|
-
};
|
|
2274
|
-
}
|
|
2275
|
-
const message = decodeStoredMessageRecordRow(this.db, sessionId, row);
|
|
2276
|
-
sourceMessages += 1;
|
|
2277
|
-
sourceBytes += recordBytes;
|
|
2278
|
-
if (!('turnId' in message) || typeof message.turnId !== 'string') {
|
|
2279
|
-
nextPosition = sequence + 1;
|
|
2280
|
-
continue;
|
|
2281
|
-
}
|
|
2282
|
-
const turnId = message.turnId;
|
|
2283
|
-
if (turnId && !contributions.has(turnId) && contributions.size >= maxContributions) {
|
|
2284
|
-
nextPosition = sequence;
|
|
2285
|
-
return {
|
|
2286
|
-
throughSequence: fixedThrough,
|
|
2287
|
-
contributions: [...contributions.values()],
|
|
2288
|
-
nextPosition,
|
|
2289
|
-
};
|
|
2290
|
-
}
|
|
2291
|
-
contributions.set(turnId, foldTurnContribution(contributions.get(turnId), turnId, sequence, message));
|
|
2292
|
-
nextPosition = sequence + 1;
|
|
2293
|
-
}
|
|
2294
|
-
}
|
|
2295
|
-
return {
|
|
2296
|
-
throughSequence: fixedThrough,
|
|
2297
|
-
contributions: [...contributions.values()],
|
|
2298
|
-
nextPosition: null,
|
|
2299
|
-
};
|
|
2300
|
-
});
|
|
2301
|
-
}
|
|
2302
|
-
async readTurnLandmarks(sessionId, maxLandmarks) {
|
|
2303
|
-
this.assertOpen();
|
|
2304
|
-
assertSafeSessionId(sessionId);
|
|
2305
|
-
if (!Number.isSafeInteger(maxLandmarks) || maxLandmarks < 1 || maxLandmarks > 64) {
|
|
2306
|
-
throw new Error('Invalid Session turn landmark limit');
|
|
2307
|
-
}
|
|
2308
|
-
return this.readTransaction(() => {
|
|
2309
|
-
if (!this.readRecordSync(sessionId))
|
|
2310
|
-
throw new SessionNotFoundError(sessionId);
|
|
2311
|
-
const throughRow = this.db
|
|
2312
|
-
.prepare(`
|
|
2313
|
-
SELECT sequence AS through_sequence
|
|
2314
|
-
FROM session_messages
|
|
2315
|
-
WHERE session_id = ?
|
|
2316
|
-
ORDER BY sequence DESC
|
|
2317
|
-
LIMIT 1
|
|
2318
|
-
`)
|
|
2319
|
-
.get(sessionId);
|
|
2320
|
-
const throughSequence = nullableStoredMessageSequence(throughRow?.through_sequence, sessionId);
|
|
2321
|
-
if (throughSequence === null) {
|
|
2322
|
-
return { throughSequence: null, landmarks: [] };
|
|
2323
|
-
}
|
|
2324
|
-
const promptRows = this.db
|
|
2325
|
-
.prepare(`
|
|
2326
|
-
SELECT admission.admitted_at, message.sequence
|
|
2327
|
-
FROM core_root_turn_admissions AS admission
|
|
2328
|
-
JOIN session_messages AS message
|
|
2329
|
-
ON message.session_id = admission.session_id
|
|
2330
|
-
AND message.message_id = json_extract(admission.record_json, '$.userMessageId')
|
|
2331
|
-
LEFT JOIN session_message_payloads AS payload
|
|
2332
|
-
ON payload.session_id = message.session_id AND payload.sequence = message.sequence
|
|
2333
|
-
WHERE admission.session_id = ? AND payload.sequence IS NULL
|
|
2334
|
-
ORDER BY admission.admitted_at ASC, admission.turn_id ASC
|
|
2335
|
-
LIMIT ?
|
|
2336
|
-
`)
|
|
2337
|
-
.all(sessionId, maxLandmarks + 1);
|
|
2338
|
-
const selected = new Set();
|
|
2339
|
-
if (promptRows.length <= maxLandmarks) {
|
|
2340
|
-
for (const row of promptRows) {
|
|
2341
|
-
selected.add(requireStoredMessageSequence(row.sequence, sessionId));
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
const forward = this.db.prepare(`
|
|
2345
|
-
SELECT admission.admitted_at, message.sequence
|
|
2346
|
-
FROM core_root_turn_admissions AS admission
|
|
2347
|
-
JOIN session_messages AS message
|
|
2348
|
-
ON message.session_id = admission.session_id
|
|
2349
|
-
AND message.message_id = json_extract(admission.record_json, '$.userMessageId')
|
|
2350
|
-
LEFT JOIN session_message_payloads AS payload
|
|
2351
|
-
ON payload.session_id = message.session_id AND payload.sequence = message.sequence
|
|
2352
|
-
WHERE admission.session_id = ? AND admission.admitted_at >= ? AND payload.sequence IS NULL
|
|
2353
|
-
ORDER BY admission.admitted_at ASC, admission.turn_id ASC
|
|
2354
|
-
LIMIT 1
|
|
2355
|
-
`);
|
|
2356
|
-
const backward = this.db.prepare(`
|
|
2357
|
-
SELECT admission.admitted_at, message.sequence
|
|
2358
|
-
FROM core_root_turn_admissions AS admission
|
|
2359
|
-
JOIN session_messages AS message
|
|
2360
|
-
ON message.session_id = admission.session_id
|
|
2361
|
-
AND message.message_id = json_extract(admission.record_json, '$.userMessageId')
|
|
2362
|
-
LEFT JOIN session_message_payloads AS payload
|
|
2363
|
-
ON payload.session_id = message.session_id AND payload.sequence = message.sequence
|
|
2364
|
-
WHERE admission.session_id = ? AND admission.admitted_at < ? AND payload.sequence IS NULL
|
|
2365
|
-
ORDER BY admission.admitted_at DESC, admission.turn_id DESC
|
|
2366
|
-
LIMIT 1
|
|
2367
|
-
`);
|
|
2368
|
-
if (promptRows.length > maxLandmarks) {
|
|
2369
|
-
const firstAdmittedAt = requireTurnLandmarkAdmittedAt(promptRows[0]?.admitted_at);
|
|
2370
|
-
const lastRow = this.db
|
|
2371
|
-
.prepare(`
|
|
2372
|
-
SELECT admitted_at
|
|
2373
|
-
FROM core_root_turn_admissions
|
|
2374
|
-
WHERE session_id = ?
|
|
2375
|
-
ORDER BY admitted_at DESC, turn_id DESC
|
|
2376
|
-
LIMIT 1
|
|
2377
|
-
`)
|
|
2378
|
-
.get(sessionId);
|
|
2379
|
-
const lastAdmittedAt = requireTurnLandmarkAdmittedAt(lastRow?.admitted_at);
|
|
2380
|
-
for (let index = 0; index < maxLandmarks; index += 1) {
|
|
2381
|
-
const target = maxLandmarks === 1
|
|
2382
|
-
? lastAdmittedAt
|
|
2383
|
-
: firstAdmittedAt +
|
|
2384
|
-
Math.floor(((lastAdmittedAt - firstAdmittedAt) * index) / (maxLandmarks - 1));
|
|
2385
|
-
const candidates = [
|
|
2386
|
-
...forward.all(sessionId, target),
|
|
2387
|
-
...backward.all(sessionId, target),
|
|
2388
|
-
];
|
|
2389
|
-
let nearest;
|
|
2390
|
-
for (const candidate of candidates) {
|
|
2391
|
-
const admittedAt = requireTurnLandmarkAdmittedAt(candidate.admitted_at);
|
|
2392
|
-
if (nearest === undefined ||
|
|
2393
|
-
Math.abs(admittedAt - target) <
|
|
2394
|
-
Math.abs(requireTurnLandmarkAdmittedAt(nearest.admitted_at) - target)) {
|
|
2395
|
-
nearest = candidate;
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
if (nearest)
|
|
2399
|
-
selected.add(requireStoredMessageSequence(nearest.sequence, sessionId));
|
|
2400
|
-
}
|
|
2401
|
-
}
|
|
2402
|
-
const firstIndexedSequence = promptRows.length > 0
|
|
2403
|
-
? requireStoredMessageSequence(promptRows[0]?.sequence, sessionId)
|
|
2404
|
-
: null;
|
|
2405
|
-
const legacyThrough = firstIndexedSequence === null ? throughSequence : firstIndexedSequence - 1;
|
|
2406
|
-
if (legacyThrough >= 0) {
|
|
2407
|
-
const firstRow = this.db
|
|
2408
|
-
.prepare(`
|
|
2409
|
-
SELECT sequence AS first_sequence
|
|
2410
|
-
FROM session_messages
|
|
2411
|
-
WHERE session_id = ?
|
|
2412
|
-
ORDER BY sequence ASC
|
|
2413
|
-
LIMIT 1
|
|
2414
|
-
`)
|
|
2415
|
-
.get(sessionId);
|
|
2416
|
-
const firstSequence = nullableStoredMessageSequence(firstRow?.first_sequence, sessionId);
|
|
2417
|
-
if (firstSequence === null || firstSequence > legacyThrough) {
|
|
2418
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, legacyThrough);
|
|
2419
|
-
}
|
|
2420
|
-
const forwardLegacy = this.db.prepare(`
|
|
2421
|
-
SELECT message.sequence, message.message_type, payload.sequence AS payload_sequence
|
|
2422
|
-
FROM session_messages AS message
|
|
2423
|
-
LEFT JOIN session_message_payloads AS payload
|
|
2424
|
-
ON payload.session_id = message.session_id AND payload.sequence = message.sequence
|
|
2425
|
-
WHERE message.session_id = ? AND message.sequence >= ? AND message.sequence <= ?
|
|
2426
|
-
ORDER BY message.sequence ASC
|
|
2427
|
-
LIMIT ${SQLITE_TURN_LANDMARK_LEGACY_NEIGHBOR_MESSAGES}
|
|
2428
|
-
`);
|
|
2429
|
-
const backwardLegacy = this.db.prepare(`
|
|
2430
|
-
SELECT message.sequence, message.message_type, payload.sequence AS payload_sequence
|
|
2431
|
-
FROM session_messages AS message
|
|
2432
|
-
LEFT JOIN session_message_payloads AS payload
|
|
2433
|
-
ON payload.session_id = message.session_id AND payload.sequence = message.sequence
|
|
2434
|
-
WHERE message.session_id = ? AND message.sequence < ? AND message.sequence >= ?
|
|
2435
|
-
ORDER BY message.sequence DESC
|
|
2436
|
-
LIMIT ${SQLITE_TURN_LANDMARK_LEGACY_NEIGHBOR_MESSAGES}
|
|
2437
|
-
`);
|
|
2438
|
-
const targetCount = Math.min(maxLandmarks, legacyThrough - firstSequence + 1);
|
|
2439
|
-
for (let index = 0; index < targetCount; index += 1) {
|
|
2440
|
-
const target = targetCount === 1
|
|
2441
|
-
? legacyThrough
|
|
2442
|
-
: firstSequence +
|
|
2443
|
-
Math.floor(((legacyThrough - firstSequence) * index) / (targetCount - 1));
|
|
2444
|
-
const candidates = [
|
|
2445
|
-
...forwardLegacy.all(sessionId, target, legacyThrough),
|
|
2446
|
-
...backwardLegacy.all(sessionId, target, firstSequence),
|
|
2447
|
-
];
|
|
2448
|
-
let nearest;
|
|
2449
|
-
for (const candidate of candidates) {
|
|
2450
|
-
const sequence = requireStoredMessageSequence(candidate.sequence, sessionId);
|
|
2451
|
-
if (candidate.message_type !== 'user' || candidate.payload_sequence !== null)
|
|
2452
|
-
continue;
|
|
2453
|
-
if (nearest === undefined || Math.abs(sequence - target) < Math.abs(nearest - target)) {
|
|
2454
|
-
nearest = sequence;
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
if (nearest !== undefined)
|
|
2458
|
-
selected.add(nearest);
|
|
2459
|
-
}
|
|
2460
|
-
}
|
|
2461
|
-
const selectedSequences = [...selected].sort((left, right) => left - right);
|
|
2462
|
-
const sampledSequences = selectedSequences.length <= maxLandmarks
|
|
2463
|
-
? selectedSequences
|
|
2464
|
-
: Array.from({ length: maxLandmarks }, (_, index) => selectedSequences[maxLandmarks === 1
|
|
2465
|
-
? selectedSequences.length - 1
|
|
2466
|
-
: Math.floor(((selectedSequences.length - 1) * index) / (maxLandmarks - 1))]);
|
|
2467
|
-
const landmarks = readStoredMessageRows(this.db, sessionId, sampledSequences).flatMap(({ sequence, recordJson }) => {
|
|
2468
|
-
let message;
|
|
2469
|
-
try {
|
|
2470
|
-
message = decodeStoredMessage(JSON.parse(recordJson));
|
|
2471
|
-
}
|
|
2472
|
-
catch (error) {
|
|
2473
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, sequence, { cause: error });
|
|
2474
|
-
}
|
|
2475
|
-
if (message.type !== 'user') {
|
|
2476
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
|
|
2477
|
-
}
|
|
2478
|
-
const label = (message.displayText ?? message.text).trim();
|
|
2479
|
-
return label ? [{ turnId: message.turnId, sequence, label }] : [];
|
|
2480
|
-
});
|
|
2481
|
-
return { throughSequence, landmarks };
|
|
2482
|
-
});
|
|
2483
|
-
}
|
|
2484
|
-
async readMessagesForRecovery(sessionId) {
|
|
2485
|
-
return this.readMessagesWith(sessionId, decodeStoredMessage);
|
|
2486
|
-
}
|
|
2487
|
-
async readPreviewMessages(sessionId, limit = 10) {
|
|
2488
|
-
this.assertOpen();
|
|
2489
|
-
assertSafeSessionId(sessionId);
|
|
2490
|
-
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 128) {
|
|
2491
|
-
throw new Error('Session message preview limit must be between 1 and 128');
|
|
2492
|
-
}
|
|
2493
|
-
if (!this.readRecordSync(sessionId))
|
|
2494
|
-
throw new SessionNotFoundError(sessionId);
|
|
2495
|
-
const sequences = this.db
|
|
2496
|
-
.prepare(`
|
|
2497
|
-
SELECT sequence FROM session_messages
|
|
2498
|
-
WHERE session_id = ? ORDER BY sequence DESC LIMIT ?
|
|
2499
|
-
`)
|
|
2500
|
-
.all(sessionId, limit)
|
|
2501
|
-
.map((row) => requireStoredMessageSequence(row.sequence, sessionId))
|
|
2502
|
-
.reverse();
|
|
2503
|
-
return readStoredMessageRows(this.db, sessionId, sequences, sequences.map(() => '?').join(', ')).map((row) => decodeStoredMessageRow({ sequence: row.sequence, record_json: row.recordJson }, sessionId));
|
|
2504
|
-
}
|
|
2505
1978
|
async beginCatalogProjectionWrite() {
|
|
2506
1979
|
this.assertOpen();
|
|
2507
1980
|
this.transaction(() => {
|
|
@@ -4022,64 +3495,6 @@ export class SqliteSessionMetadataStore {
|
|
|
4022
3495
|
.get(sessionId, messageId);
|
|
4023
3496
|
return row ? decodeStoredMessageRecordRow(this.db, sessionId, row) : undefined;
|
|
4024
3497
|
}
|
|
4025
|
-
readSessionMessageOrderingSync(sessionId) {
|
|
4026
|
-
const rows = this.db
|
|
4027
|
-
.prepare(`
|
|
4028
|
-
SELECT message.sequence, message.record_json, payload.record_bytes, payload.sha256
|
|
4029
|
-
FROM session_messages AS message
|
|
4030
|
-
LEFT JOIN session_message_payloads AS payload
|
|
4031
|
-
ON payload.session_id = message.session_id AND payload.sequence = message.sequence
|
|
4032
|
-
WHERE message.session_id = ?
|
|
4033
|
-
ORDER BY message.sequence
|
|
4034
|
-
`)
|
|
4035
|
-
.all(sessionId);
|
|
4036
|
-
return rows.map((row) => {
|
|
4037
|
-
const sequence = requireStoredMessageSequence(row.sequence, sessionId);
|
|
4038
|
-
const recordJson = readStoredMessageRecordJson(this.db, sessionId, sequence, row);
|
|
4039
|
-
return {
|
|
4040
|
-
sequence,
|
|
4041
|
-
message: decodeStoredMessage(JSON.parse(recordJson)),
|
|
4042
|
-
};
|
|
4043
|
-
});
|
|
4044
|
-
}
|
|
4045
|
-
shiftSessionMessageSuffixSync(sessionId, firstSequence, amount) {
|
|
4046
|
-
if (!Number.isSafeInteger(firstSequence) || firstSequence < 0) {
|
|
4047
|
-
throw new SessionMetadataConflictError('Invalid transcript insertion sequence');
|
|
4048
|
-
}
|
|
4049
|
-
if (!Number.isSafeInteger(amount) || amount < 1) {
|
|
4050
|
-
throw new SessionMetadataConflictError('Invalid transcript insertion size');
|
|
4051
|
-
}
|
|
4052
|
-
const sequences = this.db
|
|
4053
|
-
.prepare(`
|
|
4054
|
-
SELECT sequence
|
|
4055
|
-
FROM session_messages
|
|
4056
|
-
WHERE session_id = ? AND sequence >= ?
|
|
4057
|
-
ORDER BY sequence DESC
|
|
4058
|
-
`)
|
|
4059
|
-
.all(sessionId, firstSequence).map((row) => requireStoredMessageSequence(row.sequence, sessionId));
|
|
4060
|
-
const highest = sequences[0];
|
|
4061
|
-
if (highest !== undefined && highest > Number.MAX_SAFE_INTEGER - amount) {
|
|
4062
|
-
throw new SessionMetadataConflictError('Session message sequence overflow');
|
|
4063
|
-
}
|
|
4064
|
-
if (sequences.length === 0)
|
|
4065
|
-
return;
|
|
4066
|
-
this.db.exec('PRAGMA defer_foreign_keys = ON');
|
|
4067
|
-
const moveChunks = this.db.prepare('UPDATE session_message_chunks SET sequence = ? WHERE session_id = ? AND sequence = ?');
|
|
4068
|
-
const movePayload = this.db.prepare('UPDATE session_message_payloads SET sequence = ? WHERE session_id = ? AND sequence = ?');
|
|
4069
|
-
const moveMessage = this.db.prepare('UPDATE session_messages SET sequence = ? WHERE session_id = ? AND sequence = ?');
|
|
4070
|
-
for (const sequence of sequences) {
|
|
4071
|
-
const shifted = sequence + amount;
|
|
4072
|
-
moveChunks.run(shifted, sessionId, sequence);
|
|
4073
|
-
const payload = movePayload.run(shifted, sessionId, sequence);
|
|
4074
|
-
if (payload.changes !== 0 && payload.changes !== 1) {
|
|
4075
|
-
throw new SessionMetadataConflictError('Message payload sequence is ambiguous');
|
|
4076
|
-
}
|
|
4077
|
-
const message = moveMessage.run(shifted, sessionId, sequence);
|
|
4078
|
-
if (message.changes !== 1) {
|
|
4079
|
-
throw new SessionMetadataConflictError('Message transcript sequence changed during repair');
|
|
4080
|
-
}
|
|
4081
|
-
}
|
|
4082
|
-
}
|
|
4083
3498
|
insertSessionMessagesSync(sessionId, firstSequence, entries) {
|
|
4084
3499
|
if (!Number.isSafeInteger(firstSequence) ||
|
|
4085
3500
|
firstSequence < 0 ||
|
|
@@ -4180,12 +3595,19 @@ export class SqliteSessionMetadataStore {
|
|
|
4180
3595
|
if (!current)
|
|
4181
3596
|
throw new SessionNotFoundError(sessionId);
|
|
4182
3597
|
const lastMessageAt = maxTimestamp(current.header.lastMessageAt, projection.lastMessageAt);
|
|
3598
|
+
// The preview refuses to move backwards for the same reason the timestamp
|
|
3599
|
+
// does: a message older than the one on show is a repair of something the
|
|
3600
|
+
// catalog already passed, and recovery replays exactly those.
|
|
3601
|
+
const stale = !replacePreview &&
|
|
3602
|
+
projection.lastMessageAt !== undefined &&
|
|
3603
|
+
current.header.lastMessageAt !== undefined &&
|
|
3604
|
+
projection.lastMessageAt < current.header.lastMessageAt;
|
|
4183
3605
|
this.updateHeaderSync(sessionId, {
|
|
4184
3606
|
...(lockConnection ? { connectionLocked: true } : {}),
|
|
4185
3607
|
...(lastMessageAt === undefined ? {} : { lastMessageAt }),
|
|
4186
3608
|
}, {
|
|
4187
3609
|
skipNoop: true,
|
|
4188
|
-
...(replacePreview || projection.lastMessagePreview !== undefined
|
|
3610
|
+
...(!stale && (replacePreview || projection.lastMessagePreview !== undefined)
|
|
4189
3611
|
? {
|
|
4190
3612
|
catalogPreview: {
|
|
4191
3613
|
kind: 'replace',
|
|
@@ -5105,22 +4527,6 @@ function decodeStoredMessageRow(row, sessionId) {
|
|
|
5105
4527
|
});
|
|
5106
4528
|
}
|
|
5107
4529
|
}
|
|
5108
|
-
function requireTurnLandmarkAdmittedAt(value) {
|
|
5109
|
-
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
5110
|
-
throw new Error('Invalid root Turn admission timestamp');
|
|
5111
|
-
}
|
|
5112
|
-
return value;
|
|
5113
|
-
}
|
|
5114
|
-
function storedMessageRecordBytes(row, sessionId, sequence) {
|
|
5115
|
-
if (row.record_bytes !== null) {
|
|
5116
|
-
return requireTranscriptRecordByteLength(row.record_bytes, sessionId, sequence);
|
|
5117
|
-
}
|
|
5118
|
-
if (typeof row.record_json !== 'string' ||
|
|
5119
|
-
row.record_json === SQLITE_SESSION_MESSAGE_CHUNK_MARKER) {
|
|
5120
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
|
|
5121
|
-
}
|
|
5122
|
-
return Buffer.byteLength(row.record_json, 'utf8');
|
|
5123
|
-
}
|
|
5124
4530
|
function decodeStoredMessageRecordRow(db, sessionId, row) {
|
|
5125
4531
|
const sequence = requireStoredMessageSequence(row.sequence, sessionId);
|
|
5126
4532
|
return decodeStoredMessageRow({
|
|
@@ -5183,31 +4589,6 @@ function sameWorkHubAssignmentRequest(existing, requested) {
|
|
|
5183
4589
|
replacesDelegationId: requested.replacesDelegationId,
|
|
5184
4590
|
});
|
|
5185
4591
|
}
|
|
5186
|
-
function foldTurnContribution(current, turnId, sequence, message) {
|
|
5187
|
-
const contribution = current ?? {
|
|
5188
|
-
turnId,
|
|
5189
|
-
firstSequence: sequence,
|
|
5190
|
-
latestState: null,
|
|
5191
|
-
userPromptPreview: null,
|
|
5192
|
-
hasAssistantMessage: false,
|
|
5193
|
-
hasAssistantOutput: false,
|
|
5194
|
-
hasToolResult: false,
|
|
5195
|
-
hasFailedToolResult: false,
|
|
5196
|
-
hasAbortNote: false,
|
|
5197
|
-
};
|
|
5198
|
-
const userPrompt = message.type === 'user' ? (message.displayText ?? message.text).trim() : '';
|
|
5199
|
-
return {
|
|
5200
|
-
...contribution,
|
|
5201
|
-
latestState: message.type === 'turn_state' ? { sequence, message } : contribution.latestState,
|
|
5202
|
-
userPromptPreview: contribution.userPromptPreview ?? (userPrompt || null),
|
|
5203
|
-
hasAssistantMessage: contribution.hasAssistantMessage || message.type === 'assistant',
|
|
5204
|
-
hasAssistantOutput: contribution.hasAssistantOutput ||
|
|
5205
|
-
(message.type === 'assistant' && message.text.trim().length > 0),
|
|
5206
|
-
hasToolResult: contribution.hasToolResult || message.type === 'tool_result',
|
|
5207
|
-
hasFailedToolResult: contribution.hasFailedToolResult || (message.type === 'tool_result' && message.isError),
|
|
5208
|
-
hasAbortNote: contribution.hasAbortNote || (message.type === 'system_note' && message.kind === 'abort'),
|
|
5209
|
-
};
|
|
5210
|
-
}
|
|
5211
4592
|
function readStoredMessageRows(db, sessionId, sequences, placeholders = sequences.map(() => '?').join(', ')) {
|
|
5212
4593
|
if (sequences.length === 0)
|
|
5213
4594
|
return [];
|
|
@@ -5274,173 +4655,9 @@ function nullableStoredMessageSequence(value, sessionId) {
|
|
|
5274
4655
|
return null;
|
|
5275
4656
|
return requireStoredMessageSequence(value, sessionId);
|
|
5276
4657
|
}
|
|
5277
|
-
function requireTranscriptPayloadDigest(value, sessionId, sequence) {
|
|
5278
|
-
if (typeof value !== 'string' || !/^[0-9a-f]{64}$/.test(value)) {
|
|
5279
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
|
|
5280
|
-
}
|
|
5281
|
-
return `sha256:${value}`;
|
|
5282
|
-
}
|
|
5283
4658
|
function requireTranscriptRecordByteLength(value, sessionId, sequence) {
|
|
5284
4659
|
if (!Number.isSafeInteger(value) || value < 1) {
|
|
5285
4660
|
throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
|
|
5286
4661
|
}
|
|
5287
4662
|
return value;
|
|
5288
4663
|
}
|
|
5289
|
-
function readTranscriptSlices(db, sessionId, slices) {
|
|
5290
|
-
if (slices.length === 0)
|
|
5291
|
-
return new Map();
|
|
5292
|
-
const chunkedSlices = slices.filter((slice) => slice.chunked);
|
|
5293
|
-
const values = chunkedSlices.map(() => '(?, ?, ?)').join(', ');
|
|
5294
|
-
const parameters = chunkedSlices.flatMap((slice) => [
|
|
5295
|
-
slice.sequence,
|
|
5296
|
-
Math.floor(slice.byteOffset / SQLITE_SESSION_MESSAGE_CHUNK_BYTES),
|
|
5297
|
-
Math.floor((slice.byteOffset + slice.byteLength - 1) / SQLITE_SESSION_MESSAGE_CHUNK_BYTES),
|
|
5298
|
-
]);
|
|
5299
|
-
const rows = chunkedSlices.length === 0
|
|
5300
|
-
? []
|
|
5301
|
-
: db
|
|
5302
|
-
.prepare(`
|
|
5303
|
-
WITH requested(sequence, first_chunk, last_chunk) AS (VALUES ${values})
|
|
5304
|
-
SELECT requested.sequence, chunk.chunk_index, chunk.data, chunk.sha256
|
|
5305
|
-
FROM requested
|
|
5306
|
-
INNER JOIN session_message_chunks AS chunk
|
|
5307
|
-
ON chunk.session_id = ?
|
|
5308
|
-
AND chunk.sequence = requested.sequence
|
|
5309
|
-
AND chunk.chunk_index BETWEEN requested.first_chunk AND requested.last_chunk
|
|
5310
|
-
ORDER BY requested.sequence, chunk.chunk_index
|
|
5311
|
-
`)
|
|
5312
|
-
.all(...parameters, sessionId);
|
|
5313
|
-
const rowsBySequence = new Map();
|
|
5314
|
-
for (const row of rows) {
|
|
5315
|
-
const sequence = requireStoredMessageSequence(row.sequence, sessionId);
|
|
5316
|
-
const grouped = rowsBySequence.get(sequence);
|
|
5317
|
-
if (grouped)
|
|
5318
|
-
grouped.push(row);
|
|
5319
|
-
else
|
|
5320
|
-
rowsBySequence.set(sequence, [row]);
|
|
5321
|
-
}
|
|
5322
|
-
const result = new Map();
|
|
5323
|
-
for (const slice of slices) {
|
|
5324
|
-
if (!slice.chunked)
|
|
5325
|
-
continue;
|
|
5326
|
-
const selected = rowsBySequence.get(slice.sequence) ?? [];
|
|
5327
|
-
const firstChunk = Math.floor(slice.byteOffset / SQLITE_SESSION_MESSAGE_CHUNK_BYTES);
|
|
5328
|
-
const lastChunk = Math.floor((slice.byteOffset + slice.byteLength - 1) / SQLITE_SESSION_MESSAGE_CHUNK_BYTES);
|
|
5329
|
-
if (selected.length !== lastChunk - firstChunk + 1) {
|
|
5330
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, slice.sequence);
|
|
5331
|
-
}
|
|
5332
|
-
const chunks = [];
|
|
5333
|
-
for (let index = 0; index < selected.length; index += 1) {
|
|
5334
|
-
const row = selected[index];
|
|
5335
|
-
if (row.chunk_index !== firstChunk + index ||
|
|
5336
|
-
!(row.data instanceof Uint8Array) ||
|
|
5337
|
-
typeof row.sha256 !== 'string') {
|
|
5338
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, slice.sequence);
|
|
5339
|
-
}
|
|
5340
|
-
const chunk = Buffer.from(row.data);
|
|
5341
|
-
if (createHash('sha256').update(chunk).digest('hex') !== row.sha256) {
|
|
5342
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, slice.sequence);
|
|
5343
|
-
}
|
|
5344
|
-
chunks.push(chunk);
|
|
5345
|
-
}
|
|
5346
|
-
const joined = Buffer.concat(chunks);
|
|
5347
|
-
const start = slice.byteOffset - firstChunk * SQLITE_SESSION_MESSAGE_CHUNK_BYTES;
|
|
5348
|
-
const data = joined.subarray(start, start + slice.byteLength);
|
|
5349
|
-
if (data.byteLength !== slice.byteLength) {
|
|
5350
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, slice.sequence);
|
|
5351
|
-
}
|
|
5352
|
-
result.set(slice.sequence, data);
|
|
5353
|
-
}
|
|
5354
|
-
const inlineSlices = slices.filter((slice) => !slice.chunked);
|
|
5355
|
-
if (inlineSlices.length > 0) {
|
|
5356
|
-
const inlineValues = inlineSlices.map(() => '(?, ?, ?)').join(', ');
|
|
5357
|
-
const inlineParameters = inlineSlices.flatMap((slice) => [
|
|
5358
|
-
slice.sequence,
|
|
5359
|
-
slice.byteOffset + 1,
|
|
5360
|
-
slice.byteLength,
|
|
5361
|
-
]);
|
|
5362
|
-
const inlineRows = db
|
|
5363
|
-
.prepare(`
|
|
5364
|
-
WITH requested(sequence, byte_start, byte_length) AS (VALUES ${inlineValues})
|
|
5365
|
-
SELECT requested.sequence,
|
|
5366
|
-
substr(CAST(message.record_json AS BLOB), requested.byte_start, requested.byte_length)
|
|
5367
|
-
AS data
|
|
5368
|
-
FROM requested
|
|
5369
|
-
INNER JOIN session_messages AS message
|
|
5370
|
-
ON message.session_id = ? AND message.sequence = requested.sequence
|
|
5371
|
-
`)
|
|
5372
|
-
.all(...inlineParameters, sessionId);
|
|
5373
|
-
for (const row of inlineRows) {
|
|
5374
|
-
const sequence = requireStoredMessageSequence(row.sequence, sessionId);
|
|
5375
|
-
if (!(row.data instanceof Uint8Array)) {
|
|
5376
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, sequence);
|
|
5377
|
-
}
|
|
5378
|
-
result.set(sequence, Buffer.from(row.data));
|
|
5379
|
-
}
|
|
5380
|
-
}
|
|
5381
|
-
return result;
|
|
5382
|
-
}
|
|
5383
|
-
function validateTranscriptRecord(data, sessionId, sequence) {
|
|
5384
|
-
try {
|
|
5385
|
-
decodeStoredMessage(markPersisted(JSON.parse(typeof data === 'string' ? data : data.toString('utf8'))));
|
|
5386
|
-
}
|
|
5387
|
-
catch (error) {
|
|
5388
|
-
throw new StoredSessionMessageIncompatibleError(sessionId, sequence, {
|
|
5389
|
-
cause: error,
|
|
5390
|
-
});
|
|
5391
|
-
}
|
|
5392
|
-
}
|
|
5393
|
-
function assertTranscriptPageRequest(request) {
|
|
5394
|
-
if (request.direction !== 'older' && request.direction !== 'newer') {
|
|
5395
|
-
throw new Error('Invalid Session transcript page direction');
|
|
5396
|
-
}
|
|
5397
|
-
if (request.throughSequence !== undefined &&
|
|
5398
|
-
request.throughSequence !== null &&
|
|
5399
|
-
(!Number.isSafeInteger(request.throughSequence) || request.throughSequence < 0)) {
|
|
5400
|
-
throw new Error('Invalid Session transcript watermark');
|
|
5401
|
-
}
|
|
5402
|
-
if (request.position !== undefined &&
|
|
5403
|
-
(!Number.isSafeInteger(request.position) || request.position < 0)) {
|
|
5404
|
-
throw new Error('Invalid Session transcript position');
|
|
5405
|
-
}
|
|
5406
|
-
if (request.byteOffset !== undefined &&
|
|
5407
|
-
(request.position === undefined ||
|
|
5408
|
-
!Number.isSafeInteger(request.byteOffset) ||
|
|
5409
|
-
request.byteOffset < 0)) {
|
|
5410
|
-
throw new Error('Invalid Session transcript byte offset');
|
|
5411
|
-
}
|
|
5412
|
-
if (!Number.isSafeInteger(request.maxBytes) ||
|
|
5413
|
-
request.maxBytes < 1 ||
|
|
5414
|
-
request.maxBytes > 1024 * 1024) {
|
|
5415
|
-
throw new Error('Session transcript page byte limit must be between 1 and 1048576');
|
|
5416
|
-
}
|
|
5417
|
-
if (!Number.isSafeInteger(request.maxMessages) ||
|
|
5418
|
-
request.maxMessages < 1 ||
|
|
5419
|
-
request.maxMessages > 256) {
|
|
5420
|
-
throw new Error('Session transcript page message limit must be between 1 and 256');
|
|
5421
|
-
}
|
|
5422
|
-
}
|
|
5423
|
-
function assertTranscriptRecordScanRequest(request) {
|
|
5424
|
-
if (request.direction !== 'older' && request.direction !== 'newer') {
|
|
5425
|
-
throw new Error('Invalid Session transcript record direction');
|
|
5426
|
-
}
|
|
5427
|
-
if (request.throughSequence !== undefined &&
|
|
5428
|
-
request.throughSequence !== null &&
|
|
5429
|
-
(!Number.isSafeInteger(request.throughSequence) || request.throughSequence < 0)) {
|
|
5430
|
-
throw new Error('Invalid Session transcript watermark');
|
|
5431
|
-
}
|
|
5432
|
-
if (request.position !== undefined &&
|
|
5433
|
-
(!Number.isSafeInteger(request.position) || request.position < 0)) {
|
|
5434
|
-
throw new Error('Invalid Session transcript position');
|
|
5435
|
-
}
|
|
5436
|
-
if (!Number.isSafeInteger(request.maxStoredBytes) ||
|
|
5437
|
-
request.maxStoredBytes < 1 ||
|
|
5438
|
-
request.maxStoredBytes > 16 * 1024 * 1024) {
|
|
5439
|
-
throw new Error('Invalid Session transcript record byte limit');
|
|
5440
|
-
}
|
|
5441
|
-
if (!Number.isSafeInteger(request.maxMessages) ||
|
|
5442
|
-
request.maxMessages < 1 ||
|
|
5443
|
-
request.maxMessages > 256) {
|
|
5444
|
-
throw new Error('Invalid Session transcript record count limit');
|
|
5445
|
-
}
|
|
5446
|
-
}
|