@myagentroam/agent 0.10.59 → 0.10.60

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.
Files changed (54) hide show
  1. package/dist/agent-core/runtime/compaction/history.d.ts +1 -1
  2. package/dist/agent-core/runtime/compaction/retained-context.d.ts +1 -1
  3. package/dist/agent-core/runtime/compaction/retained-context.js +1 -1
  4. package/dist/agent-core/runtime/compaction/smart.d.ts +1 -1
  5. package/dist/agent-core/runtime/compaction/smart.js +1 -1
  6. package/dist/agent-core/runtime/context/gc-runtime.d.ts +1 -1
  7. package/dist/agent-core/runtime/context/gc-runtime.js +20 -14
  8. package/dist/agent-core/runtime/context/gc.d.ts +1 -1
  9. package/dist/agent-core/runtime/context/gc.js +4 -10
  10. package/dist/agent-core/runtime/context/projection.d.ts +1 -1
  11. package/dist/agent-core/runtime/context/projection.js +3 -5
  12. package/dist/agent-core/runtime/context/restore.d.ts +4 -4
  13. package/dist/agent-core/runtime/context/restore.js +1 -1
  14. package/dist/agent-core/runtime/context/usage.d.ts +2 -2
  15. package/dist/agent-core/runtime/context/usage.js +1 -1
  16. package/dist/agent-core/runtime/context/world-state.d.ts +2 -2
  17. package/dist/agent-core/runtime/context/world-state.js +1 -1
  18. package/dist/agent-core/sdk/agent.js +1 -1
  19. package/dist/agent-core/sdk/native-steer.d.ts +1 -1
  20. package/dist/agent-core/sdk/session.d.ts +1 -1
  21. package/dist/agent-core/sdk/session.js +1 -1
  22. package/dist/agent-core/sdk/subagent-backend.d.ts +1 -1
  23. package/dist/agent-core/session/catalog.js +1 -1
  24. package/dist/agent-core/session/context-gc-checkpoint.d.ts +4 -8
  25. package/dist/agent-core/session/context-gc-checkpoint.js +82 -137
  26. package/dist/agent-core/session/context-gc-observations.d.ts +1 -1
  27. package/dist/agent-core/session/context-gc-replacements.d.ts +4 -7
  28. package/dist/agent-core/session/context-gc-replacements.js +25 -59
  29. package/dist/agent-core/session/context-gc-subagent.d.ts +0 -3
  30. package/dist/agent-core/session/context-gc-subagent.js +2 -16
  31. package/dist/agent-core/session/deferred-input.d.ts +1 -1
  32. package/dist/agent-core/session/store/current-state.d.ts +17 -0
  33. package/dist/agent-core/session/store/current-state.js +53 -0
  34. package/dist/agent-core/session/store/directory-index.d.ts +18 -0
  35. package/dist/agent-core/session/store/directory-index.js +163 -0
  36. package/dist/agent-core/session/store/execution-state.d.ts +17 -0
  37. package/dist/agent-core/session/store/execution-state.js +178 -0
  38. package/dist/agent-core/session/store/history.d.ts +25 -0
  39. package/dist/agent-core/session/store/history.js +157 -0
  40. package/dist/agent-core/session/store/images.d.ts +21 -0
  41. package/dist/agent-core/session/store/images.js +248 -0
  42. package/dist/agent-core/session/store/index.d.ts +4 -0
  43. package/dist/agent-core/session/store/index.js +2 -0
  44. package/dist/agent-core/session/{jsonl-store.d.ts → store/jsonl-store.d.ts} +12 -92
  45. package/dist/agent-core/session/{jsonl-store.js → store/jsonl-store.js} +108 -1053
  46. package/dist/agent-core/session/store/records.d.ts +23 -0
  47. package/dist/agent-core/session/store/records.js +249 -0
  48. package/dist/agent-core/session/store/types.d.ts +83 -0
  49. package/dist/agent-core/session/store/types.js +1 -0
  50. package/dist/agent-core/session/tool-result-resources.d.ts +1 -1
  51. package/dist/agent-core/subagent/session-controller.d.ts +1 -1
  52. package/dist/agent-core/tools/code-mode/state.d.ts +1 -1
  53. package/dist/agent-core/tools/code-mode/state.js +2 -2
  54. package/package.json +1 -1
