Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c87dcc5
feat(core): define durable tool result projections
Astro-Han Aug 30, 2026
9c30108
feat(runtime): add durable projection codec
Astro-Han Aug 30, 2026
fd0266d
feat(runtime): commit projections atomically at T2
Astro-Han Aug 30, 2026
53a9290
refactor(runtime): replay effective tool result projections
Astro-Han Aug 30, 2026
8956bfa
feat(runtime-host): persist projected artifacts and recoveries
Astro-Han Aug 30, 2026
ed6b16a
refactor(runtime): remove transient Tool Result authority
Astro-Han Aug 31, 2026
06307a2
fix(storage): reuse projected image artifacts
Astro-Han Aug 31, 2026
320f23a
fix(runtime): remap projected artifacts on copy
Astro-Han Aug 31, 2026
4542178
refactor(runtime): remove synthetic projection return
Astro-Han Aug 31, 2026
2fc3d90
refactor(runtime): privatize synthetic result writer
Astro-Han Aug 31, 2026
9043ee4
fix(runtime): validate projected images before persistence
Astro-Han Aug 31, 2026
41320c0
fix(runtime): reject unowned projection copies
Astro-Han Aug 31, 2026
01a3dc7
refactor(runtime): remove retired conversation copy owner
Astro-Han Aug 31, 2026
4e13f3e
test(runtime): verify durable projection owner boundaries
Astro-Han Aug 31, 2026
2cd0773
refactor(runtime): keep projection helper types private
Astro-Han Aug 31, 2026
4afc17a
test(runtime): target durable projection T2 rollback
Astro-Han Aug 31, 2026
55e5c74
refactor(runtime): trim projection artifact input
Astro-Han Aug 31, 2026
f3c16d9
test(runtime): produce restart projection through T2
Astro-Han Aug 31, 2026
1be1a56
style(runtime): format session manager tests
Astro-Han Aug 31, 2026
f370c36
fix(runtime): make projection admission side-effect free
Astro-Han Aug 31, 2026
d330705
test(runtime): replay persisted image after restart
Astro-Han Aug 31, 2026
8051b0d
test(runtime-host): cover projection artifact restart
Astro-Han Aug 31, 2026
d5aee93
refactor(runtime): preserve projection content fidelity
Astro-Han Aug 31, 2026
ab59003
fix(storage): protect projection artifacts
Astro-Han Aug 31, 2026
eea414d
fix(runtime): admit tool calls before publication
Astro-Han Aug 31, 2026
762086a
fix(runtime): bound projection copies before allocation
Astro-Han Aug 31, 2026
3af5bfe
test(runtime-host): consolidate projection lifecycle coverage
Astro-Han Aug 31, 2026
2cd37a6
test(runtime): lock T1 failure side effects
Astro-Han Aug 31, 2026
5a70eab
refactor(core): unify artifact source policy
Astro-Han Aug 31, 2026
6190657
fix(runtime): make tool result projectors total
Astro-Han Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,10 @@
* under the License.
*/

import type { ArtifactDescriptor, ArtifactSource } from '@maka/core/artifacts';

const USER_VISIBLE_ARTIFACT_SOURCES = {
tool_result: false,
tool_result_archive: false,
synthesis_cache_block: false,
history_compact_block: false,
history_compact_source: false,
provider_request_capture: false,
session_effect: false,
subagent_writeback: true,
deep_research: true,
user_upload: false,
export: true,
snapshot: true,
fixture: true,
} satisfies Record<ArtifactSource, boolean>;
import { isArtifactUserVisible, type ArtifactDescriptor } from '@maka/core/artifacts';

export function filterUserVisibleArtifacts(
records: readonly ArtifactDescriptor[],
): ArtifactDescriptor[] {
return records.filter(
(record) => record.source === undefined || USER_VISIBLE_ARTIFACT_SOURCES[record.source],
);
return records.filter(isArtifactUserVisible);
}
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"sideEffects": false,
"private": true,
"exports": {
"./durable-tool-result-projection": "./dist/durable-tool-result-projection.js",
"./canonical-runtime-event": "./dist/canonical-runtime-event.js",
"./runtime-boundary": "./dist/runtime-boundary.js",
"./runtime-event": "./dist/runtime-event.js",
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/__tests__/artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
ARTIFACT_ENTITY_ID_MAX_CHARS,
ARTIFACT_TURN_KEY_MAX_CHARS,
canUserDeleteArtifact,
isArtifactSharedSessionReadable,
isArtifactUserVisible,
isArtifactTurnKey,
isCanonicalArtifactEntityId,
} from '../artifacts.js';
Expand Down Expand Up @@ -61,3 +63,21 @@ describe('Artifact user-delete policy', () => {
assert.equal(canUserDeleteArtifact({ source: undefined }), true);
});
});

