@adhdev/daemon-core 1.0.20 → 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.
@@ -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.20",
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 — use \`mesh_launch_session\` to start an agent.`;
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);
@@ -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: { readSession: 'readNativeHistory' },
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
  // ────────────────────────────────────────────────────────────────────────────