@@ -0,0 +1,17 @@
1
+ import { type CoreCheckpoint } from '../../extensions/data.js';
2
+ import { type CodeModeStateEntry } from '../../tools/code-mode/state.js';
3
+ interface StateRecord {
4
+ readonly type: string;
5
+ readonly payload: unknown;
6
+ }
7
+ export type ExtensionCheckpointIndex = Map<string, Map<string, Record<string, unknown>>>;
8
+ /** Derived non-model state owned by one physical Session writer lease. */
9
+ export interface SessionCurrentState {
10
+ codeState?: CodeModeStateEntry[];
11
+ extensionCheckpoints?: ExtensionCheckpointIndex;
12
+ }
13
+ export declare function updateSessionCurrentState(cache: SessionCurrentState | undefined, type: string, serialized: string): void;
14
+ export declare function restoreExtensionCheckpoint(records: readonly StateRecord[], extensionId: string, key: string): CoreCheckpoint;
15
+ export declare function indexExtensionCheckpoint(index: ExtensionCheckpointIndex, payload: unknown): void;
16
+ export declare function extensionCheckpointFromPayload(payload: Record<string, unknown> | undefined): CoreCheckpoint;
17
+ export {};
@@ -0,0 +1,53 @@
1
+ import { MarAgentError } from '../../error.js';
2
+ import { cloneCoreJson } from '../../extensions/data.js';
3
+ import { restoreCodeModeState } from '../../tools/code-mode/state.js';
4
+ export function updateSessionCurrentState(cache, type, serialized) {
5
+ if (!cache)
6
+ return;
7
+ if (type === 'code.state.updated' && cache.codeState !== undefined) {
8
+ try {
9
+ cache.codeState = restoreCodeModeState([JSON.parse(serialized)], cache.codeState);
10
+ }
11
+ catch {
12
+ // Cache maintenance must not fail an already durable append.
13
+ delete cache.codeState;
14
+ }
15
+ }
16
+ else if (type === 'extension.checkpoint.updated' && cache.extensionCheckpoints) {
17
+ indexExtensionCheckpoint(cache.extensionCheckpoints, JSON.parse(serialized).payload);
18
+ }
19
+ }
20
+ export function restoreExtensionCheckpoint(records, extensionId, key) {
21
+ for (const record of records.toReversed()) {
22
+ if (record.type !== 'extension.checkpoint.updated')
23
+ continue;
24
+ const payload = record.payload;
25
+ if (!payload || payload.extensionId !== extensionId || payload.key !== key)
26
+ continue;
27
+ return extensionCheckpointFromPayload(payload);
28
+ }
29
+ return { revision: 0 };
30
+ }
31
+ export function indexExtensionCheckpoint(index, payload) {
32
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload))
33
+ return;
34
+ const value = payload;
35
+ if (typeof value.extensionId !== 'string' || typeof value.key !== 'string')
36
+ return;
37
+ let namespace = index.get(value.extensionId);
38
+ if (!namespace)
39
+ index.set(value.extensionId, (namespace = new Map()));
40
+ namespace.set(value.key, value);
41
+ }
42
+ export function extensionCheckpointFromPayload(payload) {
43
+ if (!payload)
44
+ return { revision: 0 };
45
+ if (typeof payload.revision !== 'number' ||
46
+ !Number.isSafeInteger(payload.revision) ||
47
+ payload.revision < 1)
48
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Extension checkpoint revision is corrupt.');
49
+ return {
50
+ revision: payload.revision,
51
+ ...(payload.deleted === true ? {} : { value: cloneCoreJson(payload.value) })
52
+ };
53
+ }
@@ -0,0 +1,18 @@
1
+ import type { SessionListInput, SessionSource } from '../types.js';
2
+ import type { SessionIndex, SessionMeta } from './types.js';
3
+ /** Merge a derived cache only onto the unchanged history it was computed from. */
4
+ export declare function mergeFirstUserMessagePreviewCache(current: SessionMeta, cached: SessionMeta): SessionMeta;
5
+ export declare function readSessionIndex(path: string): Promise<SessionIndex | undefined>;
6
+ export declare function persistSessionIndex(path: string, index: SessionIndex): Promise<void>;
7
+ export declare function normalizeSessionSource(value: SessionSource | undefined): SessionSource;
8
+ interface SessionListFilter {
9
+ readonly source: SessionSource['type'];
10
+ readonly parentSessionId?: string;
11
+ readonly rootSessionId?: string;
12
+ }
13
+ export declare function sessionListFilter(input: SessionListInput): SessionListFilter;
14
+ export declare function sessionMatchesFilter(meta: SessionMeta, filter: SessionListFilter): boolean;
15
+ export declare function encodeSessionListCursor(revision: number, offset: number, filter: SessionListFilter): string;
16
+ export declare function decodeSessionListCursor(cursor: string | undefined, revision: number, filter: SessionListFilter): number;
17
+ export declare function sessionTreeIds(sessions: readonly SessionMeta[], sessionId: string): string[];
18
+ export {};
@@ -0,0 +1,163 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ import { MarAgentError } from '../../error.js';
5
+ /** Merge a derived cache only onto the unchanged history it was computed from. */
6
+ export function mergeFirstUserMessagePreviewCache(current, cached) {
7
+ if (cached.resume?.firstUserMessageIndexed !== true ||
8
+ current.nextSequence !== cached.nextSequence ||
9
+ current.storageTransactionId !== cached.storageTransactionId ||
10
+ current.resume?.firstUserMessagePreview !== undefined ||
11
+ (current.resume?.firstUserMessageIndexed === true &&
12
+ cached.resume.firstUserMessagePreview === undefined))
13
+ return current;
14
+ return {
15
+ ...current,
16
+ resume: {
17
+ ...(current.resume ?? { version: 1, todoRevision: 0 }),
18
+ firstUserMessageIndexed: true,
19
+ ...(cached.resume?.firstUserMessagePreview === undefined
20
+ ? {}
21
+ : { firstUserMessagePreview: cached.resume.firstUserMessagePreview })
22
+ }
23
+ };
24
+ }
25
+ export async function readSessionIndex(path) {
26
+ let raw;
27
+ try {
28
+ raw = await readFile(path, 'utf8');
29
+ }
30
+ catch (cause) {
31
+ if (cause.code === 'ENOENT')
32
+ return undefined;
33
+ throw cause;
34
+ }
35
+ try {
36
+ const value = JSON.parse(raw);
37
+ if (value?.version !== 1 ||
38
+ !Number.isSafeInteger(value.revision) ||
39
+ value.revision < 0 ||
40
+ !Array.isArray(value.sessions) ||
41
+ value.sessions.some((session) => typeof session?.id !== 'string' ||
42
+ !/^[0-9a-f-]{36}$/iu.test(session.id) ||
43
+ typeof session.title !== 'string' ||
44
+ typeof session.workspace !== 'string' ||
45
+ typeof session.createdAt !== 'string' ||
46
+ typeof session.lastActivityAt !== 'string' ||
47
+ !Number.isSafeInteger(session.nextSequence) ||
48
+ session.nextSequence < 1))
49
+ throw new Error('invalid index shape');
50
+ return value;
51
+ }
52
+ catch (cause) {
53
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session index is corrupt.', { cause });
54
+ }
55
+ }
56
+ export async function persistSessionIndex(path, index) {
57
+ const temporary = `${path}.${randomUUID()}.tmp`;
58
+ try {
59
+ await writeFile(temporary, JSON.stringify(index), { mode: 0o600, flag: 'wx', flush: true });
60
+ await rename(temporary, path);
61
+ }
62
+ finally {
63
+ await rm(temporary, { force: true });
64
+ }
65
+ }
66
+ export function normalizeSessionSource(value) {
67
+ if (value === undefined || value.type === 'user')
68
+ return { type: 'user' };
69
+ if (value.type !== 'subagent' ||
70
+ typeof value.parentSessionId !== 'string' ||
71
+ !value.parentSessionId.trim() ||
72
+ typeof value.rootSessionId !== 'string' ||
73
+ !value.rootSessionId.trim() ||
74
+ (!value.parentExecutionId && !value.parentOperationId) ||
75
+ [
76
+ value.parentExecutionId,
77
+ value.parentOperationId,
78
+ value.ownerInstanceId,
79
+ value.extensionId
80
+ ].some((identity) => identity !== undefined && (typeof identity !== 'string' || !identity.trim())) ||
81
+ ((value.ownerInstanceId !== undefined || value.extensionId !== undefined) &&
82
+ value.parentOperationId === undefined) ||
83
+ typeof value.description !== 'string' ||
84
+ !value.description.trim() ||
85
+ (value.role !== 'general-purpose' && value.role !== 'explore'))
86
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Session source is invalid.');
87
+ return {
88
+ type: 'subagent',
89
+ parentSessionId: value.parentSessionId,
90
+ rootSessionId: value.rootSessionId,
91
+ ...(value.parentExecutionId === undefined
92
+ ? {}
93
+ : { parentExecutionId: value.parentExecutionId }),
94
+ ...(value.parentOperationId === undefined
95
+ ? {}
96
+ : { parentOperationId: value.parentOperationId }),
97
+ ...(value.ownerInstanceId === undefined ? {} : { ownerInstanceId: value.ownerInstanceId }),
98
+ ...(value.extensionId === undefined ? {} : { extensionId: value.extensionId }),
99
+ role: value.role,
100
+ description: value.description.trim()
101
+ };
102
+ }
103
+ export function sessionListFilter(input) {
104
+ const source = input.source ??
105
+ (input.parentSessionId !== undefined || input.rootSessionId !== undefined
106
+ ? 'subagent'
107
+ : 'user');
108
+ if ((source !== 'user' && source !== 'subagent') ||
109
+ (input.parentSessionId !== undefined &&
110
+ (source !== 'subagent' || input.parentSessionId.length === 0)) ||
111
+ (input.rootSessionId !== undefined &&
112
+ (source !== 'subagent' || input.rootSessionId.length === 0)))
113
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Session list filter is invalid.');
114
+ return {
115
+ source,
116
+ ...(input.parentSessionId === undefined ? {} : { parentSessionId: input.parentSessionId }),
117
+ ...(input.rootSessionId === undefined ? {} : { rootSessionId: input.rootSessionId })
118
+ };
119
+ }
120
+ export function sessionMatchesFilter(meta, filter) {
121
+ const source = normalizeSessionSource(meta.source);
122
+ return (source.type === filter.source &&
123
+ (filter.parentSessionId === undefined ||
124
+ (source.type === 'subagent' && source.parentSessionId === filter.parentSessionId)) &&
125
+ (filter.rootSessionId === undefined ||
126
+ (source.type === 'subagent' && source.rootSessionId === filter.rootSessionId)));
127
+ }
128
+ export function encodeSessionListCursor(revision, offset, filter) {
129
+ return Buffer.from(JSON.stringify({ version: 1, revision, offset, filter })).toString('base64url');
130
+ }
131
+ export function decodeSessionListCursor(cursor, revision, filter) {
132
+ if (cursor === undefined)
133
+ return 0;
134
+ try {
135
+ const value = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
136
+ if (value.version !== 1 ||
137
+ value.revision !== revision ||
138
+ !Number.isSafeInteger(value.offset) ||
139
+ value.offset < 0 ||
140
+ !isDeepStrictEqual(value.filter, filter))
141
+ throw new Error('invalid cursor');
142
+ return value.offset;
143
+ }
144
+ catch {
145
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Session list cursor is invalid.');
146
+ }
147
+ }
148
+ export function sessionTreeIds(sessions, sessionId) {
149
+ if (!sessions.some((session) => session.id === sessionId))
150
+ return [];
151
+ const ids = [sessionId];
152
+ for (let index = 0; index < ids.length; index++) {
153
+ const parentId = ids[index];
154
+ for (const session of sessions) {
155
+ const source = normalizeSessionSource(session.source);
156
+ if (source.type === 'subagent' &&
157
+ source.parentSessionId === parentId &&
158
+ !ids.includes(session.id))
159
+ ids.push(session.id);
160
+ }
161
+ }
162
+ return ids;
163
+ }
@@ -0,0 +1,17 @@
1
+ import type { ModelInputMetadata } from '../../model/contracts.js';
2
+ import type { CompactHistoryMessage, SessionExecutionState, SessionMeta, SessionRecord } from './types.js';
3
+ export declare function deriveExecutionState(records: readonly SessionRecord[]): SessionExecutionState & {
4
+ resume: NonNullable<SessionMeta['resume']>;
5
+ };
6
+ export declare function userMessagePreviewFromRecord(record: SessionRecord): string | undefined;
7
+ export declare function executionModelFromRecord(record: SessionRecord): {
8
+ modelId: string;
9
+ modelName?: string;
10
+ reasoningEffort?: string;
11
+ } | undefined;
12
+ export declare function compactSummaryFromCheckpoint(record: SessionRecord | undefined): string | undefined;
13
+ export declare function replacementHistoryFromCheckpoint(record: SessionRecord): CompactHistoryMessage[] | undefined;
14
+ export declare function parseModelInputMetadata(value: unknown): ModelInputMetadata | undefined;
15
+ export declare function inputMetadataFromRecord(record: SessionRecord): ModelInputMetadata;
16
+ export declare function isCompactCheckpoint(record: SessionRecord | undefined): boolean;
17
+ export declare function sameResumeIndex(left: SessionMeta['resume'], right: NonNullable<SessionMeta['resume']>): boolean;
@@ -0,0 +1,178 @@
1
+ import { todoItemsFromSnapshot } from '../../tools/agent/todo.js';
2
+ import { contextGcBaseSequence, latestValidContextGcCheckpoint } from '../context-gc-checkpoint.js';
3
+ export function deriveExecutionState(records) {
4
+ let automaticTitle = false;
5
+ let sawUserTurn = false;
6
+ let compactSequence;
7
+ let todoSequence;
8
+ let todoRevision = 0;
9
+ let todoItems = [];
10
+ let executionModel;
11
+ let firstUserMessagePreview;
12
+ for (const record of records) {
13
+ firstUserMessagePreview ??= userMessagePreviewFromRecord(record);
14
+ if (record.type === 'session.created')
15
+ automaticTitle =
16
+ record.payload?.automaticTitle === true;
17
+ else if (record.type === 'turn.user')
18
+ sawUserTurn = true;
19
+ else if (record.type === 'session.title.updated')
20
+ automaticTitle = false;
21
+ else if (isCompactCheckpoint(record))
22
+ compactSequence = record.sequence;
23
+ else if (record.type === 'todo.updated') {
24
+ const items = todoItemsFromSnapshot(record.payload);
25
+ if (!items)
26
+ continue;
27
+ todoSequence = record.sequence;
28
+ todoRevision++;
29
+ todoItems = items;
30
+ }
31
+ const candidate = executionModelFromRecord(record);
32
+ if (candidate !== undefined)
33
+ executionModel = candidate;
34
+ }
35
+ const boundary = records.findLastIndex(isCompactCheckpoint);
36
+ const checkpoint = latestValidContextGcCheckpoint(records);
37
+ return {
38
+ automaticTitlePending: automaticTitle && !sawUserTurn,
39
+ todo: { revision: todoRevision, items: todoItems },
40
+ contextRecords: boundary < 0 ? [...records] : records.slice(boundary),
41
+ resume: {
42
+ version: 1,
43
+ ...(compactSequence === undefined ? {} : { compactSequence }),
44
+ ...(checkpoint === undefined
45
+ ? {}
46
+ : {
47
+ gcSequence: checkpoint.record.sequence,
48
+ gcGeneration: checkpoint.payload.generation,
49
+ gcBaseSequence: contextGcBaseSequence(checkpoint.payload.base)
50
+ }),
51
+ ...(todoSequence === undefined ? {} : { todoSequence }),
52
+ todoRevision,
53
+ executionModelIndexed: true,
54
+ firstUserMessageIndexed: true,
55
+ ...(firstUserMessagePreview === undefined ? {} : { firstUserMessagePreview }),
56
+ ...(executionModel === undefined ? {} : executionModel)
57
+ }
58
+ };
59
+ }
60
+ export function userMessagePreviewFromRecord(record) {
61
+ if (record.type !== 'turn.user' ||
62
+ !isObject(record.payload) ||
63
+ record.payload.inputSource === 'parent_agent' ||
64
+ typeof record.payload.prompt !== 'string')
65
+ return undefined;
66
+ const preview = record.payload.prompt.trim().replace(/\s+/gu, ' ').slice(0, 160);
67
+ return preview || undefined;
68
+ }
69
+ export function executionModelFromRecord(record) {
70
+ if (record.type !== 'execution.header' || !isObject(record.payload))
71
+ return undefined;
72
+ const modelId = record.payload.modelId;
73
+ if (typeof modelId !== 'string' || modelId.trim().length === 0)
74
+ return undefined;
75
+ const modelName = record.payload.modelName;
76
+ const reasoningEffort = record.payload.reasoningEffort;
77
+ return {
78
+ modelId,
79
+ ...(typeof modelName === 'string' && modelName.trim().length > 0 ? { modelName } : {}),
80
+ ...(typeof reasoningEffort === 'string' && reasoningEffort.trim().length > 0
81
+ ? { reasoningEffort }
82
+ : {})
83
+ };
84
+ }
85
+ export function compactSummaryFromCheckpoint(record) {
86
+ if (record?.type !== 'compact.completed')
87
+ return undefined;
88
+ const payload = record.payload;
89
+ if (payload === null || typeof payload !== 'object' || Array.isArray(payload))
90
+ return undefined;
91
+ const summary = payload.summary;
92
+ return typeof summary === 'string' && summary.trim().length > 0 ? summary : undefined;
93
+ }
94
+ export function replacementHistoryFromCheckpoint(record) {
95
+ if (compactSummaryFromCheckpoint(record) === undefined)
96
+ return undefined;
97
+ const history = record.payload.replacementHistory;
98
+ if (!Array.isArray(history) ||
99
+ !history.every((message) => message !== null &&
100
+ typeof message === 'object' &&
101
+ message.role === 'user' &&
102
+ typeof message.content === 'string' &&
103
+ (message.metadata === undefined ||
104
+ (parseModelInputMetadata(message.metadata) !== undefined &&
105
+ (message.kind === 'extension_input'
106
+ ? message.metadata.source === 'extension'
107
+ : message.kind === 'user_input' && message.metadata.source !== 'extension'))) &&
108
+ (message.kind === 'user_input' ||
109
+ message.kind === 'extension_input' ||
110
+ (message.kind === 'summary' && message.content.trim().length > 0))) ||
111
+ history.filter((message) => message.kind === 'summary').length !== 1)
112
+ return undefined;
113
+ return history.map(({ role, content, kind, metadata }) => ({
114
+ role,
115
+ content,
116
+ kind,
117
+ ...(parseModelInputMetadata(metadata) === undefined
118
+ ? {}
119
+ : { metadata: parseModelInputMetadata(metadata) })
120
+ }));
121
+ }
122
+ export function parseModelInputMetadata(value) {
123
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
124
+ return undefined;
125
+ const metadata = value;
126
+ if (metadata.source !== 'user' &&
127
+ metadata.source !== 'parent_agent' &&
128
+ metadata.source !== 'extension')
129
+ return undefined;
130
+ return {
131
+ source: metadata.source,
132
+ ...(typeof metadata.recordId === 'string' && metadata.recordId.length <= 128
133
+ ? { recordId: metadata.recordId }
134
+ : {}),
135
+ ...(typeof metadata.extensionId === 'string' && metadata.extensionId.length <= 128
136
+ ? { extensionId: metadata.extensionId }
137
+ : {}),
138
+ ...(metadata.truncated === true ? { truncated: true } : {}),
139
+ ...(metadata.smartCompaction === true ? { smartCompaction: true } : {})
140
+ };
141
+ }
142
+ export function inputMetadataFromRecord(record) {
143
+ const payload = record.payload;
144
+ const source = payload.source;
145
+ return {
146
+ source: record.type === 'extension.input' || payload.inputSource === 'extension'
147
+ ? 'extension'
148
+ : payload.inputSource === 'parent_agent'
149
+ ? 'parent_agent'
150
+ : 'user',
151
+ recordId: record.recordId,
152
+ ...(payload.smartCompaction === true ? { smartCompaction: true } : {}),
153
+ ...(typeof source?.extensionId === 'string' && source.extensionId.length <= 128
154
+ ? { extensionId: source.extensionId }
155
+ : {})
156
+ };
157
+ }
158
+ export function isCompactCheckpoint(record) {
159
+ return compactSummaryFromCheckpoint(record) !== undefined;
160
+ }
161
+ export function sameResumeIndex(left, right) {
162
+ return (left?.version === right.version &&
163
+ left.compactSequence === right.compactSequence &&
164
+ left.gcSequence === right.gcSequence &&
165
+ left.gcGeneration === right.gcGeneration &&
166
+ left.gcBaseSequence === right.gcBaseSequence &&
167
+ left.todoSequence === right.todoSequence &&
168
+ left.todoRevision === right.todoRevision &&
169
+ left.executionModelIndexed === right.executionModelIndexed &&
170
+ left.firstUserMessageIndexed === right.firstUserMessageIndexed &&
171
+ left.firstUserMessagePreview === right.firstUserMessagePreview &&
172
+ left.modelId === right.modelId &&
173
+ left.modelName === right.modelName &&
174
+ left.reasoningEffort === right.reasoningEffort);
175
+ }
176
+ function isObject(value) {
177
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
178
+ }
@@ -0,0 +1,25 @@
1
+ import type { SessionMeta, SessionRecord } from './types.js';
2
+ export declare function renameIfExists(from: string, to: string): Promise<boolean>;
3
+ interface StagedSessionSegments {
4
+ readonly files: readonly {
5
+ readonly path: string;
6
+ readonly temporaryPath: string;
7
+ }[];
8
+ readonly segments: NonNullable<SessionMeta['segments']>;
9
+ }
10
+ export declare function stageSessionSegments(sessionDirectory: string, records: readonly SessionRecord[], operation?: 'rollback' | 'fork'): Promise<StagedSessionSegments>;
11
+ export declare function stageUnreferencedImages(sessionDirectory: string, directoryName: 'input_images' | 'generated_images' | 'tool_results', retained: ReadonlySet<string>, transactionId: string): Promise<Array<{
12
+ from: string;
13
+ to: string;
14
+ trash: string;
15
+ }>>;
16
+ export declare function restoreRenamedFiles(files: readonly {
17
+ readonly from: string;
18
+ readonly to: string;
19
+ }[]): Promise<void>;
20
+ export declare function retainedSessionTitle(records: readonly SessionRecord[], fallback: string): string;
21
+ export declare function forkedPayload(type: string, value: unknown, sourceSessionId: string, targetSessionId: string, title: string, identities: ReadonlyMap<string, {
22
+ recordId: string;
23
+ sequence: number;
24
+ }>): unknown;
25
+ export {};
@@ -0,0 +1,157 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { open, readdir, rename, rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { MarAgentError } from '../../error.js';
5
+ import { MAX_SEGMENT_BYTES } from './records.js';
6
+ export async function renameIfExists(from, to) {
7
+ try {
8
+ await rename(from, to);
9
+ return true;
10
+ }
11
+ catch (error) {
12
+ if (error.code === 'ENOENT')
13
+ return false;
14
+ throw error;
15
+ }
16
+ }
17
+ export async function stageSessionSegments(sessionDirectory, records, operation = 'rollback') {
18
+ let group;
19
+ if (records.length === 0)
20
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.');
21
+ const transactionId = randomUUID();
22
+ const files = [];
23
+ const segments = [];
24
+ const flush = async () => {
25
+ if (group === undefined)
26
+ return;
27
+ const number = segments.length + 1;
28
+ const path = join(sessionDirectory, `events-${String(number).padStart(6, '0')}.jsonl`);
29
+ const temporaryPath = `${path}.${transactionId}.${operation}-tmp`;
30
+ const handle = await open(temporaryPath, 'wx', 0o600);
31
+ files.push({ path, temporaryPath });
32
+ try {
33
+ await handle.writeFile(group.lines.join(''));
34
+ await handle.sync();
35
+ }
36
+ finally {
37
+ await handle.close();
38
+ }
39
+ segments.push({
40
+ number,
41
+ firstSequence: group.firstSequence,
42
+ lastSequence: group.lastSequence,
43
+ bytes: group.bytes
44
+ });
45
+ group = undefined;
46
+ };
47
+ try {
48
+ for (const record of records) {
49
+ const line = `${JSON.stringify(record)}\n`;
50
+ const bytes = Buffer.byteLength(line);
51
+ if (group !== undefined && group.bytes > 0 && group.bytes + bytes > MAX_SEGMENT_BYTES)
52
+ await flush();
53
+ if (group === undefined) {
54
+ group = {
55
+ lines: [],
56
+ bytes: 0,
57
+ firstSequence: record.sequence,
58
+ lastSequence: record.sequence
59
+ };
60
+ }
61
+ group.lines.push(line);
62
+ group.bytes += bytes;
63
+ group.lastSequence = record.sequence;
64
+ }
65
+ await flush();
66
+ return { files, segments };
67
+ }
68
+ catch (error) {
69
+ await Promise.all(files.map((item) => rm(item.temporaryPath, { force: true }))).catch(() => undefined);
70
+ throw error;
71
+ }
72
+ }
73
+ export async function stageUnreferencedImages(sessionDirectory, directoryName, retained, transactionId) {
74
+ const directory = join(sessionDirectory, directoryName);
75
+ let names;
76
+ try {
77
+ names = await readdir(directory);
78
+ }
79
+ catch (error) {
80
+ if (error.code === 'ENOENT')
81
+ return [];
82
+ throw error;
83
+ }
84
+ const moved = [];
85
+ try {
86
+ for (const name of names) {
87
+ if (retained.has(name))
88
+ continue;
89
+ const from = join(directory, name);
90
+ const trash = join(directory, `.${name}.${transactionId}.rollback-trash`);
91
+ await rename(from, trash);
92
+ moved.push({ from: trash, to: from, trash });
93
+ }
94
+ return moved;
95
+ }
96
+ catch (error) {
97
+ await restoreRenamedFiles(moved);
98
+ throw error;
99
+ }
100
+ }
101
+ export async function restoreRenamedFiles(files) {
102
+ for (const file of files.toReversed())
103
+ await rename(file.from, file.to).catch(() => undefined);
104
+ }
105
+ export function retainedSessionTitle(records, fallback) {
106
+ let title = fallback;
107
+ for (const record of records) {
108
+ if ((record.type === 'session.created' || record.type === 'session.title.updated') &&
109
+ isObject(record.payload) &&
110
+ typeof record.payload.title === 'string' &&
111
+ record.payload.title.trim().length > 0)
112
+ title = record.payload.title;
113
+ }
114
+ return title;
115
+ }
116
+ export function forkedPayload(type, value, sourceSessionId, targetSessionId, title, identities) {
117
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
118
+ return value;
119
+ const payload = { ...value };
120
+ if (payload.sessionId === sourceSessionId)
121
+ payload.sessionId = targetSessionId;
122
+ const application = (input) => {
123
+ if (typeof input.inputRef !== 'string')
124
+ return input;
125
+ const identity = identities.get(input.inputRef);
126
+ if (!identity)
127
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Fork input reference is missing.');
128
+ return {
129
+ ...input,
130
+ inputRef: identity.recordId,
131
+ ...(isObject(input.metadata)
132
+ ? { metadata: { ...input.metadata, recordId: identity.recordId } }
133
+ : {})
134
+ };
135
+ };
136
+ if (type === 'model.context')
137
+ return application(payload);
138
+ if (type === 'compact.completed') {
139
+ if (Array.isArray(payload.retainedModelContext))
140
+ payload.retainedModelContext = payload.retainedModelContext.map((item) => isObject(item) ? application(item) : item);
141
+ if (Array.isArray(payload.pendingInputs))
142
+ payload.pendingInputs = payload.pendingInputs.map((record) => {
143
+ const identity = identities.get(record.recordId);
144
+ if (!identity)
145
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Fork pending input reference is missing.');
146
+ return { ...record, ...identity, sessionId: targetSessionId };
147
+ });
148
+ }
149
+ if (type === 'session.title.updated') {
150
+ payload.title = title;
151
+ payload.automatic = false;
152
+ }
153
+ return payload;
154
+ }
155
+ function isObject(value) {
156
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
157
+ }
@@ -0,0 +1,21 @@
1
+ import type { SessionInputImage, SessionInputImageMetadata, SessionRecord } from './types.js';
2
+ export declare const MAX_INPUT_IMAGES = 5;
3
+ export declare function generatedImageFileNames(records: readonly SessionRecord[]): readonly string[];
4
+ export declare function copyGeneratedImages(sourceSessionDirectory: string, targetSessionDirectory: string, fileNames: readonly string[]): Promise<void>;
5
+ interface InputImageReference {
6
+ readonly name: string;
7
+ readonly fileName: string;
8
+ readonly mimeType: string;
9
+ readonly byteSize: number;
10
+ readonly sha256: string;
11
+ }
12
+ export declare function inputImageReferencesFromRecords(records: readonly SessionRecord[]): readonly InputImageReference[];
13
+ export declare function inputImageReferences(value: unknown): InputImageReference[];
14
+ export declare function inputImageReference(value: unknown): InputImageReference | undefined;
15
+ export declare function validInputImageFileName(fileName: string): boolean;
16
+ export declare function readInputImageBytes(directory: string, reference: InputImageReference): Promise<Buffer>;
17
+ export declare function copyInputImages(sourceSessionDirectory: string, targetSessionDirectory: string, references: readonly InputImageReference[]): Promise<void>;
18
+ /** A pending marker is removable only after the committed JSONL references are checked. */
19
+ export declare function recoverPendingInputImages(sessionDirectory: string, readHistory: () => Promise<SessionRecord[]>): Promise<void>;
20
+ export declare function writeInputImages(directory: string, images: readonly SessionInputImage[]): Promise<SessionInputImageMetadata[]>;
21
+ export {};