describe('Artifact source policy', () => {
test('keeps projection artifacts internal, durable, and readable in shared sessions', () => {
const projection = { source: 'tool_result_projection' as const };

assert.equal(canUserDeleteArtifact(projection), false);
assert.equal(isArtifactUserVisible(projection), false);
assert.equal(isArtifactSharedSessionReadable(projection), true);
});

test('preserves unattributed artifact defaults', () => {
const unattributed = { source: undefined };

assert.equal(canUserDeleteArtifact(unattributed), true);
assert.equal(isArtifactUserVisible(unattributed), true);
assert.equal(isArtifactSharedSessionReadable(unattributed), false);
});
});
160 changes: 160 additions & 0 deletions packages/core/src/__tests__/durable-tool-result-projection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import { describe, it } from 'node:test';

import {
decodeDurableToolResultProjection,
DURABLE_TOOL_RESULT_PROJECTION_MAX_BYTES,
DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_DEPTH,
DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_NODES,
} from '../durable-tool-result-projection.js';

describe('durable Tool Result projection', () => {
it('accepts only the closed current-version schema', () => {
const projection = { version: 1, kind: 'text', text: 'safe' } as const;

assert.deepEqual(decodeDurableToolResultProjection(projection), projection);
assert.throws(
() => decodeDurableToolResultProjection({ ...projection, version: 2 }),
/Invalid durable Tool Result projection/,
);
assert.throws(
() => decodeDurableToolResultProjection({ ...projection, providerOptions: {} }),
/Invalid durable Tool Result projection/,
);
assert.throws(
() =>
decodeDurableToolResultProjection({
version: 1,
kind: 'content',
parts: [],
}),
/Invalid durable Tool Result projection/,
);
});

it('rejects projections whose serialized JSON exceeds the durable byte bound', () => {
assert.throws(
() =>
decodeDurableToolResultProjection({
version: 1,
kind: 'text',
text: 'x'.repeat(DURABLE_TOOL_RESULT_PROJECTION_MAX_BYTES),
}),
/Invalid durable Tool Result projection/,
);
});

it('accepts only content-addressed references owned by the same Session protocol', () => {
assert.deepEqual(
decodeDurableToolResultProjection({
version: 1,
kind: 'content',
parts: [
{
kind: 'artifact',
mediaType: 'image/png',
ref: { kind: 'session_context', sessionId: 'session-1', refId: 'sha256-image' },
},
],
}).kind,
'content',
);
assert.throws(
() =>
decodeDurableToolResultProjection({
version: 1,
kind: 'content',
parts: [
{
kind: 'artifact',
mediaType: 'image/png',
ref: { kind: 'external_file', absolutePath: '/private/image.png' },
},
],
}),
/Invalid durable Tool Result projection/,
);
assert.throws(
() =>
decodeDurableToolResultProjection({
version: 1,
kind: 'content',
parts: [
{
kind: 'artifact',
mediaType: 'image/png',
ref: {
kind: 'session_file',
sessionId: 'session-1',
relativePath: 'arbitrary/path.png',
},
},
],
}),
/Invalid durable Tool Result projection/,
);
});

it('admits only canonical safe image media types', () => {
const projection = (mediaType: string) => ({
version: 1,
kind: 'content',
parts: [
{
kind: 'artifact',
mediaType,
ref: { kind: 'session_file', sessionId: 'session-1', relativePath: 'artifact-1' },
},
],
});

assert.equal(decodeDurableToolResultProjection(projection('image/png')).kind, 'content');
assert.throws(
() => decodeDurableToolResultProjection(projection('image/png; token=sk-secret')),
/Invalid durable Tool Result projection/,
);
assert.throws(
() => decodeDurableToolResultProjection(projection('image/svg+xml')),
/Invalid durable Tool Result projection/,
);
});

it('applies the same bounded JSON budget to persisted projections', () => {
let tooDeep: unknown = 'leaf';
for (let depth = 0; depth <= DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_DEPTH; depth += 1) {
tooDeep = [tooDeep];
}
assert.throws(
() => decodeDurableToolResultProjection({ version: 1, kind: 'json', value: tooDeep }),
/Invalid durable Tool Result projection/,
);
assert.throws(
() =>
decodeDurableToolResultProjection({
version: 1,
kind: 'json',
value: Array.from({ length: DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_NODES }, () => null),
}),
/Invalid durable Tool Result projection/,
);
});
});
34 changes: 34 additions & 0 deletions packages/core/src/__tests__/runtime-event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,40 @@ describe('RuntimeEvent content variants', () => {
});
});

