You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by ku...@apache.org on 2026/09/01 14:35:31 UTC
(maka) 01/02: feat(runtime-host): write Read images to Session context
This is an automated email from the ASF dual-hosted git repository.
likun666661 pushed a commit to branch refactor/read-image-context-writer
in repository https://gitbox.apache.org/repos/asf/maka.git
commit d7b40afe0b4feb94c1e6757ad11d486274e93c8a
Author: likun666661 <90...@users.noreply.github.com>
AuthorDate: Sat Aug 29 22:29:54 2026 +0800
feat(runtime-host): write Read images to Session context
---
.../src/__tests__/execution-composition.test.ts | 4 +-
.../runtime-host/src/__tests__/protocol.test.ts | 4 ++
.../session-retirement-coordinator.test.ts | 13 ++--
packages/runtime-host/src/protocol/index.ts | 3 +-
.../src/server/execution-composition.ts | 69 ++++++++++---------
.../src/server/session-retirement-coordinator.ts | 9 +--
.../src/server/session-revision-coordinator.ts | 32 +++++++++
.../src/server/session-sidecar-purge.ts | 3 +
.../__tests__/builtin-tools-file-worker.test.ts | 15 +++--
.../src/__tests__/conversation-copy.test.ts | 77 +++++++++++-----------
.../src/__tests__/filesystem-authority.test.ts | 2 +-
packages/runtime/src/builtin-tools.ts | 8 +--
packages/runtime/src/conversation-copy.ts | 50 ++++++++++++--
13 files changed, 193 insertions(+), 96 deletions(-)
diff --git a/packages/runtime-host/src/__tests__/execution-composition.test.ts b/packages/runtime-host/src/__tests__/execution-composition.test.ts
index b43bafeccb..a427b76a36 100644
--- a/packages/runtime-host/src/__tests__/execution-composition.test.ts
+++ b/packages/runtime-host/src/__tests__/execution-composition.test.ts
@@ -103,7 +103,7 @@ test('production composition owns the long-term memory database lifecycle', asyn
});
});
-test('production composition reaches Ready when the optional context reader cannot open', async () => {
+test('production composition reaches Ready when the optional context Store cannot open', async () => {
await withCompositionRoot(async ({ root, owner }) => {
await mkdir(join(root, CONTEXT_OFFLOAD_DATABASE_NAME));
const originalConsoleError = console.error;
@@ -114,7 +114,7 @@ test('production composition reaches Ready when the optional context reader cann
composition = await createExecutionRuntimeHostComposition(compositionContext(owner));
assert.equal(composition.workspaceExecution.state, 'ready');
assert.equal(
- diagnostics.some((message) => message.includes('optional context-offload reader')),
+ diagnostics.some((message) => message.includes('optional context-offload Store')),
true,
);
} finally {
diff --git a/packages/runtime-host/src/__tests__/protocol.test.ts b/packages/runtime-host/src/__tests__/protocol.test.ts
index db9f2d70f6..ca477086b0 100644
--- a/packages/runtime-host/src/__tests__/protocol.test.ts
+++ b/packages/runtime-host/src/__tests__/protocol.test.ts
@@ -140,6 +140,10 @@ describe('Runtime Host bootstrap protocol', () => {
assert.ok(RUNTIME_HOST_COMPATIBILITY_EPOCH > 78);
});
+ test('publishes a new compatibility epoch for Read image Session context refs', () => {
+ assert.ok(RUNTIME_HOST_COMPATIBILITY_EPOCH > 87);
+ });
+
test('rejects the legacy connection update result in the current compatibility epoch', () => {
assert.throws(
() =>
diff --git a/packages/runtime-host/src/__tests__/session-retirement-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-retirement-coordinator.test.ts
index affd668029..ed1b35b5db 100644
--- a/packages/runtime-host/src/__tests__/session-retirement-coordinator.test.ts
+++ b/packages/runtime-host/src/__tests__/session-retirement-coordinator.test.ts
@@ -239,7 +239,7 @@ describe('Host Session retirement coordinator', () => {
'parent retirement cleanup did not converge',
);
assert.deepEqual(new Set(harness.actions.purgedArtifacts), new Set(harness.familyIds));
- assert.deepEqual(new Set(harness.actions.checkedContext), new Set(harness.familyIds));
+ assert.deepEqual(new Set(harness.actions.retiredContext), new Set(harness.familyIds));
});
});
@@ -1027,7 +1027,7 @@ interface RetirementActions {
readonly retiredCapabilities: string[];
readonly retiredMessages: string[];
readonly purgedArtifacts: string[];
- readonly checkedContext: string[];
+ readonly retiredContext: string[];
readonly purgedTasks: string[];
readonly purgedOperationalState: string[];
readonly purgedAgentGraphs: string[];
@@ -1066,7 +1066,7 @@ async function withHarness(
retiredCapabilities: [],
retiredMessages: [],
purgedArtifacts: [],
- checkedContext: [],
+ retiredContext: [],
purgedTasks: [],
purgedOperationalState: [],
purgedAgentGraphs: [],
@@ -1232,8 +1232,11 @@ async function withHarness(
actions.purgedTasks.push(sessionId);
},
},
- assertNoContextOffloadReferences: async (sessionIds) => {
- actions.checkedContext.push(...sessionIds);
+ contextOffload: {
+ retireSession: async (sessionId) => {
+ actions.retiredContext.push(sessionId);
+ return { releasedReferences: 0, releasedLogicalBytes: 0 };
+ },
},
purgeOperationalState: async (sessionId) => {
actions.purgedOperationalState.push(sessionId);
diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts
index 8aff63a16a..84dcd3ba91 100644
--- a/packages/runtime-host/src/protocol/index.ts
+++ b/packages/runtime-host/src/protocol/index.ts
@@ -95,7 +95,8 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
// Increment when the same protocol version no longer guarantees safe Client-Host
// interoperability. Mismatches are rejected before domain commands are admitted.
-export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 87 as const;
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 88 as const;
+// 88: Read image tool results may carry durable `session_context` refs.
// 87: The connection catalog projects each model as the Host resolved it —
// a `catalog_entry` item per model, counted by the connection header. Clients
// render those entries instead of merging the stored row against their own
diff --git a/packages/runtime-host/src/server/execution-composition.ts b/packages/runtime-host/src/server/execution-composition.ts
index e5fc5a826f..1297a2cb41 100644
--- a/packages/runtime-host/src/server/execution-composition.ts
+++ b/packages/runtime-host/src/server/execution-composition.ts
@@ -77,14 +77,9 @@ import {
import { type MakaTool } from '@maka/runtime/tool-runtime';
import { type RuntimeHostedRootAuthority } from '@maka/runtime/message-authority';
import { createAgentGraphControlStore } from '@maka/storage/agent-graph-control-store';
-import {
- createArtifactAttachmentResourceReader,
- createReadImageSnapshotter,
-} from '@maka/storage/artifact-stores';
-import {
- isSessionNotFoundError,
- SessionMetadataConflictError,
-} from '@maka/storage/execution-stores';
+import { createArtifactAttachmentResourceReader } from '@maka/storage/artifact-stores';
+import { createReadImageSnapshotStore } from '@maka/storage/read-image-snapshot-store';
+import { isSessionNotFoundError } from '@maka/storage/execution-stores';
import { createExternalSessionAdapterRegistry } from '@maka/storage/external-sessions';
import { createGitWorktreeChildExecutor } from '@maka/storage/git-worktree-child-executor';
import { runWithStorageRootLease } from '@maka/storage/root-authority';
@@ -207,15 +202,16 @@ export interface ExecutionRuntimeHostComposition extends RuntimeHostComposition
readonly plugins: HostPluginPlatform;
}
-const CONTEXT_OFFLOAD_READER_LIMITS: ContextOffloadLimits = Object.freeze({
+const GIBIBYTE = 1024 * 1024 * 1024;
+const CONTEXT_OFFLOAD_LIMITS: ContextOffloadLimits = Object.freeze({
ownerMaxBytes: Object.freeze({
read_image_snapshot: MAX_READ_IMAGE_BYTES,
tool_result_archive: 0,
}),
- // This expand slice opens only the reader path. Zero quotas make accidental
- // non-empty puts fail closed until the writer/lifecycle cutover lands.
- sessionLogicalBytes: 0,
- workspacePhysicalBytes: 0,
+ // Read images are bounded individually and logically per Session. Physical
+ // bytes are content-addressed across Sessions and bounded per workspace.
+ sessionLogicalBytes: GIBIBYTE,
+ workspacePhysicalBytes: 20 * GIBIBYTE,
});
export interface CreateExecutionRuntimeHostCompositionOptions {
@@ -244,7 +240,7 @@ export async function createExecutionRuntimeHostComposition(
dependencies: ExecutionRuntimeHostCompositionDependencies = {},
): Promise<ExecutionRuntimeHostComposition> {
const storage = await openStorageWriterComposition(context.owner.lease, {
- contextOffloadLimits: CONTEXT_OFFLOAD_READER_LIMITS,
+ contextOffloadLimits: CONTEXT_OFFLOAD_LIMITS,
afterRuntimePolicyOpened: async (stores) => {
if (options.bootstrapRuntimePolicy !== false) {
await ensureBootstrapRuntimePolicy({
@@ -260,7 +256,7 @@ export async function createExecutionRuntimeHostComposition(
});
if (storage.contextOffloadUnavailable) {
console.error(
- `[runtime-host] optional context-offload reader could not be opened: ${generalizedErrorMessage(storage.contextOffloadUnavailable.cause)}`,
+ `[runtime-host] optional context-offload Store could not be opened: ${generalizedErrorMessage(storage.contextOffloadUnavailable.cause)}`,
);
}
const stores = storage.execution;
@@ -292,6 +288,17 @@ export async function createExecutionRuntimeHostComposition(
const openedContextOffloadReader = openedContextOffloadStore
? createInteractiveContextOffloadReader(openedContextOffloadStore)
: undefined;
+ const contextOffloadRetirement = openedContextOffloadStore
+ ? openedContextOffloadStore
+ : storage.contextOffloadUnavailable
+ ? {
+ retireSession: async (_sessionId: string): Promise<never> => {
+ throw new Error('Context-offload Store is unavailable during Session retirement', {
+ cause: storage.contextOffloadUnavailable?.cause,
+ });
+ },
+ }
+ : undefined;
const openedUsageStores = storage.usage;
const openedShellRunStore = storage.shellRuns;
const worktreeChildExecutor = createGitWorktreeChildExecutor({
@@ -401,7 +408,21 @@ export async function createExecutionRuntimeHostComposition(
}),
backgroundTasks: runtimeResources,
ptyControls: runtimeResources,
- snapshotImage: createReadImageSnapshotter(openedArtifactStore),
+ ...(openedContextOffloadStore
+ ? {
+ snapshotImage: async (input: {
+ readonly sessionId: string;
+ readonly ownerId: string;
+ readonly bytes: Uint8Array;
+ readonly mimeType: string;
+ }) =>
+ createReadImageSnapshotStore(openedContextOffloadStore, input.sessionId).snapshot({
+ ownerId: input.ownerId,
+ bytes: input.bytes,
+ mimeType: input.mimeType,
+ }),
+ }
+ : {}),
...(sandboxManager ? { sandboxManager } : {}),
...(filesystemWorker ? { filesystemWorker } : {}),
};
@@ -1539,6 +1560,7 @@ export async function createExecutionRuntimeHostComposition(
stores,
artifacts: openedArtifactStore,
sessionTodo: sessionTodoStore,
+ ...(contextOffloadRetirement ? { contextOffload: contextOffloadRetirement } : {}),
manager,
admission: sessionAdmission,
continuity: continuityCoordinator,
@@ -1563,20 +1585,7 @@ export async function createExecutionRuntimeHostComposition(
continuity: continuityCoordinator,
artifacts: openedArtifactStore,
sessionTodo: sessionTodoStore,
- assertNoContextOffloadReferences: async (sessionIds) => {
- if (!openedContextOffloadStore) {
- throw new Error('Context-offload reader is unavailable during Session removal', {
- cause: storage.contextOffloadUnavailable?.cause,
- });
- }
- for (const sessionId of sessionIds) {
- if ((await openedContextOffloadStore.usage(sessionId)).references > 0) {
- throw new SessionMetadataConflictError(
- 'Session removal does not support Session context references yet',
- );
- }
- }
- },
+ ...(contextOffloadRetirement ? { contextOffload: contextOffloadRetirement } : {}),
purgeOperationalState: async (sessionId) => {
await stores.purgeConversationOperationalState(sessionId);
await openedPlanStore.purgeSessionState(sessionId);
diff --git a/packages/runtime-host/src/server/session-retirement-coordinator.ts b/packages/runtime-host/src/server/session-retirement-coordinator.ts
index 72c6e9632f..52b672de96 100644
--- a/packages/runtime-host/src/server/session-retirement-coordinator.ts
+++ b/packages/runtime-host/src/server/session-retirement-coordinator.ts
@@ -32,6 +32,7 @@ import {
} from '@maka/storage/execution-stores';
import { type SessionManager } from '@maka/runtime/session-manager';
import type { InteractiveSessionTodoWriter } from '@maka/storage/session-todo-authority';
+import type { InteractiveContextOffloadWriter } from '@maka/storage/context-offload-store';
import {
type OperationOutcome,
type SessionCatalogItem,
@@ -122,7 +123,7 @@ export interface HostSessionRetirementCoordinatorOptions {
readonly continuity: RetirementContinuity;
readonly artifacts: Pick<InteractiveArtifactStoreWriter, 'purgeSessionArtifacts'>;
readonly sessionTodo: Pick<InteractiveSessionTodoWriter, 'purgeSessionState'>;
- readonly assertNoContextOffloadReferences?: (sessionIds: readonly string[]) => Promise<void>;
+ readonly contextOffload?: Pick<InteractiveContextOffloadWriter, 'retireSession'>;
readonly purgeOperationalState: (sessionId: string) => Promise<void>;
readonly purgeAgentGraphState: (sessionId: string) => Promise<void>;
readonly worktrees?: Pick<SubagentWorktreeExecutor, 'retire'>;
@@ -194,7 +195,7 @@ export class HostSessionRetirementCoordinator {
readonly #continuity: RetirementContinuity;
readonly #artifacts: HostSessionRetirementCoordinatorOptions['artifacts'];
readonly #sessionTodo: HostSessionRetirementCoordinatorOptions['sessionTodo'];
- readonly #assertNoContextOffloadReferences: HostSessionRetirementCoordinatorOptions['assertNoContextOffloadReferences'];
+ readonly #contextOffload: HostSessionRetirementCoordinatorOptions['contextOffload'];
readonly #purgeOperationalState: HostSessionRetirementCoordinatorOptions['purgeOperationalState'];
readonly #purgeAgentGraphState: HostSessionRetirementCoordinatorOptions['purgeAgentGraphState'];
readonly #worktrees: HostSessionRetirementCoordinatorOptions['worktrees'];
@@ -222,7 +223,7 @@ export class HostSessionRetirementCoordinator {
this.#continuity = options.continuity;
this.#artifacts = options.artifacts;
this.#sessionTodo = options.sessionTodo;
- this.#assertNoContextOffloadReferences = options.assertNoContextOffloadReferences;
+ this.#contextOffload = options.contextOffload;
this.#purgeOperationalState = options.purgeOperationalState;
this.#purgeAgentGraphState = options.purgeAgentGraphState;
this.#worktrees = options.worktrees;
@@ -338,7 +339,6 @@ export class HostSessionRetirementCoordinator {
if (plan.archive.sessionIds.length > 0) {
archiveHandles = await this.#prepareRetirement(plan.archive, 'archive');
}
- await this.#assertNoContextOffloadReferences?.(plan.remove.sessionIds);
const allSessionIds = [...plan.remove.sessionIds, ...plan.archive.sessionIds];
await this.#finalizeWorkspacePatches(allSessionIds);
await this.#disposeBackends(allSessionIds);
@@ -725,6 +725,7 @@ export class HostSessionRetirementCoordinator {
{
artifacts: this.#artifacts,
sessionTodo: this.#sessionTodo,
+ ...(this.#contextOffload ? { contextOffload: this.#contextOffload } : {}),
purgeOperationalState: this.#purgeOperationalState,
},
sessionId,
diff --git a/packages/runtime-host/src/server/session-revision-coordinator.ts b/packages/runtime-host/src/server/session-revision-coordinator.ts
index 16c918fff4..dd051dcfc8 100644
--- a/packages/runtime-host/src/server/session-revision-coordinator.ts
+++ b/packages/runtime-host/src/server/session-revision-coordinator.ts
@@ -35,6 +35,7 @@ import {
archivedToolResultContainsConversationOwnedReferences,
cloneConversationRuntimeLedger,
collectConversationCopyLinkedChildReferences,
+ collectConversationCopySessionContextRefIds,
collectConversationCopySessionFileRefs,
createConversationCopySlice,
prepareConversationRuntimeLedgerCopy,
@@ -61,6 +62,7 @@ import {
authenticateInteractiveSessionTodoWriter,
type InteractiveSessionTodoWriter,
} from '@maka/storage/session-todo-authority';
+import type { InteractiveContextOffloadWriter } from '@maka/storage/context-offload-store';
import type {
OperationOutcome,
SessionConversationCopyInput,
@@ -106,6 +108,10 @@ export interface HostSessionRevisionCoordinatorOptions {
readonly stores: ExecutionStoresWriter<'interactive'>;
readonly artifacts: InteractiveArtifactStoreWriter;
readonly sessionTodo: InteractiveSessionTodoWriter;
+ readonly contextOffload?: Pick<
+ InteractiveContextOffloadWriter,
+ 'copyReferences' | 'retireSession'
+ >;
readonly manager: SessionManager;
readonly admission: SessionAdmissionGate;
readonly continuity: SessionContinuityCoordinator;
@@ -503,6 +509,28 @@ export class HostSessionRevisionCoordinator {
)
.map(({ descriptor, serializedResult }) => [descriptor.artifactId, serializedResult]),
);
+ const sourceContextRefIds = collectConversationCopySessionContextRefIds({
+ sourceSessionId: input.sourceSessionId,
+ copiedMessages: slice.messages,
+ plan,
+ });
+ if (sourceContextRefIds.length > 0 && !this.options.contextOffload) {
+ throw new Error('Session context copy authority is unavailable');
+ }
+ const contextCopy =
+ sourceContextRefIds.length === 0
+ ? { ok: true as const, copied: [] }
+ : await this.options.contextOffload!.copyReferences({
+ sourceSessionId: input.sourceSessionId,
+ targetSessionId: input.targetSessionId,
+ references: sourceContextRefIds.map((sourceRefId) => ({
+ sourceRefId,
+ targetOwner: { kind: 'read_image_snapshot', ownerId: sourceRefId },
+ })),
+ });
+ if (!contextCopy.ok) {
+ throw new Error(`Session context references could not be copied: ${contextCopy.reason}`);
+ }
const artifactCopy = await this.#artifacts.copyConversationArtifacts({
sourceSessionId: input.sourceSessionId,
targetSessionId: input.targetSessionId,
@@ -528,6 +556,9 @@ export class HostSessionRevisionCoordinator {
targetSessionId: input.targetSessionId,
artifactIds: artifactCopy.artifactIds,
relativePaths: artifactCopy.relativePaths,
+ contextRefs: new Map(
+ contextCopy.copied.map(({ sourceRefId, targetRefId }) => [sourceRefId, targetRefId]),
+ ),
linkedChildren:
kind === 'side_conversation'
? {
@@ -819,6 +850,7 @@ export class HostSessionRevisionCoordinator {
{
artifacts: this.#artifacts,
sessionTodo: this.#sessionTodo,
+ ...(this.options.contextOffload ? { contextOffload: this.options.contextOffload } : {}),
purgeOperationalState: (sessionId) =>
this.#stores.purgeConversationOperationalState(sessionId),
},
diff --git a/packages/runtime-host/src/server/session-sidecar-purge.ts b/packages/runtime-host/src/server/session-sidecar-purge.ts
index 54a09e4676..59484fcf12 100644
--- a/packages/runtime-host/src/server/session-sidecar-purge.ts
+++ b/packages/runtime-host/src/server/session-sidecar-purge.ts
@@ -19,10 +19,12 @@
import type { InteractiveArtifactStoreWriter } from '@maka/storage/artifact-stores';
import type { InteractiveSessionTodoWriter } from '@maka/storage/session-todo-authority';
+import type { InteractiveContextOffloadWriter } from '@maka/storage/context-offload-store';
export interface SessionSidecarPurgeAuthority {
readonly artifacts: Pick<InteractiveArtifactStoreWriter, 'purgeSessionArtifacts'>;
readonly sessionTodo: Pick<InteractiveSessionTodoWriter, 'purgeSessionState'>;
+ readonly contextOffload?: Pick<InteractiveContextOffloadWriter, 'retireSession'>;
readonly purgeOperationalState: (sessionId: string) => Promise<void>;
}
@@ -33,6 +35,7 @@ export async function purgeSessionSidecars(
const outcomes = await Promise.allSettled([
authority.artifacts.purgeSessionArtifacts(sessionId),
authority.sessionTodo.purgeSessionState(sessionId),
+ ...(authority.contextOffload ? [authority.contextOffload.retireSession(sessionId)] : []),
authority.purgeOperationalState(sessionId),
]);
const failures = outcomes.flatMap((outcome) =>
diff --git a/packages/runtime/src/__tests__/builtin-tools-file-worker.test.ts b/packages/runtime/src/__tests__/builtin-tools-file-worker.test.ts
index bc0eaa5886..fe9c9a1c24 100644
--- a/packages/runtime/src/__tests__/builtin-tools-file-worker.test.ts
+++ b/packages/runtime/src/__tests__/builtin-tools-file-worker.test.ts
@@ -169,6 +169,7 @@ describe('builtin file tools use the sandboxed worker', () => {
test('uses one worker read operation for image paths', async () => {
const cwd = await temporaryDirectory('maka-file-worker-cwd-');
const calls: FilesystemWorkerExecuteInput[] = [];
+ let snapshotOwnerId: string | undefined;
const tools = buildBuiltinTools({
filesystemWorker: {
execute: async (input) => {
@@ -176,11 +177,14 @@ describe('builtin file tools use the sandboxed worker', () => {
return { kind: 'read_image', base64: 'iVBORw0KGgo=', mimeType: 'image/png' };
},
},
- snapshotImage: async () => ({
- kind: 'session_file',
- sessionId: 'session-1',
- relativePath: 'artifact-1',
- }),
+ snapshotImage: async (input) => {
+ snapshotOwnerId = input.ownerId;
+ return {
+ kind: 'session_context',
+ sessionId: 'session-1',
+ refId: 'context-1',
+ };
+ },
sandboxPlatform: 'darwin',
});
@@ -188,6 +192,7 @@ describe('builtin file tools use the sandboxed worker', () => {
assert.equal(calls.length, 1);
assert.deepEqual(calls[0]?.operation, { kind: 'read', path: 'image.png', offset: 1, limit: 1 });
+ assert.equal(snapshotOwnerId, 'tool-Read');
});
test('serializes writes through real and symlinked cwd paths', async () => {
diff --git a/packages/runtime/src/__tests__/conversation-copy.test.ts b/packages/runtime/src/__tests__/conversation-copy.test.ts
index 36bed08f12..7c1504ad39 100644
--- a/packages/runtime/src/__tests__/conversation-copy.test.ts
+++ b/packages/runtime/src/__tests__/conversation-copy.test.ts
@@ -44,6 +44,7 @@ import {
archivedToolResultContainsConversationOwnedReferences,
cloneConversationRuntimeLedger,
collectConversationCopyLinkedChildReferences,
+ collectConversationCopySessionContextRefIds,
collectConversationCopySessionFileRefs,
createConversationCopySlice,
prepareConversationRuntimeLedgerCopy,
@@ -721,6 +722,17 @@ test('conversation copy rewrites owned references without changing opaque tool p
relativePath: 'session-source/artifact-source-file.txt',
},
},
+ {
+ kind: 'image',
+ name: 'snapshot.png',
+ mimeType: 'image/png',
+ bytes: 4,
+ ref: {
+ kind: 'session_context',
+ sessionId: 'session-source',
+ refId: 'context-source',
+ },
+ },
],
},
{
@@ -800,6 +812,7 @@ test('conversation copy rewrites owned references without changing opaque tool p
relativePaths: new Map([
['session-source/artifact-source-file.txt', 'session-target/artifact-target-file.txt'],
]),
+ contextRefs: new Map([['context-source', 'context-target']]),
runIds: new Map([['run-source', 'run-target']]),
invocationIds: new Map([['invocation-source', 'invocation-target']]),
runtimeEventIds: new Map([['event-source', 'event-target']]),
@@ -812,6 +825,32 @@ test('conversation copy rewrites owned references without changing opaque tool p
sessionId: 'session-target',
relativePath: 'session-target/artifact-target-file.txt',
});
+ assert.deepEqual(rewritten[0]?.type === 'user' ? rewritten[0].attachments?.[1]?.ref : undefined, {
+ kind: 'session_context',
+ sessionId: 'session-target',
+ refId: 'context-target',
+ });
+ assert.deepEqual(
+ collectConversationCopySessionContextRefIds({
+ sourceSessionId: 'session-source',
+ copiedMessages: messages,
+ plan: {
+ sourceSessionId: 'session-source',
+ copyTurnIds: ['turn-1'],
+ inlineRuntimeEvents: [],
+ runs: [],
+ },
+ }),
+ ['context-source'],
+ );
+ assert.throws(
+ () =>
+ rewriteConversationCopyMessage(messages[0]!, {
+ ...references,
+ contextRefs: new Map(),
+ }),
+ /missing Session context context-source/,
+ );
const userMessage = messages[0];
assert.equal(userMessage?.type, 'user');
if (userMessage?.type !== 'user') return;
@@ -1004,44 +1043,6 @@ test('conversation copy rewrites owned references without changing opaque tool p
);
});
-test('reader-only conversation copy rejects source-owned Session context refs', () => {
- const message: StoredMessage = {
- type: 'user',
- id: 'user-context',
- turnId: 'turn-1',
- ts: 1,
- text: 'context',
- attachments: [
- {
- kind: 'image',
- name: 'snapshot.png',
- mimeType: 'image/png',
- bytes: 4,
- ref: {
- kind: 'session_context',
- sessionId: 'session-source',
- refId: 'context-source',
- },
- },
- ],
- };
- assert.throws(
- () =>
- rewriteConversationCopyMessage(message, {
- mode: 'exact',
- sourceSessionId: 'session-source',
- targetSessionId: 'session-target',
- artifactIds: new Map(),
- relativePaths: new Map(),
- linkedChildren: { mode: 'reject' },
- runIds: new Map(),
- runtimeEventIds: new Map(),
- providerTraceIds: new Map(),
- }),
- /does not support Session context references yet/,
- );
-});
-
test('conversation copy rejects continuation authority selected through the child-run closure', async () => {
const parent = agentRunHeader({ runId: 'run-parent', turnId: 'turn-parent' });
const child = agentRunHeader({
diff --git a/packages/runtime/src/__tests__/filesystem-authority.test.ts b/packages/runtime/src/__tests__/filesystem-authority.test.ts
index b70ff74418..c755002fa4 100644
--- a/packages/runtime/src/__tests__/filesystem-authority.test.ts
+++ b/packages/runtime/src/__tests__/filesystem-authority.test.ts
@@ -435,7 +435,7 @@ describe('file tools follow the execution boundary', () => {
},
snapshotImage: async (input) => {
snapshots.push(input.bytes);
- return { kind: 'session_file', sessionId: input.sessionId, relativePath: 'artifact-1' };
+ return { kind: 'session_context', sessionId: input.sessionId, refId: 'context-1' };
},
});
diff --git a/packages/runtime/src/builtin-tools.ts b/packages/runtime/src/builtin-tools.ts
index dbb1d7dc65..37a2f43011 100644
--- a/packages/runtime/src/builtin-tools.ts
+++ b/packages/runtime/src/builtin-tools.ts
@@ -184,11 +184,10 @@ export interface BuildBuiltinToolsOptions {
sandboxPlatform?: SandboxPlatform;
snapshotImage?: (input: {
sessionId: string;
- turnId: string;
- name: string;
+ ownerId: string;
bytes: Uint8Array;
mimeType: string;
- }) => Promise<Extract<StorageRef, { kind: 'session_file' }>>;
+ }) => Promise<Extract<StorageRef, { kind: 'session_context' }>>;
}
export function buildBuiltinTools(options: BuildBuiltinToolsOptions = {}): MakaTool[] {
@@ -400,8 +399,7 @@ export function buildBuiltinTools(options: BuildBuiltinToolsOptions = {}): MakaT
throw new Error('Read image snapshots are not available in this toolset.');
const ref = await options.snapshotImage({
sessionId,
- turnId: ctx.turnId,
- name: basename(path),
+ ownerId: ctx.toolCallId,
bytes: result.bytes,
mimeType: result.mimeType,
});
diff --git a/packages/runtime/src/conversation-copy.ts b/packages/runtime/src/conversation-copy.ts
index f013c89302..fe42b81b12 100644
--- a/packages/runtime/src/conversation-copy.ts
+++ b/packages/runtime/src/conversation-copy.ts
@@ -25,7 +25,7 @@ import type {
} from '@maka/core/agent-run';
import type { RuntimeEvent } from '@maka/core/runtime-event';
import type { RuntimeEventStore } from '@maka/core/runtime-event-store';
-import type { StorageRef, ToolResultContent } from '@maka/core/events';
+import { isStorageRef, type StorageRef, type ToolResultContent } from '@maka/core/events';
import { parseAttachmentResourceRef } from '@maka/core/attachments';
import { markPersisted } from '@maka/core/persisted-value';
import type { StoredMessage } from '@maka/core/session';
@@ -104,6 +104,7 @@ export type ConversationCopyArtifactReferenceMap =
readonly mode: 'exact';
readonly artifactIds: ReadonlyMap<string, string>;
readonly relativePaths: ReadonlyMap<string, string>;
+ readonly contextRefs?: ReadonlyMap<string, string>;
readonly linkedChildren:
| { readonly mode: 'reject' }
| {
@@ -159,6 +160,35 @@ export interface ConversationRuntimeLedgerCopyPlan {
}[];
}
+/** Finds durable Session context references that the exact copy will rewrite. */
+export function collectConversationCopySessionContextRefIds(input: {
+ readonly sourceSessionId: string;
+ readonly copiedMessages: readonly StoredMessage[];
+ readonly plan: ConversationRuntimeLedgerCopyPlan;
+}): readonly string[] {
+ const refIds = new Set<string>();
+ const seen = new WeakSet<object>();
+ const visit = (value: unknown): void => {
+ if (isStorageRef(value)) {
+ if (value.kind === 'session_context' && value.sessionId === input.sourceSessionId) {
+ refIds.add(value.refId);
+ }
+ return;
+ }
+ if (typeof value !== 'object' || value === null || seen.has(value)) return;
+ seen.add(value);
+ if (Array.isArray(value)) {
+ for (const item of value) visit(item);
+ return;
+ }
+ for (const item of Object.values(value)) visit(item);
+ };
+ visit(input.copiedMessages);
+ visit(input.plan.inlineRuntimeEvents);
+ for (const run of input.plan.runs) visit(run.runtimeEvents);
+ return [...refIds].sort();
+}
+
export interface CloneConversationRuntimeLedgerResult {
readonly copiedMessages: readonly StoredMessage[];
readonly runIdMap: readonly {
@@ -1779,12 +1809,22 @@ function rewriteStorageRef(
ref: StorageRef,
references: ConversationCopyArtifactReferenceMap,
): StorageRef {
- if (ref.kind === 'session_context' && ref.sessionId === references.sourceSessionId) {
- if (references.mode === 'preserve_external') return ref;
- throw new Error('Conversation copy does not support Session context references yet');
+ if (
+ (ref.kind !== 'session_file' && ref.kind !== 'session_context') ||
+ ref.sessionId !== references.sourceSessionId
+ ) {
+ return ref;
}
- if (ref.kind !== 'session_file' || ref.sessionId !== references.sourceSessionId) return ref;
if (references.mode === 'preserve_external') return ref;
+ if (ref.kind === 'session_context') {
+ const refId = references.contextRefs?.get(ref.refId);
+ if (!refId) throw new Error(`Conversation copy is missing Session context ${ref.refId}`);
+ return {
+ ...ref,
+ sessionId: references.targetSessionId,
+ refId,
+ };
+ }
const artifactId = references.artifactIds.get(ref.relativePath);
if (artifactId) {
return {
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org