@adhdev/daemon-core 1.0.19 → 1.0.21-rc.1
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/index.js +129 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +129 -6
- package/dist/index.mjs.map +1 -1
- package/dist/providers/auto-approve-modes.d.ts +1 -1
- package/dist/providers/spec/native-history-executor.d.ts +38 -0
- package/package.json +3 -3
- package/src/mesh/mesh-events-utils.ts +1 -1
- package/src/providers/auto-approve-modes.ts +7 -1
- package/src/providers/provider-loader.ts +25 -2
- package/src/providers/spec/native-history-executor.ts +211 -0
|
@@ -10,5 +10,5 @@ export declare function deriveAutoApproveModeRisk(mode: Pick<AutoApproveMode, 'r
|
|
|
10
10
|
* Resolve new mode settings before the legacy boolean. A stale/unknown explicit
|
|
11
11
|
* mode id fails closed instead of falling through to an enabled legacy setting.
|
|
12
12
|
*/
|
|
13
|
-
export declare function resolveProviderAutoApproveMode(provider: ProviderModule, settings: Record<string, unknown> | undefined): ResolvedAutoApproveMode;
|
|
13
|
+
export declare function resolveProviderAutoApproveMode(provider: ProviderModule | null | undefined, settings: Record<string, unknown> | undefined): ResolvedAutoApproveMode;
|
|
14
14
|
export declare function findProviderAutoApproveMode(provider: ProviderModule | undefined, modeId: string): AutoApproveMode | undefined;
|
|
@@ -39,6 +39,44 @@ export interface NativeHistoryResult {
|
|
|
39
39
|
workspace?: string;
|
|
40
40
|
}
|
|
41
41
|
export declare function executeNativeHistory(cfg: NativeHistoryConfig, input: NativeHistoryInput): NativeHistoryResult | null;
|
|
42
|
+
/**
|
|
43
|
+
* One enumerated saved session. Structurally matches the fields the chat-history
|
|
44
|
+
* `list_saved_sessions` pipeline expects (`normalizeProviderNativeHistorySessionSummary`
|
|
45
|
+
* reads exactly these keys), so the executor can be wired straight into
|
|
46
|
+
* `listNativeHistory` with no daemon-side adapter.
|
|
47
|
+
*/
|
|
48
|
+
export interface NativeHistorySessionListItem {
|
|
49
|
+
historySessionId: string;
|
|
50
|
+
sessionTitle?: string;
|
|
51
|
+
messageCount: number;
|
|
52
|
+
firstMessageAt: number;
|
|
53
|
+
lastMessageAt: number;
|
|
54
|
+
preview?: string;
|
|
55
|
+
workspace?: string;
|
|
56
|
+
sourcePath: string;
|
|
57
|
+
sourceMtimeMs: number;
|
|
58
|
+
}
|
|
59
|
+
export interface NativeHistoryListResult {
|
|
60
|
+
sessions: NativeHistorySessionListItem[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Enumerate every on-disk saved session for a declarative jsonl source.
|
|
64
|
+
*
|
|
65
|
+
* Where `executeNativeHistory` resolves the ONE file for a pinned/current
|
|
66
|
+
* session, this walks the whole store: it turns the source `path` template into
|
|
67
|
+
* a directory glob (per-session template vars — {session_id}, {cwd*}, the date
|
|
68
|
+
* segments — collapse to `*`) and lists every file matching the leaf pattern
|
|
69
|
+
* across all matched dirs. Each file becomes one session summary. session_id is
|
|
70
|
+
* extracted the same way the reader does (`session_id_from`), and per-session
|
|
71
|
+
* preview/messageCount/first/last come from a MINIMAL projection (first + last
|
|
72
|
+
* projected message only, no full-array build).
|
|
73
|
+
*
|
|
74
|
+
* Without this, the loader wired only the read function and dropped the list
|
|
75
|
+
* marker, so `list_saved_sessions` always returned `[]` for every v2.0
|
|
76
|
+
* declarative-source provider (claude/codex/antigravity/kimi/cursor) even with
|
|
77
|
+
* thousands of transcripts on disk.
|
|
78
|
+
*/
|
|
79
|
+
export declare function executeNativeHistoryList(cfg: NativeHistoryConfig, input?: NativeHistoryInput): NativeHistoryListResult | null;
|
|
42
80
|
/**
|
|
43
81
|
* Resolve the concrete on-disk transcript file a jsonl native-history source
|
|
44
82
|
* points at, applying the same slug/date/session-bound/scan fallbacks the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21-rc.1",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"author": "vilmire",
|
|
48
48
|
"license": "AGPL-3.0-or-later",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@adhdev/mesh-shared": "
|
|
51
|
-
"@adhdev/session-host-core": "
|
|
50
|
+
"@adhdev/mesh-shared": "1.0.21-rc.1",
|
|
51
|
+
"@adhdev/session-host-core": "1.0.21-rc.1",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -425,7 +425,7 @@ export function buildMeshSystemMessage(args: {
|
|
|
425
425
|
if (args.worktreeHasQueuedTask) {
|
|
426
426
|
return `${prefix} The worktree is ready; a queued task targeting this node will auto-claim it — no manual \`mesh_launch_session\` needed.`;
|
|
427
427
|
}
|
|
428
|
-
return `${prefix} The worktree is ready —
|
|
428
|
+
return `${prefix} The worktree is ready. If a task is already queued for this worktree, auto-launch will claim it — no action needed. Launch a session manually only if you need one and none exists yet.`;
|
|
429
429
|
}
|
|
430
430
|
if (args.event === 'worktree_bootstrap_failed') {
|
|
431
431
|
const error = readNonEmptyString(args.metadataEvent.error);
|
|
@@ -61,9 +61,15 @@ function resolveConfiguredMode(
|
|
|
61
61
|
* mode id fails closed instead of falling through to an enabled legacy setting.
|
|
62
62
|
*/
|
|
63
63
|
export function resolveProviderAutoApproveMode(
|
|
64
|
-
provider: ProviderModule,
|
|
64
|
+
provider: ProviderModule | null | undefined,
|
|
65
65
|
settings: Record<string, unknown> | undefined,
|
|
66
66
|
): ResolvedAutoApproveMode {
|
|
67
|
+
// No provider (e.g. an instance whose module isn't set when a modal-park /
|
|
68
|
+
// auto-approve classification runs) ⇒ auto-approve is inactive. this.provider is
|
|
69
|
+
// genuinely nullable at runtime (the instance reads this.provider?.nativeHistory
|
|
70
|
+
// elsewhere), and the sibling helper findProviderAutoApproveMode already guards
|
|
71
|
+
// with provider?.autoApproveModes. Guard the entry so every caller is safe.
|
|
72
|
+
if (!provider) return inactiveMode();
|
|
67
73
|
const config = provider.autoApproveModes;
|
|
68
74
|
const explicitModeId = settings?.autoApproveMode;
|
|
69
75
|
if (typeof explicitModeId === 'string') {
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
resolveProviderTarballTarget,
|
|
44
44
|
} from '../config/registry-resolver.js';
|
|
45
45
|
import type { ProviderSourceConfigSnapshot, ProviderUserDirSource } from '../config/provider-source-config.js';
|
|
46
|
-
import { executeNativeHistory } from './spec/native-history-executor.js';
|
|
46
|
+
import { executeNativeHistory, executeNativeHistoryList } from './spec/native-history-executor.js';
|
|
47
47
|
import { createNativeHistoryDispatcher, type ReaderId } from './native-history/dispatcher.js';
|
|
48
48
|
|
|
49
49
|
/**
|
|
@@ -1408,11 +1408,23 @@ export class ProviderLoader {
|
|
|
1408
1408
|
}
|
|
1409
1409
|
if (nh) {
|
|
1410
1410
|
let reader: ((input: any) => any) | null = null;
|
|
1411
|
+
// lister enumerates all saved sessions for the store. Only the
|
|
1412
|
+
// declarative jsonl `source` path can enumerate by directory walk;
|
|
1413
|
+
// override/reader providers wire their own listSessions (or none).
|
|
1414
|
+
let lister: ((input: any) => any) | null = null;
|
|
1411
1415
|
let format = 'spec';
|
|
1412
1416
|
|
|
1413
1417
|
if (nh.source) {
|
|
1414
1418
|
format = `spec-${nh.source.kind}`;
|
|
1415
1419
|
reader = (input: any) => executeNativeHistory(nh, input);
|
|
1420
|
+
// Only jsonl stores are file-per-session and enumerable by a
|
|
1421
|
+
// directory walk. sqlite sources enumerate through their own
|
|
1422
|
+
// `session_query` (not implemented as a lister yet), so leave
|
|
1423
|
+
// listSessions unwired there rather than advertising an enumerator
|
|
1424
|
+
// that always returns empty.
|
|
1425
|
+
if (nh.source.kind === 'jsonl') {
|
|
1426
|
+
lister = (input: any) => executeNativeHistoryList(nh, input);
|
|
1427
|
+
}
|
|
1416
1428
|
} else if (nh.override_path) {
|
|
1417
1429
|
const overrideFile = path.resolve(providerDir, nh.override_path);
|
|
1418
1430
|
if (fs.existsSync(overrideFile)) {
|
|
@@ -1437,10 +1449,21 @@ export class ProviderLoader {
|
|
|
1437
1449
|
if (reader) {
|
|
1438
1450
|
resolved.scripts = { ...(resolved.scripts || {}) };
|
|
1439
1451
|
(resolved.scripts as any).readNativeHistory = reader;
|
|
1452
|
+
// Wire the enumerator alongside the reader. Without both the
|
|
1453
|
+
// `scripts.listSessions` marker AND the `listNativeHistory` fn,
|
|
1454
|
+
// `getProviderNativeHistoryScript(...,'listSessions')` resolves to
|
|
1455
|
+
// undefined and `list_saved_sessions` returns [] for every
|
|
1456
|
+
// declarative-source provider (claude/codex/antigravity/kimi/cursor)
|
|
1457
|
+
// regardless of how many transcripts are on disk.
|
|
1458
|
+
const scriptsMarker: { readSession: string; listSessions?: string } = { readSession: 'readNativeHistory' };
|
|
1459
|
+
if (lister) {
|
|
1460
|
+
(resolved.scripts as any).listNativeHistory = lister;
|
|
1461
|
+
scriptsMarker.listSessions = 'listNativeHistory';
|
|
1462
|
+
}
|
|
1440
1463
|
(resolved as any).nativeHistory = {
|
|
1441
1464
|
format,
|
|
1442
1465
|
watchPath: undefined,
|
|
1443
|
-
scripts:
|
|
1466
|
+
scripts: scriptsMarker,
|
|
1444
1467
|
mode: 'native-source',
|
|
1445
1468
|
};
|
|
1446
1469
|
}
|
|
@@ -83,6 +83,217 @@ export function executeNativeHistory(cfg: NativeHistoryConfig, input: NativeHist
|
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* One enumerated saved session. Structurally matches the fields the chat-history
|
|
88
|
+
* `list_saved_sessions` pipeline expects (`normalizeProviderNativeHistorySessionSummary`
|
|
89
|
+
* reads exactly these keys), so the executor can be wired straight into
|
|
90
|
+
* `listNativeHistory` with no daemon-side adapter.
|
|
91
|
+
*/
|
|
92
|
+
export interface NativeHistorySessionListItem {
|
|
93
|
+
historySessionId: string;
|
|
94
|
+
sessionTitle?: string;
|
|
95
|
+
messageCount: number;
|
|
96
|
+
firstMessageAt: number;
|
|
97
|
+
lastMessageAt: number;
|
|
98
|
+
preview?: string;
|
|
99
|
+
workspace?: string;
|
|
100
|
+
sourcePath: string;
|
|
101
|
+
sourceMtimeMs: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface NativeHistoryListResult {
|
|
105
|
+
sessions: NativeHistorySessionListItem[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Enumerate every on-disk saved session for a declarative jsonl source.
|
|
110
|
+
*
|
|
111
|
+
* Where `executeNativeHistory` resolves the ONE file for a pinned/current
|
|
112
|
+
* session, this walks the whole store: it turns the source `path` template into
|
|
113
|
+
* a directory glob (per-session template vars — {session_id}, {cwd*}, the date
|
|
114
|
+
* segments — collapse to `*`) and lists every file matching the leaf pattern
|
|
115
|
+
* across all matched dirs. Each file becomes one session summary. session_id is
|
|
116
|
+
* extracted the same way the reader does (`session_id_from`), and per-session
|
|
117
|
+
* preview/messageCount/first/last come from a MINIMAL projection (first + last
|
|
118
|
+
* projected message only, no full-array build).
|
|
119
|
+
*
|
|
120
|
+
* Without this, the loader wired only the read function and dropped the list
|
|
121
|
+
* marker, so `list_saved_sessions` always returned `[]` for every v2.0
|
|
122
|
+
* declarative-source provider (claude/codex/antigravity/kimi/cursor) even with
|
|
123
|
+
* thousands of transcripts on disk.
|
|
124
|
+
*/
|
|
125
|
+
export function executeNativeHistoryList(cfg: NativeHistoryConfig, input?: NativeHistoryInput): NativeHistoryListResult | null {
|
|
126
|
+
if (!cfg?.source) return null;
|
|
127
|
+
// sqlite sources enumerate through their own `session_query`; only jsonl
|
|
128
|
+
// stores are file-per-session and enumerable by directory walk here.
|
|
129
|
+
if (cfg.source.kind !== 'jsonl') return null;
|
|
130
|
+
return { sessions: enumerateJsonlSessions(cfg.source, input ?? {}) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
134
|
+
// JSONL enumeration (list_saved_sessions)
|
|
135
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function enumerateJsonlSessions(src: NativeHistoryJsonlSource, input: NativeHistoryInput): NativeHistorySessionListItem[] {
|
|
138
|
+
const files = enumerateSessionFiles(src, input);
|
|
139
|
+
const shapes = compileRecordShapes(src);
|
|
140
|
+
const out: NativeHistorySessionListItem[] = [];
|
|
141
|
+
const seen = new Set<string>();
|
|
142
|
+
for (const filePath of files) {
|
|
143
|
+
const item = summarizeSessionFile(src, filePath, shapes);
|
|
144
|
+
if (!item) continue;
|
|
145
|
+
// A store can surface the same session from more than one matched dir
|
|
146
|
+
// (glob overlap). Keep the newest-touched instance per session id.
|
|
147
|
+
const key = item.historySessionId.toLowerCase();
|
|
148
|
+
if (seen.has(key)) {
|
|
149
|
+
const existing = out.find(s => s.historySessionId.toLowerCase() === key);
|
|
150
|
+
if (existing && item.sourceMtimeMs > existing.sourceMtimeMs) {
|
|
151
|
+
out[out.indexOf(existing)] = item;
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
seen.add(key);
|
|
156
|
+
out.push(item);
|
|
157
|
+
}
|
|
158
|
+
out.sort((a, b) => b.lastMessageAt - a.lastMessageAt);
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolve the source `path` template into every concrete transcript file on
|
|
164
|
+
* disk. Per-session template vars ({session_id}, {cwd*}, {yyyy}/{mm}/{dd})
|
|
165
|
+
* collapse to a `*` wildcard so the walk spans all sessions/workspaces/days;
|
|
166
|
+
* literal `*`/`**` segments pass through to `expandDirGlob`.
|
|
167
|
+
*
|
|
168
|
+
* When `file_pattern` is set, the whole `path` is the directory template and
|
|
169
|
+
* `file_pattern` matches the leaf file. Otherwise the last path segment is the
|
|
170
|
+
* file template (e.g. `{session_id}.jsonl`) — its template vars become `*` and
|
|
171
|
+
* it becomes the leaf matcher, while the preceding segments are the directory
|
|
172
|
+
* template.
|
|
173
|
+
*/
|
|
174
|
+
function enumerateSessionFiles(src: NativeHistoryJsonlSource, input: NativeHistoryInput): string[] {
|
|
175
|
+
const expandedRoot = expandTemplateRootForEnumeration(src.path, input);
|
|
176
|
+
if (!expandedRoot) return [];
|
|
177
|
+
|
|
178
|
+
let dirTemplate: string;
|
|
179
|
+
let fileRegex: RegExp;
|
|
180
|
+
if (src.file_pattern) {
|
|
181
|
+
dirTemplate = templateVarsToGlob(expandedRoot);
|
|
182
|
+
fileRegex = globToRegex(src.file_pattern);
|
|
183
|
+
} else {
|
|
184
|
+
const idx = expandedRoot.lastIndexOf('/');
|
|
185
|
+
const dirPart = idx >= 0 ? expandedRoot.slice(0, idx) : '';
|
|
186
|
+
const leaf = idx >= 0 ? expandedRoot.slice(idx + 1) : expandedRoot;
|
|
187
|
+
dirTemplate = templateVarsToGlob(dirPart);
|
|
188
|
+
fileRegex = globToRegex(templateVarsToGlob(leaf));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const dirs = expandDirGlob(dirTemplate);
|
|
192
|
+
const files: string[] = [];
|
|
193
|
+
for (const d of dirs) {
|
|
194
|
+
for (const p of listMatchingFiles(d, fileRegex)) files.push(p);
|
|
195
|
+
}
|
|
196
|
+
return files;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Expand the leading `~` and `${ENV}` portions of a path template WITHOUT
|
|
201
|
+
* substituting per-session vars, so the caller can decide which of those become
|
|
202
|
+
* enumeration wildcards. Mirrors the head of `expandPath` (tilde + env) but
|
|
203
|
+
* leaves `{...}` template markers intact.
|
|
204
|
+
*/
|
|
205
|
+
function expandTemplateRootForEnumeration(template: string, input: NativeHistoryInput): string {
|
|
206
|
+
if (!template) return '';
|
|
207
|
+
let out = template;
|
|
208
|
+
if (out.startsWith('~/') || out === '~') out = path.join(os.homedir(), out.slice(2));
|
|
209
|
+
out = out.replace(/\$\{([A-Z_][A-Z0-9_]*)(?::-(.*?))?\}/g, (_m, name, fallback) => {
|
|
210
|
+
const v = input.envOverrides?.[name] ?? process.env[name];
|
|
211
|
+
return v != null && v !== '' ? v : (fallback ?? '');
|
|
212
|
+
});
|
|
213
|
+
if (out.startsWith('~/')) out = path.join(os.homedir(), out.slice(2));
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Turn every remaining `{var}` template marker into a `*` glob segment. */
|
|
218
|
+
function templateVarsToGlob(template: string): string {
|
|
219
|
+
return template.replace(/\{[a-zA-Z_][a-zA-Z0-9_]*\}/g, '*');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Extract the session id from a resolved transcript file exactly the way the
|
|
224
|
+
* reader (`executeJsonl`) does, honouring `session_id_from`. Returns '' when no
|
|
225
|
+
* id can be derived so the caller can drop the file.
|
|
226
|
+
*/
|
|
227
|
+
function sessionIdForFile(src: NativeHistoryJsonlSource, filePath: string): string {
|
|
228
|
+
if (src.session_id_from === 'first_record' && src.session_id_path) {
|
|
229
|
+
const lines = readJsonlLines(filePath);
|
|
230
|
+
if (lines.length > 0) {
|
|
231
|
+
const v = jsonPathGet(lines[0], src.session_id_path);
|
|
232
|
+
if (typeof v === 'string' && v) return v;
|
|
233
|
+
}
|
|
234
|
+
return '';
|
|
235
|
+
}
|
|
236
|
+
if (src.session_id_from === 'dir_uuid') {
|
|
237
|
+
return dirUuid(filePath) || '';
|
|
238
|
+
}
|
|
239
|
+
// filename_uuid (explicit or default).
|
|
240
|
+
return filenameUuid(filePath);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build one session summary from a transcript file with a MINIMAL parse: project
|
|
245
|
+
* records through the same record-shape machinery the reader uses, but keep only
|
|
246
|
+
* the running count plus the first and last projected message (no full-array
|
|
247
|
+
* materialization). preview/sessionTitle come from the last non-tool message.
|
|
248
|
+
*/
|
|
249
|
+
function summarizeSessionFile(
|
|
250
|
+
src: NativeHistoryJsonlSource,
|
|
251
|
+
filePath: string,
|
|
252
|
+
shapes: { pick: (record: any) => { map: NativeHistoryMessageMap } | null },
|
|
253
|
+
): NativeHistorySessionListItem | null {
|
|
254
|
+
const historySessionId = sessionIdForFile(src, filePath);
|
|
255
|
+
if (!historySessionId) return null;
|
|
256
|
+
|
|
257
|
+
const mtime = safeMtimeMs(filePath);
|
|
258
|
+
const lines = readJsonlLines(filePath);
|
|
259
|
+
if (lines.length === 0) return null;
|
|
260
|
+
|
|
261
|
+
let messageCount = 0;
|
|
262
|
+
let first: NativeHistoryMessage | null = null;
|
|
263
|
+
let last: NativeHistoryMessage | null = null;
|
|
264
|
+
let lastNonTool: NativeHistoryMessage | null = null;
|
|
265
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
266
|
+
const rec = lines[i];
|
|
267
|
+
const shape = shapes.pick(rec);
|
|
268
|
+
if (!shape) continue;
|
|
269
|
+
for (const msg of projectMessages(rec, shape.map, i, lines.length, mtime)) {
|
|
270
|
+
messageCount += 1;
|
|
271
|
+
if (!first) first = msg;
|
|
272
|
+
last = msg;
|
|
273
|
+
if (msg.kind !== 'tool') lastNonTool = msg;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (messageCount === 0 || !first || !last) return null;
|
|
277
|
+
|
|
278
|
+
// Workspace attribution mirrors the reader: prefer an in-transcript
|
|
279
|
+
// session_meta cwd, then a sidecar, then the (verified) input workspace.
|
|
280
|
+
const workspace = readSessionMetaWorkspace(lines)
|
|
281
|
+
?? (src.workspace_from_sidecar ? readSidecarWorkspace(filePath, src.workspace_from_sidecar) : undefined);
|
|
282
|
+
|
|
283
|
+
const previewMsg = lastNonTool ?? last;
|
|
284
|
+
return {
|
|
285
|
+
historySessionId,
|
|
286
|
+
sessionTitle: previewMsg.content || undefined,
|
|
287
|
+
messageCount,
|
|
288
|
+
firstMessageAt: first.receivedAt || mtime,
|
|
289
|
+
lastMessageAt: last.receivedAt || first.receivedAt || mtime,
|
|
290
|
+
preview: previewMsg.content || undefined,
|
|
291
|
+
workspace,
|
|
292
|
+
sourcePath: filePath,
|
|
293
|
+
sourceMtimeMs: mtime,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
86
297
|
// ────────────────────────────────────────────────────────────────────────────
|
|
87
298
|
// JSONL
|
|
88
299
|
// ────────────────────────────────────────────────────────────────────────────
|