test('rejects a Tool Result projection that references another Session artifact', () => {
assert.throws(
() =>
decodeRuntimeEvent(
baseEvent({
role: 'tool',
author: 'tool',
content: {
kind: 'function_response',
id: 'call-1',
name: 'Read',
result: { kind: 'image' },
modelProjection: {
version: 1,
kind: 'content',
parts: [
{
kind: 'artifact',
mediaType: 'image/png',
ref: {
kind: 'session_context',
sessionId: 'another-session',
refId: 'image-1',
},
},
],
},
},
}),
),
/Invalid RuntimeEvent schema/,
);
});

describe('RuntimeEvent actions', () => {
test('binds the managed mutation digest to its canonical execution semantics', () => {
const canonicalProfile = JSON.stringify({
Expand Down
48 changes: 32 additions & 16 deletions packages/core/src/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export function resolveArtifactImagePreview(

export const ARTIFACT_SOURCES = [
'tool_result',
'tool_result_projection',
'tool_result_archive',
'synthesis_cache_block',
'history_compact_block',
Expand Down Expand Up @@ -152,24 +153,39 @@ export interface ArtifactRecord extends ArtifactDescriptor {
deepResearchRole?: import('./deep-research-run.js').DeepResearchArtifactRole;
}

const ARTIFACT_USER_DELETE_ALLOWED_BY_SOURCE = {
tool_result: true,
tool_result_archive: false,
synthesis_cache_block: true,
history_compact_block: true,
history_compact_source: true,
provider_request_capture: true,
subagent_writeback: false,
deep_research: false,
user_upload: true,
export: true,
snapshot: true,
session_effect: false,
fixture: true,
} as const satisfies Record<ArtifactSource, boolean>;
interface ArtifactSourcePolicy {
readonly userDeletable: boolean;
readonly userVisible: boolean;
readonly sharedReadable: boolean;
}

const ARTIFACT_SOURCE_POLICIES = {
tool_result: { userDeletable: true, userVisible: false, sharedReadable: true },
tool_result_projection: { userDeletable: false, userVisible: false, sharedReadable: true },
tool_result_archive: { userDeletable: false, userVisible: false, sharedReadable: false },
synthesis_cache_block: { userDeletable: true, userVisible: false, sharedReadable: false },
history_compact_block: { userDeletable: true, userVisible: false, sharedReadable: false },
history_compact_source: { userDeletable: true, userVisible: false, sharedReadable: false },
provider_request_capture: { userDeletable: true, userVisible: false, sharedReadable: false },
subagent_writeback: { userDeletable: false, userVisible: true, sharedReadable: false },
deep_research: { userDeletable: false, userVisible: true, sharedReadable: false },
user_upload: { userDeletable: true, userVisible: false, sharedReadable: true },
export: { userDeletable: true, userVisible: true, sharedReadable: false },
snapshot: { userDeletable: true, userVisible: true, sharedReadable: false },
session_effect: { userDeletable: false, userVisible: false, sharedReadable: false },
fixture: { userDeletable: true, userVisible: true, sharedReadable: false },
} as const satisfies Record<ArtifactSource, ArtifactSourcePolicy>;

export function canUserDeleteArtifact(record: Pick<ArtifactRecord, 'source'>): boolean {
return record.source === undefined || ARTIFACT_USER_DELETE_ALLOWED_BY_SOURCE[record.source];
return record.source === undefined || ARTIFACT_SOURCE_POLICIES[record.source].userDeletable;
}

export function isArtifactUserVisible(record: Pick<ArtifactRecord, 'source'>): boolean {
return record.source === undefined || ARTIFACT_SOURCE_POLICIES[record.source].userVisible;
}

export function isArtifactSharedSessionReadable(record: Pick<ArtifactRecord, 'source'>): boolean {
return record.source !== undefined && ARTIFACT_SOURCE_POLICIES[record.source].sharedReadable;
}

export type ArtifactChangedReason = 'created' | 'deleted' | 'purged';
Expand Down
Loading