maka-agent 0.2.0-dev.23.20260906 → 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-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/canonical-runtime-event.js +3 -1
- 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 +9 -2
- 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.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-record-schema.js +26 -35
- package/node_modules/@maka/core/package.json +4 -0
- 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 +127 -133
- 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/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/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 +2 -0
- package/node_modules/@maka/runtime-host/dist/client/launcher.js +3 -0
- 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/protocol/host-status.js +21 -12
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +25 -1
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
- 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/server/access-credential-store.js +1 -0
- package/node_modules/@maka/runtime-host/dist/server/canonical-turn-snapshot.js +19 -4
- package/node_modules/@maka/runtime-host/dist/server/connection-session.js +7 -6
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +100 -24
- 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/goal-coordinator.js +40 -1
- package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +96 -17
- 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 +76 -6
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +325 -73
- 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 +50 -7
- 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/workhub-coordination-coordinator.js +0 -1
- 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/execution-stores.js +9 -7
- package/node_modules/@maka/storage/dist/file-session-repository.js +1012 -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/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 +7 -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/tool-result-archive-evidence.js +111 -0
- package/node_modules/@maka/storage/package.json +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
* or more contributor license agreements. See the NOTICE file
|
|
4
|
+
* distributed with this work for additional information
|
|
5
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
* to you under the Apache License, Version 2.0 (the
|
|
7
|
+
* "License"); you may not use this file except in compliance
|
|
8
|
+
* with the License. You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing,
|
|
13
|
+
* software distributed under the License is distributed on an
|
|
14
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
* KIND, either express or implied. See the License for the
|
|
16
|
+
* specific language governing permissions and limitations
|
|
17
|
+
* under the License.
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
import { open, readFile, rm } from 'node:fs/promises';
|
|
21
|
+
import { isNonEmptyUnicodeString, isSha256Digest, } from './session-bundle-contract.js';
|
|
22
|
+
const MAX_IDENTIFIER_LENGTH = 512;
|
|
23
|
+
const MAX_OBJECT_REF_LENGTH = 2_048;
|
|
24
|
+
export const SESSION_BUNDLE_OBJECT_MEDIA_TYPE = 'application/vnd.maka.session-bundle+tar;version=1;compression=zstd';
|
|
25
|
+
export const SESSION_CHECKPOINT_MANIFEST_MEDIA_TYPE = 'application/vnd.maka.session-checkpoint-manifest+json;version=1';
|
|
26
|
+
export class SessionRepositoryError extends Error {
|
|
27
|
+
code;
|
|
28
|
+
constructor(code, message, options = {}) {
|
|
29
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.name = 'SessionRepositoryError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Publishes and verifies the compatibility Bundle before publishing and
|
|
36
|
+
* verifying the immutable Manifest that names it. The returned checkpoint is
|
|
37
|
+
* suitable for a later Repository create or head-CAS operation.
|
|
38
|
+
*/
|
|
39
|
+
export async function publishSessionCheckpointV1(input) {
|
|
40
|
+
if (!isRecord(input))
|
|
41
|
+
throw new TypeError('Session checkpoint publication input must be an object');
|
|
42
|
+
const objectStore = requireImmutableObjectStore(input.objectStore);
|
|
43
|
+
const artifact = admitSessionBundleArtifact(input.compatibilityBundle);
|
|
44
|
+
const compatibilityBundle = await publishVerifiedObject(objectStore, {
|
|
45
|
+
digest: artifact.archiveDigest,
|
|
46
|
+
bytes: artifact.compressedBytes,
|
|
47
|
+
mediaType: SESSION_BUNDLE_OBJECT_MEDIA_TYPE,
|
|
48
|
+
source: { kind: 'file', path: artifact.path },
|
|
49
|
+
});
|
|
50
|
+
const value = createSessionCheckpointManifestV1(compatibilityBundle);
|
|
51
|
+
const manifestBytes = encodeSessionCheckpointManifestV1(value);
|
|
52
|
+
const manifest = await publishVerifiedObject(objectStore, {
|
|
53
|
+
digest: digestBytes(manifestBytes),
|
|
54
|
+
bytes: manifestBytes.byteLength,
|
|
55
|
+
mediaType: SESSION_CHECKPOINT_MANIFEST_MEDIA_TYPE,
|
|
56
|
+
source: { kind: 'bytes', value: manifestBytes },
|
|
57
|
+
});
|
|
58
|
+
return storedSessionCheckpoint({ manifest, value });
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Converts a retained V1 compatibility Bundle reference into the exact
|
|
62
|
+
* filesystem source expected by the Bundle inspect/hydrate boundary.
|
|
63
|
+
*
|
|
64
|
+
* This is deliberately a materialization seam, not a general Repository read:
|
|
65
|
+
* callers own the bounded destination and clean it up after hydration/repack.
|
|
66
|
+
*/
|
|
67
|
+
export async function materializeSessionCheckpointV1(input) {
|
|
68
|
+
if (!isRecord(input))
|
|
69
|
+
throw new TypeError('Session checkpoint materialization input must be an object');
|
|
70
|
+
const objectStore = requireImmutableObjectStore(input.objectStore);
|
|
71
|
+
const checkpoint = admitStoredSessionCheckpoint(input.checkpoint);
|
|
72
|
+
const request = admitImmutableObjectMaterializationInput({
|
|
73
|
+
ref: checkpoint.value.compatibilityBundle,
|
|
74
|
+
destination: input.destination,
|
|
75
|
+
maxBytes: input.maxBytes,
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
await objectStore.assertReadable(checkpoint.manifest);
|
|
79
|
+
await objectStore.assertReadable(request.ref);
|
|
80
|
+
await objectStore.materialize(request);
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
path: request.destination,
|
|
83
|
+
expectedArchiveDigest: request.ref.digest,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
throw normalizeObjectStoreError(error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function createSessionCheckpointManifestV1(compatibilityBundle) {
|
|
91
|
+
const admitted = admitImmutableObjectRef(compatibilityBundle);
|
|
92
|
+
if (admitted.mediaType !== SESSION_BUNDLE_OBJECT_MEDIA_TYPE) {
|
|
93
|
+
throw new TypeError('V1 compatibility Bundle has an unsupported media type');
|
|
94
|
+
}
|
|
95
|
+
return Object.freeze({
|
|
96
|
+
schemaVersion: 1,
|
|
97
|
+
compatibilityBundle: copyImmutableObjectRef(admitted),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** RFC 8785/JCS V1 encoding used to bind a Manifest value to its object digest. */
|
|
101
|
+
export function encodeSessionCheckpointManifestV1(input) {
|
|
102
|
+
const value = admitSessionCheckpointManifestV1(input);
|
|
103
|
+
return new TextEncoder().encode(JSON.stringify({
|
|
104
|
+
compatibilityBundle: {
|
|
105
|
+
bytes: value.compatibilityBundle.bytes,
|
|
106
|
+
digest: value.compatibilityBundle.digest,
|
|
107
|
+
mediaType: value.compatibilityBundle.mediaType,
|
|
108
|
+
objectRef: value.compatibilityBundle.objectRef,
|
|
109
|
+
},
|
|
110
|
+
schemaVersion: value.schemaVersion,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
export function createInMemoryImmutableObjectStore() {
|
|
114
|
+
return new InMemoryImmutableObjectStore();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Deterministic conformance implementation for coordinators and Fork tests.
|
|
118
|
+
* It models the V1 single-current-head retention policy, but it is not a
|
|
119
|
+
* durable control-plane backend.
|
|
120
|
+
*/
|
|
121
|
+
export function createInMemorySessionRepository(options) {
|
|
122
|
+
if (!isRecord(options))
|
|
123
|
+
throw new TypeError('In-memory Repository options must be an object');
|
|
124
|
+
return new InMemorySessionRepository(requireImmutableObjectStore(options.objectStore));
|
|
125
|
+
}
|
|
126
|
+
class InMemorySessionRepository {
|
|
127
|
+
objectStore;
|
|
128
|
+
forkIdempotencyRetention = 'indefinite';
|
|
129
|
+
sessions = new Map();
|
|
130
|
+
commitsBySession = new Map();
|
|
131
|
+
forks = new Map();
|
|
132
|
+
constructor(objectStore) {
|
|
133
|
+
this.objectStore = objectStore;
|
|
134
|
+
}
|
|
135
|
+
async checkoutCurrent(sessionId) {
|
|
136
|
+
const admittedSessionId = requireIdentifier(sessionId, 'Session identity');
|
|
137
|
+
const session = this.sessions.get(admittedSessionId);
|
|
138
|
+
if (!session)
|
|
139
|
+
throw repositoryError('session_not_found', 'Cloud Session was not found');
|
|
140
|
+
// Capture exactly the current head selected by this read before awaiting
|
|
141
|
+
// object verification. A subsequent writer cannot substitute its head.
|
|
142
|
+
const committed = copyCommittedSessionRevision(session.head);
|
|
143
|
+
await this.assertCheckpointReadable(committed.checkpoint);
|
|
144
|
+
return committed;
|
|
145
|
+
}
|
|
146
|
+
async checkoutExact(ref) {
|
|
147
|
+
const requested = admitRevisionRef(ref, 'Session revision reference');
|
|
148
|
+
const session = this.sessions.get(requested.sessionId);
|
|
149
|
+
if (!session)
|
|
150
|
+
throw repositoryError('session_not_found', 'Cloud Session was not found');
|
|
151
|
+
if (session.head.ref.revision !== requested.revision) {
|
|
152
|
+
throw repositoryError('revision_not_available', 'Requested Session revision is not available');
|
|
153
|
+
}
|
|
154
|
+
// Capture the exact record before asynchronous object reads. A later head
|
|
155
|
+
// advance may make this revision non-current, but cannot substitute bytes.
|
|
156
|
+
const committed = copyCommittedSessionRevision(session.head);
|
|
157
|
+
await this.assertCheckpointReadable(committed.checkpoint);
|
|
158
|
+
return committed;
|
|
159
|
+
}
|
|
160
|
+
async commit(input) {
|
|
161
|
+
const admitted = admitCommitSessionRevisionInput(input);
|
|
162
|
+
const prior = this.reconcilePriorCommit(admitted);
|
|
163
|
+
if (prior) {
|
|
164
|
+
await this.assertCheckpointReadable(prior.checkpoint);
|
|
165
|
+
return prior;
|
|
166
|
+
}
|
|
167
|
+
const initial = this.sessions.get(admitted.sessionId);
|
|
168
|
+
if (!initial)
|
|
169
|
+
throw repositoryError('session_not_found', 'Cloud Session was not found');
|
|
170
|
+
if (initial.head.ref.revision !== admitted.expectedRevision) {
|
|
171
|
+
throw repositoryError('revision_conflict', 'Cloud Session head changed before commit');
|
|
172
|
+
}
|
|
173
|
+
await this.assertCheckpointReadable(admitted.checkpoint);
|
|
174
|
+
// Object verification may yield. Reconcile an identical concurrent retry,
|
|
175
|
+
// then recheck CAS so another commit cannot be overwritten.
|
|
176
|
+
const admittedWhileReading = this.reconcilePriorCommit(admitted);
|
|
177
|
+
if (admittedWhileReading)
|
|
178
|
+
return admittedWhileReading;
|
|
179
|
+
const session = this.sessions.get(admitted.sessionId);
|
|
180
|
+
if (!session)
|
|
181
|
+
throw repositoryError('session_not_found', 'Cloud Session was not found');
|
|
182
|
+
if (session.head.ref.revision !== admitted.expectedRevision) {
|
|
183
|
+
throw repositoryError('revision_conflict', 'Cloud Session head changed before commit');
|
|
184
|
+
}
|
|
185
|
+
const result = committedRevision({
|
|
186
|
+
sessionId: admitted.sessionId,
|
|
187
|
+
revision: nextRevision(session),
|
|
188
|
+
agentId: session.agentId,
|
|
189
|
+
checkpoint: admitted.checkpoint,
|
|
190
|
+
lastCommittedActivationId: admitted.lastCommittedActivationId,
|
|
191
|
+
forkedFrom: session.forkedFrom,
|
|
192
|
+
});
|
|
193
|
+
session.head = result;
|
|
194
|
+
if (admitted.commitId !== undefined) {
|
|
195
|
+
let records = this.commitsBySession.get(admitted.sessionId);
|
|
196
|
+
if (!records) {
|
|
197
|
+
records = new Map();
|
|
198
|
+
this.commitsBySession.set(admitted.sessionId, records);
|
|
199
|
+
}
|
|
200
|
+
records.set(admitted.commitId, { input: admitted, result });
|
|
201
|
+
}
|
|
202
|
+
return copyCommittedSessionRevision(result);
|
|
203
|
+
}
|
|
204
|
+
async createSession(input) {
|
|
205
|
+
const admitted = admitCreateSessionInput(input);
|
|
206
|
+
const existing = this.sessions.get(admitted.sessionId);
|
|
207
|
+
if (existing) {
|
|
208
|
+
const reconciled = this.reconcileExistingSessionCreate(existing, admitted);
|
|
209
|
+
await this.assertCheckpointReadable(reconciled.checkpoint);
|
|
210
|
+
return reconciled;
|
|
211
|
+
}
|
|
212
|
+
await this.assertCheckpointReadable(admitted.checkpoint);
|
|
213
|
+
// Object verification may yield. Create-if-absent is decided only after it
|
|
214
|
+
// returns, at this method's synchronous linearization point.
|
|
215
|
+
const afterVerification = this.sessions.get(admitted.sessionId);
|
|
216
|
+
if (afterVerification) {
|
|
217
|
+
const reconciled = this.reconcileExistingSessionCreate(afterVerification, admitted);
|
|
218
|
+
await this.assertCheckpointReadable(reconciled.checkpoint);
|
|
219
|
+
return reconciled;
|
|
220
|
+
}
|
|
221
|
+
if (admitted.createdByForkId !== undefined)
|
|
222
|
+
this.assertPendingForkCreate(admitted);
|
|
223
|
+
const initial = committedRevision({
|
|
224
|
+
sessionId: admitted.sessionId,
|
|
225
|
+
revision: 'r1',
|
|
226
|
+
agentId: admitted.agentId,
|
|
227
|
+
checkpoint: admitted.checkpoint,
|
|
228
|
+
lastCommittedActivationId: admitted.lastCommittedActivationId,
|
|
229
|
+
forkedFrom: admitted.forkedFrom,
|
|
230
|
+
});
|
|
231
|
+
this.sessions.set(admitted.sessionId, {
|
|
232
|
+
agentId: admitted.agentId,
|
|
233
|
+
head: initial,
|
|
234
|
+
nextRevisionNumber: 2,
|
|
235
|
+
forkedFrom: admitted.forkedFrom,
|
|
236
|
+
createdByForkId: admitted.createdByForkId,
|
|
237
|
+
createdRevision: initial,
|
|
238
|
+
});
|
|
239
|
+
return copyCommittedSessionRevision(initial);
|
|
240
|
+
}
|
|
241
|
+
async claimFork(input) {
|
|
242
|
+
const admitted = admitClaimForkInput(input);
|
|
243
|
+
const existing = this.forks.get(admitted.forkId);
|
|
244
|
+
if (existing) {
|
|
245
|
+
if (!sameForkClaim(existing, admitted)) {
|
|
246
|
+
throw repositoryError('idempotency_conflict', 'Fork identity was reused with different input');
|
|
247
|
+
}
|
|
248
|
+
return copyForkOperation(existing);
|
|
249
|
+
}
|
|
250
|
+
if (admitted.targetSessionId === admitted.source.sessionId) {
|
|
251
|
+
throw repositoryError('invalid_fork_target', 'Fork target Session must differ from its source Session');
|
|
252
|
+
}
|
|
253
|
+
// The first claim is the linearization point for the source binding. It
|
|
254
|
+
// must prove the named revision is still current and readable before a
|
|
255
|
+
// retry can rely on this durable Fork record.
|
|
256
|
+
const initial = this.sessions.get(admitted.source.sessionId);
|
|
257
|
+
if (!initial || initial.head.ref.revision !== admitted.source.revision) {
|
|
258
|
+
throw repositoryError('source_revision_not_available', 'Fork source Session revision is not available');
|
|
259
|
+
}
|
|
260
|
+
const source = copyCommittedSessionRevision(initial.head);
|
|
261
|
+
await this.assertCheckpointReadable(source.checkpoint);
|
|
262
|
+
// Source verification may yield. Reconcile another claimant that won while
|
|
263
|
+
// it was in progress rather than overwriting its durable idempotency fact.
|
|
264
|
+
const claimedWhileReading = this.forks.get(admitted.forkId);
|
|
265
|
+
if (claimedWhileReading) {
|
|
266
|
+
if (!sameForkClaim(claimedWhileReading, admitted)) {
|
|
267
|
+
throw repositoryError('idempotency_conflict', 'Fork identity was reused with different input');
|
|
268
|
+
}
|
|
269
|
+
return copyForkOperation(claimedWhileReading);
|
|
270
|
+
}
|
|
271
|
+
// Only a new claim needs a current source. Keep this final check and the
|
|
272
|
+
// insertion synchronous, after reconciling any already admitted operation.
|
|
273
|
+
const afterVerification = this.sessions.get(admitted.source.sessionId);
|
|
274
|
+
if (!afterVerification ||
|
|
275
|
+
afterVerification.head.ref.revision !== admitted.source.revision ||
|
|
276
|
+
afterVerification.agentId !== source.agentId) {
|
|
277
|
+
throw repositoryError('source_revision_not_available', 'Fork source Session revision is no longer current');
|
|
278
|
+
}
|
|
279
|
+
const pending = {
|
|
280
|
+
state: 'pending',
|
|
281
|
+
forkId: admitted.forkId,
|
|
282
|
+
source: admitted.source,
|
|
283
|
+
sourceAgentId: source.agentId,
|
|
284
|
+
sourceCheckpoint: source.checkpoint,
|
|
285
|
+
targetSessionId: admitted.targetSessionId,
|
|
286
|
+
};
|
|
287
|
+
this.forks.set(admitted.forkId, pending);
|
|
288
|
+
return copyForkOperation(pending);
|
|
289
|
+
}
|
|
290
|
+
async completeFork(input) {
|
|
291
|
+
const forkId = requireIdentifier(input?.forkId, 'Fork identity');
|
|
292
|
+
const operation = this.forks.get(forkId);
|
|
293
|
+
if (!operation)
|
|
294
|
+
throw repositoryError('idempotency_conflict', 'Fork identity was not claimed');
|
|
295
|
+
if (operation.state === 'completed')
|
|
296
|
+
return copyCompletedForkOperation(operation);
|
|
297
|
+
const target = this.sessions.get(operation.targetSessionId);
|
|
298
|
+
if (!target)
|
|
299
|
+
throw repositoryError('session_not_found', 'Fork target Session was not found');
|
|
300
|
+
if (target.agentId !== operation.sourceAgentId) {
|
|
301
|
+
throw repositoryError('fork_agent_mismatch', 'Fork target Agent does not match its verified source Agent');
|
|
302
|
+
}
|
|
303
|
+
if (target.createdByForkId !== operation.forkId ||
|
|
304
|
+
!target.forkedFrom ||
|
|
305
|
+
!sameRevisionRef(target.forkedFrom, operation.source)) {
|
|
306
|
+
throw repositoryError('idempotency_conflict', 'Fork target was created by another operation');
|
|
307
|
+
}
|
|
308
|
+
await this.assertCheckpointReadable(target.createdRevision.checkpoint);
|
|
309
|
+
const completed = {
|
|
310
|
+
...operation,
|
|
311
|
+
state: 'completed',
|
|
312
|
+
target: copyRevisionRef(target.createdRevision.ref),
|
|
313
|
+
};
|
|
314
|
+
this.forks.set(forkId, completed);
|
|
315
|
+
return copyCompletedForkOperation(completed);
|
|
316
|
+
}
|
|
317
|
+
reconcilePriorCommit(input) {
|
|
318
|
+
if (input.commitId === undefined)
|
|
319
|
+
return undefined;
|
|
320
|
+
const prior = this.commitsBySession.get(input.sessionId)?.get(input.commitId);
|
|
321
|
+
if (!prior)
|
|
322
|
+
return undefined;
|
|
323
|
+
if (!sameCommitInput(prior.input, input)) {
|
|
324
|
+
throw repositoryError('idempotency_conflict', 'Commit identity was reused with different input');
|
|
325
|
+
}
|
|
326
|
+
return copyCommittedSessionRevision(prior.result);
|
|
327
|
+
}
|
|
328
|
+
reconcileExistingSessionCreate(existing, input) {
|
|
329
|
+
if (input.createdByForkId !== undefined &&
|
|
330
|
+
existing.createdByForkId === input.createdByForkId &&
|
|
331
|
+
sameCreateInput(existing.createdRevision, existing.agentId, existing.forkedFrom, input)) {
|
|
332
|
+
return copyCommittedSessionRevision(existing.createdRevision);
|
|
333
|
+
}
|
|
334
|
+
throw repositoryError('session_already_exists', 'Cloud Session already exists');
|
|
335
|
+
}
|
|
336
|
+
assertPendingForkCreate(input) {
|
|
337
|
+
const operation = this.forks.get(input.createdByForkId);
|
|
338
|
+
if (!operation ||
|
|
339
|
+
operation.state !== 'pending' ||
|
|
340
|
+
operation.targetSessionId !== input.sessionId ||
|
|
341
|
+
!input.forkedFrom ||
|
|
342
|
+
!sameRevisionRef(operation.source, input.forkedFrom)) {
|
|
343
|
+
throw repositoryError('idempotency_conflict', 'Fork target does not match its claimed operation');
|
|
344
|
+
}
|
|
345
|
+
if (operation.sourceAgentId !== input.agentId) {
|
|
346
|
+
throw repositoryError('fork_agent_mismatch', 'Fork target Agent must match its verified source Agent');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async assertCheckpointReadable(input) {
|
|
350
|
+
const checkpoint = admitStoredSessionCheckpoint(input);
|
|
351
|
+
try {
|
|
352
|
+
await this.objectStore.assertReadable(checkpoint.manifest);
|
|
353
|
+
await this.objectStore.assertReadable(checkpoint.value.compatibilityBundle);
|
|
354
|
+
return checkpoint;
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
throw normalizeObjectStoreError(error);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
class InMemoryImmutableObjectStore {
|
|
362
|
+
objects = new Map();
|
|
363
|
+
async publish(input) {
|
|
364
|
+
const admitted = admitImmutableObjectInput(input);
|
|
365
|
+
let bytes;
|
|
366
|
+
try {
|
|
367
|
+
bytes =
|
|
368
|
+
admitted.source.kind === 'file'
|
|
369
|
+
? await readFile(admitted.source.path)
|
|
370
|
+
: Uint8Array.from(admitted.source.value);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
throw repositoryError('io_failure', 'Immutable object publication could not read bytes', error);
|
|
374
|
+
}
|
|
375
|
+
if (bytes.byteLength !== admitted.bytes || digestBytes(bytes) !== admitted.digest) {
|
|
376
|
+
throw repositoryError('integrity_mismatch', 'Immutable object bytes do not match declared metadata');
|
|
377
|
+
}
|
|
378
|
+
const mediaTypeKey = digestBytes(new TextEncoder().encode(admitted.mediaType)).slice('sha256:'.length, 'sha256:'.length + 16);
|
|
379
|
+
const ref = immutableObjectRef({
|
|
380
|
+
objectRef: `memory://immutable-objects/${admitted.digest.slice('sha256:'.length)}/${mediaTypeKey}`,
|
|
381
|
+
digest: admitted.digest,
|
|
382
|
+
bytes: admitted.bytes,
|
|
383
|
+
mediaType: admitted.mediaType,
|
|
384
|
+
});
|
|
385
|
+
const existing = this.objects.get(ref.objectRef);
|
|
386
|
+
if (existing) {
|
|
387
|
+
if (!sameImmutableObjectRef(existing.ref, ref) || !sameBytes(existing.bytes, bytes)) {
|
|
388
|
+
throw repositoryError('integrity_mismatch', 'Immutable object reference already contains different bytes or metadata');
|
|
389
|
+
}
|
|
390
|
+
return copyImmutableObjectRef(existing.ref);
|
|
391
|
+
}
|
|
392
|
+
this.objects.set(ref.objectRef, { ref, bytes: Uint8Array.from(bytes) });
|
|
393
|
+
return copyImmutableObjectRef(ref);
|
|
394
|
+
}
|
|
395
|
+
async assertReadable(input) {
|
|
396
|
+
const ref = admitImmutableObjectRef(input);
|
|
397
|
+
const stored = this.objects.get(ref.objectRef);
|
|
398
|
+
if (!stored)
|
|
399
|
+
throw repositoryError('object_not_found', 'Immutable object was not found');
|
|
400
|
+
if (!sameImmutableObjectRef(stored.ref, ref) ||
|
|
401
|
+
stored.bytes.byteLength !== ref.bytes ||
|
|
402
|
+
digestBytes(stored.bytes) !== ref.digest) {
|
|
403
|
+
throw repositoryError('integrity_mismatch', 'Immutable object bytes no longer match their trusted metadata');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async materialize(input) {
|
|
407
|
+
const request = admitImmutableObjectMaterializationInput(input);
|
|
408
|
+
const stored = this.objects.get(request.ref.objectRef);
|
|
409
|
+
if (!stored)
|
|
410
|
+
throw repositoryError('object_not_found', 'Immutable object was not found');
|
|
411
|
+
if (!sameImmutableObjectRef(stored.ref, request.ref) ||
|
|
412
|
+
stored.bytes.byteLength !== request.ref.bytes ||
|
|
413
|
+
digestBytes(stored.bytes) !== request.ref.digest) {
|
|
414
|
+
throw repositoryError('integrity_mismatch', 'Immutable object bytes no longer match their trusted metadata');
|
|
415
|
+
}
|
|
416
|
+
if (request.ref.bytes > request.maxBytes) {
|
|
417
|
+
throw repositoryError('quota_exceeded', 'Immutable object exceeds materialization byte limit');
|
|
418
|
+
}
|
|
419
|
+
let handle;
|
|
420
|
+
try {
|
|
421
|
+
handle = await open(request.destination, 'wx', 0o600);
|
|
422
|
+
await handle.writeFile(stored.bytes);
|
|
423
|
+
await handle.close();
|
|
424
|
+
handle = undefined;
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
// An exclusive-open failure gives us no ownership of the existing path.
|
|
428
|
+
if (handle) {
|
|
429
|
+
await handle.close().catch(() => { });
|
|
430
|
+
await rm(request.destination, { force: true }).catch(() => { });
|
|
431
|
+
}
|
|
432
|
+
throw repositoryError('io_failure', 'Immutable object could not be materialized', error);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function publishVerifiedObject(objectStore, input) {
|
|
437
|
+
const expected = admitImmutableObjectInput(input);
|
|
438
|
+
try {
|
|
439
|
+
const published = admitImmutableObjectRef(await objectStore.publish(expected));
|
|
440
|
+
if (published.digest !== expected.digest ||
|
|
441
|
+
published.bytes !== expected.bytes ||
|
|
442
|
+
published.mediaType !== expected.mediaType) {
|
|
443
|
+
throw repositoryError('integrity_mismatch', 'Published immutable object metadata does not match its input');
|
|
444
|
+
}
|
|
445
|
+
await objectStore.assertReadable(published);
|
|
446
|
+
return copyImmutableObjectRef(published);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
throw normalizeObjectStoreError(error);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function admitSessionBundleArtifact(input) {
|
|
453
|
+
if (!isRecord(input))
|
|
454
|
+
throw new TypeError('Session Bundle artifact must be an object');
|
|
455
|
+
const path = requireIdentifier(input.path, 'Bundle archive path', MAX_OBJECT_REF_LENGTH);
|
|
456
|
+
if (!isSha256Digest(input.archiveDigest)) {
|
|
457
|
+
throw new TypeError('Bundle archive digest must be SHA-256');
|
|
458
|
+
}
|
|
459
|
+
if (!isByteCount(input.compressedBytes)) {
|
|
460
|
+
throw new TypeError('Bundle compressed byte count must be a non-negative safe integer');
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
...input,
|
|
464
|
+
path,
|
|
465
|
+
archiveDigest: input.archiveDigest,
|
|
466
|
+
compressedBytes: input.compressedBytes,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function admitImmutableObjectInput(input) {
|
|
470
|
+
if (!isRecord(input))
|
|
471
|
+
throw new TypeError('Immutable object input must be an object');
|
|
472
|
+
if (!isSha256Digest(input.digest))
|
|
473
|
+
throw new TypeError('Immutable object digest must be SHA-256');
|
|
474
|
+
if (!isByteCount(input.bytes)) {
|
|
475
|
+
throw new TypeError('Immutable object byte count must be a non-negative safe integer');
|
|
476
|
+
}
|
|
477
|
+
const mediaType = requireIdentifier(input.mediaType, 'Immutable object media type');
|
|
478
|
+
if (!isRecord(input.source))
|
|
479
|
+
throw new TypeError('Immutable object source must be an object');
|
|
480
|
+
if (input.source.kind === 'file') {
|
|
481
|
+
return Object.freeze({
|
|
482
|
+
digest: input.digest,
|
|
483
|
+
bytes: input.bytes,
|
|
484
|
+
mediaType,
|
|
485
|
+
source: Object.freeze({
|
|
486
|
+
kind: 'file',
|
|
487
|
+
path: requireIdentifier(input.source.path, 'Immutable object source path', MAX_OBJECT_REF_LENGTH),
|
|
488
|
+
}),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
if (input.source.kind === 'bytes' && input.source.value instanceof Uint8Array) {
|
|
492
|
+
return Object.freeze({
|
|
493
|
+
digest: input.digest,
|
|
494
|
+
bytes: input.bytes,
|
|
495
|
+
mediaType,
|
|
496
|
+
source: Object.freeze({ kind: 'bytes', value: Uint8Array.from(input.source.value) }),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
throw new TypeError('Immutable object source must contain file or byte content');
|
|
500
|
+
}
|
|
501
|
+
function admitImmutableObjectMaterializationInput(input) {
|
|
502
|
+
if (!isRecord(input))
|
|
503
|
+
throw new TypeError('Immutable object materialization input must be an object');
|
|
504
|
+
if (!isByteCount(input.maxBytes)) {
|
|
505
|
+
throw new TypeError('Immutable object materialization byte limit must be a non-negative safe integer');
|
|
506
|
+
}
|
|
507
|
+
return Object.freeze({
|
|
508
|
+
ref: admitImmutableObjectRef(input.ref),
|
|
509
|
+
destination: requireIdentifier(input.destination, 'Immutable object materialization destination', MAX_OBJECT_REF_LENGTH),
|
|
510
|
+
maxBytes: input.maxBytes,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function admitImmutableObjectRef(input) {
|
|
514
|
+
if (!isRecord(input))
|
|
515
|
+
throw new TypeError('Immutable object reference must be an object');
|
|
516
|
+
const objectRef = requireIdentifier(input.objectRef, 'Immutable object reference', MAX_OBJECT_REF_LENGTH);
|
|
517
|
+
if (!isSha256Digest(input.digest))
|
|
518
|
+
throw new TypeError('Immutable object digest must be SHA-256');
|
|
519
|
+
if (!isByteCount(input.bytes)) {
|
|
520
|
+
throw new TypeError('Immutable object byte count must be a non-negative safe integer');
|
|
521
|
+
}
|
|
522
|
+
return immutableObjectRef({
|
|
523
|
+
objectRef,
|
|
524
|
+
digest: input.digest,
|
|
525
|
+
bytes: input.bytes,
|
|
526
|
+
mediaType: requireIdentifier(input.mediaType, 'Immutable object media type'),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
function admitSessionCheckpointManifestV1(input) {
|
|
530
|
+
if (!isRecord(input))
|
|
531
|
+
throw new TypeError('Session checkpoint Manifest must be an object');
|
|
532
|
+
if (input.schemaVersion !== 1) {
|
|
533
|
+
throw new TypeError('Session checkpoint Manifest schema version must be 1');
|
|
534
|
+
}
|
|
535
|
+
return createSessionCheckpointManifestV1(input.compatibilityBundle);
|
|
536
|
+
}
|
|
537
|
+
function admitStoredSessionCheckpoint(input) {
|
|
538
|
+
if (!isRecord(input))
|
|
539
|
+
throw new TypeError('Stored Session checkpoint must be an object');
|
|
540
|
+
const manifest = admitImmutableObjectRef(input.manifest);
|
|
541
|
+
const value = admitSessionCheckpointManifestV1(input.value);
|
|
542
|
+
if (manifest.mediaType !== SESSION_CHECKPOINT_MANIFEST_MEDIA_TYPE) {
|
|
543
|
+
throw new TypeError('Session checkpoint Manifest has an unsupported media type');
|
|
544
|
+
}
|
|
545
|
+
const encoded = encodeSessionCheckpointManifestV1(value);
|
|
546
|
+
if (manifest.digest !== digestBytes(encoded) || manifest.bytes !== encoded.byteLength) {
|
|
547
|
+
throw repositoryError('integrity_mismatch', 'Session checkpoint Manifest value does not match its immutable reference');
|
|
548
|
+
}
|
|
549
|
+
return storedSessionCheckpoint({ manifest, value });
|
|
550
|
+
}
|
|
551
|
+
function admitCommitSessionRevisionInput(input) {
|
|
552
|
+
if (!isRecord(input))
|
|
553
|
+
throw new TypeError('Session commit input must be an object');
|
|
554
|
+
return Object.freeze({
|
|
555
|
+
sessionId: requireIdentifier(input.sessionId, 'Session identity'),
|
|
556
|
+
expectedRevision: requireIdentifier(input.expectedRevision, 'Expected revision'),
|
|
557
|
+
checkpoint: admitStoredSessionCheckpoint(input.checkpoint),
|
|
558
|
+
...(input.lastCommittedActivationId === undefined
|
|
559
|
+
? {}
|
|
560
|
+
: {
|
|
561
|
+
lastCommittedActivationId: requireIdentifier(input.lastCommittedActivationId, 'Activation identity'),
|
|
562
|
+
}),
|
|
563
|
+
...(input.commitId === undefined
|
|
564
|
+
? {}
|
|
565
|
+
: { commitId: requireIdentifier(input.commitId, 'Commit identity') }),
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
function admitCreateSessionInput(input) {
|
|
569
|
+
if (!isRecord(input))
|
|
570
|
+
throw new TypeError('Session creation input must be an object');
|
|
571
|
+
const forkedFrom = input.forkedFrom === undefined ? undefined : admitRevisionRef(input.forkedFrom, 'Fork source');
|
|
572
|
+
const createdByForkId = input.createdByForkId === undefined
|
|
573
|
+
? undefined
|
|
574
|
+
: requireIdentifier(input.createdByForkId, 'Fork identity');
|
|
575
|
+
if ((forkedFrom === undefined) !== (createdByForkId === undefined)) {
|
|
576
|
+
throw new TypeError('Fork lineage and Fork identity must be supplied together');
|
|
577
|
+
}
|
|
578
|
+
if (createdByForkId !== undefined && input.lastCommittedActivationId !== undefined) {
|
|
579
|
+
throw new TypeError('Fork-created Session must not carry an Activation identity');
|
|
580
|
+
}
|
|
581
|
+
return Object.freeze({
|
|
582
|
+
sessionId: requireIdentifier(input.sessionId, 'Session identity'),
|
|
583
|
+
agentId: requireIdentifier(input.agentId, 'Agent identity'),
|
|
584
|
+
checkpoint: admitStoredSessionCheckpoint(input.checkpoint),
|
|
585
|
+
...(input.lastCommittedActivationId === undefined
|
|
586
|
+
? {}
|
|
587
|
+
: {
|
|
588
|
+
lastCommittedActivationId: requireIdentifier(input.lastCommittedActivationId, 'Activation identity'),
|
|
589
|
+
}),
|
|
590
|
+
...(forkedFrom === undefined ? {} : { forkedFrom }),
|
|
591
|
+
...(createdByForkId === undefined ? {} : { createdByForkId }),
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
function admitClaimForkInput(input) {
|
|
595
|
+
if (!isRecord(input))
|
|
596
|
+
throw new TypeError('Fork claim input must be an object');
|
|
597
|
+
return Object.freeze({
|
|
598
|
+
forkId: requireIdentifier(input.forkId, 'Fork identity'),
|
|
599
|
+
source: admitRevisionRef(input.source, 'Fork source'),
|
|
600
|
+
targetSessionId: requireIdentifier(input.targetSessionId, 'Fork target Session identity'),
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
function admitRevisionRef(input, label) {
|
|
604
|
+
if (!isRecord(input))
|
|
605
|
+
throw new TypeError(`${label} must be an object`);
|
|
606
|
+
return copyRevisionRef({
|
|
607
|
+
sessionId: requireIdentifier(input.sessionId, `${label} Session identity`),
|
|
608
|
+
revision: requireIdentifier(input.revision, `${label} revision`),
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
function committedRevision(input) {
|
|
612
|
+
return Object.freeze({
|
|
613
|
+
ref: copyRevisionRef({ sessionId: input.sessionId, revision: input.revision }),
|
|
614
|
+
agentId: input.agentId,
|
|
615
|
+
checkpoint: copyStoredSessionCheckpoint(input.checkpoint),
|
|
616
|
+
...(input.lastCommittedActivationId === undefined
|
|
617
|
+
? {}
|
|
618
|
+
: { lastCommittedActivationId: input.lastCommittedActivationId }),
|
|
619
|
+
...(input.forkedFrom === undefined ? {} : { forkedFrom: copyRevisionRef(input.forkedFrom) }),
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
function immutableObjectRef(input) {
|
|
623
|
+
return Object.freeze({
|
|
624
|
+
objectRef: input.objectRef,
|
|
625
|
+
digest: input.digest,
|
|
626
|
+
bytes: input.bytes,
|
|
627
|
+
mediaType: input.mediaType,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
function copyImmutableObjectRef(input) {
|
|
631
|
+
return immutableObjectRef(input);
|
|
632
|
+
}
|
|
633
|
+
function storedSessionCheckpoint(input) {
|
|
634
|
+
return Object.freeze({
|
|
635
|
+
manifest: copyImmutableObjectRef(input.manifest),
|
|
636
|
+
value: Object.freeze({
|
|
637
|
+
schemaVersion: 1,
|
|
638
|
+
compatibilityBundle: copyImmutableObjectRef(input.value.compatibilityBundle),
|
|
639
|
+
}),
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
function copyStoredSessionCheckpoint(input) {
|
|
643
|
+
return storedSessionCheckpoint(input);
|
|
644
|
+
}
|
|
645
|
+
function copyRevisionRef(input) {
|
|
646
|
+
return Object.freeze({ sessionId: input.sessionId, revision: input.revision });
|
|
647
|
+
}
|
|
648
|
+
function copyCommittedSessionRevision(input) {
|
|
649
|
+
return committedRevision({
|
|
650
|
+
sessionId: input.ref.sessionId,
|
|
651
|
+
revision: input.ref.revision,
|
|
652
|
+
agentId: input.agentId,
|
|
653
|
+
checkpoint: input.checkpoint,
|
|
654
|
+
...(input.lastCommittedActivationId === undefined
|
|
655
|
+
? {}
|
|
656
|
+
: { lastCommittedActivationId: input.lastCommittedActivationId }),
|
|
657
|
+
...(input.forkedFrom === undefined ? {} : { forkedFrom: input.forkedFrom }),
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
function copyForkOperation(input) {
|
|
661
|
+
if (input.state === 'pending') {
|
|
662
|
+
return Object.freeze({
|
|
663
|
+
state: 'pending',
|
|
664
|
+
forkId: input.forkId,
|
|
665
|
+
source: copyRevisionRef(input.source),
|
|
666
|
+
sourceAgentId: input.sourceAgentId,
|
|
667
|
+
sourceCheckpoint: copyStoredSessionCheckpoint(input.sourceCheckpoint),
|
|
668
|
+
targetSessionId: input.targetSessionId,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
return copyCompletedForkOperation(input);
|
|
672
|
+
}
|
|
673
|
+
function copyCompletedForkOperation(input) {
|
|
674
|
+
return Object.freeze({
|
|
675
|
+
state: 'completed',
|
|
676
|
+
forkId: input.forkId,
|
|
677
|
+
source: copyRevisionRef(input.source),
|
|
678
|
+
sourceAgentId: input.sourceAgentId,
|
|
679
|
+
sourceCheckpoint: copyStoredSessionCheckpoint(input.sourceCheckpoint),
|
|
680
|
+
targetSessionId: input.targetSessionId,
|
|
681
|
+
target: copyRevisionRef(input.target),
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
function sameImmutableObjectRef(left, right) {
|
|
685
|
+
return (left.objectRef === right.objectRef &&
|
|
686
|
+
left.digest === right.digest &&
|
|
687
|
+
left.bytes === right.bytes &&
|
|
688
|
+
left.mediaType === right.mediaType);
|
|
689
|
+
}
|
|
690
|
+
function sameSessionCheckpointManifestV1(left, right) {
|
|
691
|
+
return (left.schemaVersion === right.schemaVersion &&
|
|
692
|
+
sameImmutableObjectRef(left.compatibilityBundle, right.compatibilityBundle));
|
|
693
|
+
}
|
|
694
|
+
function sameStoredSessionCheckpoint(left, right) {
|
|
695
|
+
return (sameImmutableObjectRef(left.manifest, right.manifest) &&
|
|
696
|
+
sameSessionCheckpointManifestV1(left.value, right.value));
|
|
697
|
+
}
|
|
698
|
+
function sameRevisionRef(left, right) {
|
|
699
|
+
return left.sessionId === right.sessionId && left.revision === right.revision;
|
|
700
|
+
}
|
|
701
|
+
function sameCommitInput(left, right) {
|
|
702
|
+
return (left.sessionId === right.sessionId &&
|
|
703
|
+
left.expectedRevision === right.expectedRevision &&
|
|
704
|
+
sameStoredSessionCheckpoint(left.checkpoint, right.checkpoint) &&
|
|
705
|
+
left.lastCommittedActivationId === right.lastCommittedActivationId &&
|
|
706
|
+
left.commitId === right.commitId);
|
|
707
|
+
}
|
|
708
|
+
function sameCreateInput(created, agentId, forkedFrom, input) {
|
|
709
|
+
return (created.agentId === agentId &&
|
|
710
|
+
created.agentId === input.agentId &&
|
|
711
|
+
sameStoredSessionCheckpoint(created.checkpoint, input.checkpoint) &&
|
|
712
|
+
created.lastCommittedActivationId === input.lastCommittedActivationId &&
|
|
713
|
+
sameOptionalRevisionRef(forkedFrom, input.forkedFrom));
|
|
714
|
+
}
|
|
715
|
+
function sameOptionalRevisionRef(left, right) {
|
|
716
|
+
return left === undefined || right === undefined ? left === right : sameRevisionRef(left, right);
|
|
717
|
+
}
|
|
718
|
+
function sameForkClaim(operation, input) {
|
|
719
|
+
return (operation.targetSessionId === input.targetSessionId &&
|
|
720
|
+
sameRevisionRef(operation.source, input.source));
|
|
721
|
+
}
|
|
722
|
+
function nextRevision(session) {
|
|
723
|
+
const revision = `r${session.nextRevisionNumber}`;
|
|
724
|
+
session.nextRevisionNumber += 1;
|
|
725
|
+
return revision;
|
|
726
|
+
}
|
|
727
|
+
function requireIdentifier(value, label, maximumLength = MAX_IDENTIFIER_LENGTH) {
|
|
728
|
+
if (!isNonEmptyUnicodeString(value) || value.length > maximumLength) {
|
|
729
|
+
throw new TypeError(`${label} must be a bounded non-empty Unicode string`);
|
|
730
|
+
}
|
|
731
|
+
return value;
|
|
732
|
+
}
|
|
733
|
+
function requireImmutableObjectStore(value) {
|
|
734
|
+
if (!isRecord(value) ||
|
|
735
|
+
typeof value.publish !== 'function' ||
|
|
736
|
+
typeof value.assertReadable !== 'function' ||
|
|
737
|
+
typeof value.materialize !== 'function') {
|
|
738
|
+
throw new TypeError('Immutable Object Store must implement publish, assertReadable, and materialize');
|
|
739
|
+
}
|
|
740
|
+
return value;
|
|
741
|
+
}
|
|
742
|
+
function isByteCount(value) {
|
|
743
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
744
|
+
}
|
|
745
|
+
function isRecord(value) {
|
|
746
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
747
|
+
}
|
|
748
|
+
function digestBytes(value) {
|
|
749
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
750
|
+
}
|
|
751
|
+
function sameBytes(left, right) {
|
|
752
|
+
if (left.byteLength !== right.byteLength)
|
|
753
|
+
return false;
|
|
754
|
+
for (let index = 0; index < left.byteLength; index += 1) {
|
|
755
|
+
if (left[index] !== right[index])
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
760
|
+
function normalizeObjectStoreError(error) {
|
|
761
|
+
if (error instanceof SessionRepositoryError)
|
|
762
|
+
return error;
|
|
763
|
+
return repositoryError('io_failure', 'Immutable Object Store operation failed', error);
|
|
764
|
+
}
|
|
765
|
+
function repositoryError(code, message, cause) {
|
|
766
|
+
return new SessionRepositoryError(code, message, cause === undefined ? {} : { cause });
|
|
767
|
+
}
|