@adhdev/daemon-core 0.9.82-rc.534 → 0.9.82-rc.536

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.
@@ -51,6 +51,19 @@ export interface TranscriptPtySpec {
51
51
  * so the plain assistant answer is the only assistant bubble left. Matched
52
52
  * against the raw pre-strip line; the visible text is still ANSI-stripped
53
53
  * for the bubble content.
54
+ *
55
+ * Value-agnostic mode: the sentinel "*" (or "48;*" / "bg") matches ANY
56
+ * background-color SGR — 256-color `48;5;<n>` OR truecolor `48;2;<r>;<g>;<b>`
57
+ * — instead of an allowlist of exact color numbers. This is the robust form:
58
+ * the earlier allowlist bug recurred every time a TUI changed its exact box
59
+ * color (cursor `48;5;233` guess, opencode rc.531), because a hard-coded
60
+ * color that no longer matched let the boxed user echo fall through to the
61
+ * permissive assistantPrefix. The sentinel keys off the *presence* of a
62
+ * background-color param group — the structural signal of a boxed turn —
63
+ * so a genuine assistant line (no background SGR) is still classified
64
+ * assistant. Chrome patterns are filtered BEFORE this check, so bg-colored
65
+ * chrome (e.g. the "→ Add a follow-up" composer footer) is already excluded
66
+ * and does not become a spurious user bubble.
54
67
  */
55
68
  userBackgroundSgr?: string[];
56
69
  }
@@ -76,6 +76,23 @@ export interface NativeHistoryJsonlSource {
76
76
  message_filter?: {
77
77
  where: string;
78
78
  };
79
+ /**
80
+ * Fallback workspace attribution when the transcript carries no
81
+ * `session_meta` cwd record. Some stores (cursor-agent) do NOT write a
82
+ * session_meta line and keep the workspace only in the on-disk project-slug
83
+ * directory (`~/.cursor/projects/<slug>/…`), which is a lossy, sometimes
84
+ * truncated+hashed transform of the real path and cannot be reversed. When
85
+ * this flag is set and no session_meta workspace was found, the executor
86
+ * stamps `input.workspace` onto each message — but ONLY after confirming the
87
+ * resolved file actually lives under that workspace's project slug (a path
88
+ * segment matches the input workspace's cursor/claude slug, allowing for
89
+ * cursor's length-truncation). This closes the first-turn read gap (before a
90
+ * provider session id is pinned, the downstream hasSafeNativeHistoryMapping
91
+ * guard needs a per-message workspace) without risking cross-workspace
92
+ * aliasing: a slug mismatch leaves the workspace unset and the read fails
93
+ * closed as before.
94
+ */
95
+ workspace_from_input?: boolean;
79
96
  message_map: NativeHistoryMessageMap;
80
97
  }
81
98
  export interface NativeHistorySqliteSource {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.534",
3
+ "version": "0.9.82-rc.536",
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": "0.9.82-rc.534",
51
- "@adhdev/session-host-core": "0.9.82-rc.534",
50
+ "@adhdev/mesh-shared": "0.9.82-rc.536",
51
+ "@adhdev/session-host-core": "0.9.82-rc.536",
52
52
  "@agentclientprotocol/sdk": "^0.16.1",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
@@ -1206,7 +1206,19 @@ export class CliStateEngine {
1206
1206
  const detectFn = typeof this.transport.runDetectStatus === 'function'
1207
1207
  ? () => this.transport.runDetectStatus!(snap.recentOutputBuffer)
1208
1208
  : () => this.runDetectStatus(snap);
1209
- const latestStatus = detectFn() || this.currentStatus;
1209
+ // Only a POSITIVE `generating` verdict from the live detector defers the
1210
+ // finish. A null verdict is "no cue matched", NOT "still generating" — for
1211
+ // a provider whose only idle cue is a composer placeholder (opencode's
1212
+ // `Ask anything`) that can momentarily fall out of the captured frame while
1213
+ // the TUI redraws its status chip, detectStatus returns null under the
1214
+ // manifest's `onNoMatch: preserve-last` policy. Collapsing that null to
1215
+ // `this.currentStatus` (which is `generating` while the hold is armed) made
1216
+ // the finish defer on EVERY tick, so the completion never fired and the
1217
+ // session wedged in `generating` forever even though its assistant reply had
1218
+ // already landed in native-history. Treat null as "no evidence to defer" and
1219
+ // let the idle-finish proceed; a real in-flight turn still re-reports a
1220
+ // positive `generating` here and defers as before.
1221
+ const latestStatus = detectFn();
1210
1222
  if (latestStatus === 'generating') {
1211
1223
  this.evaluateSettled(snap);
1212
1224
  return true;
@@ -1339,13 +1339,24 @@ export class ProviderLoader {
1339
1339
  candidates.push(path.join(providerDir, 'specs', 'default.json'));
1340
1340
  candidates.push(path.join(providerDir, 'spec.json'));
1341
1341
  const specPath = candidates.find((p: string) => fs.existsSync(p));
1342
+ // native_history block, resolved from either the separate spec file
1343
+ // (snake_case `native_history`) or — for v1-manifest-only providers that
1344
+ // ship no specs/*.json — the inline camelCase `nativeHistory` on the
1345
+ // manifest itself. The separate spec file wins when both exist. Without
1346
+ // the v1-manifest fallback, a provider whose ONLY declaration is an
1347
+ // inline `nativeHistory.source` (e.g. opencode's sqlite source) never got
1348
+ // its `scripts.readNativeHistory` wired: the whole block was gated on
1349
+ // `specPath`, so read_chat returned native-unavailable, the assistant
1350
+ // reply (only in the on-disk store, never in the PTY snapshot) was
1351
+ // dropped, providerSessionId stayed null, and the session wedged in
1352
+ // `generating` because no native completion evidence ever arrived.
1353
+ let nh: any | undefined;
1342
1354
  if (specPath) {
1343
1355
  // Hand the resolved spec path off to route.ts via a hidden field
1344
1356
  // so the routing layer doesn't have to repeat the candidate walk.
1345
1357
  (resolved as any)._resolvedSpecPath = specPath;
1346
1358
  // Extract control_bar + native_history directly from the JSON header.
1347
1359
  let specControls: any[] | undefined;
1348
- let nh: any | undefined;
1349
1360
  try {
1350
1361
  const rawSpec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
1351
1362
  specControls = rawSpec.control_bar;
@@ -1382,44 +1393,56 @@ export class ProviderLoader {
1382
1393
  }
1383
1394
  }
1384
1395
  }
1385
- if (nh) {
1386
- let reader: ((input: any) => any) | null = null;
1387
- let format = 'spec';
1388
-
1389
- if (nh.source) {
1390
- format = `spec-${nh.source.kind}`;
1391
- reader = (input: any) => executeNativeHistory(nh, input);
1392
- } else if (nh.override_path) {
1393
- const overrideFile = path.resolve(providerDir, nh.override_path);
1394
- if (fs.existsSync(overrideFile)) {
1395
- try {
1396
- registerProviderScriptRootSafely(path.dirname(path.dirname(providerDir)));
1397
- delete require.cache[require.resolve(overrideFile)];
1398
- // eslint-disable-next-line @typescript-eslint/no-var-requires
1399
- const mod = require(overrideFile);
1400
- const fn = typeof mod === 'function' ? mod : (mod && typeof mod.default === 'function' ? mod.default : null);
1401
- if (fn) {
1402
- format = 'spec-override';
1403
- reader = (input: any) => fn(input);
1404
- }
1405
- } catch { /* fall through — leave native unavailable */ }
1406
- }
1407
- } else if (nh.reader) {
1408
- const dispatch = createNativeHistoryDispatcher(nh.reader as ReaderId);
1409
- format = nh.reader;
1410
- reader = (input: any) => dispatch(input);
1396
+ }
1397
+ // Fall back to the v1 manifest's inline `nativeHistory` (camelCase) when
1398
+ // no separate spec file provided a `native_history` block. Only treat it
1399
+ // as a declarative reader source when it actually carries source/
1400
+ // override_path/reader — a bare `nativeHistory` marker that only names
1401
+ // `scripts.readSession` (claude/codex/antigravity, whose real reader is
1402
+ // wired from their specs/*.json) must not be mistaken for one.
1403
+ if (!nh) {
1404
+ const inlineNh = (base as any)?.nativeHistory || (resolved as any)?.nativeHistory;
1405
+ if (inlineNh && (inlineNh.source || inlineNh.override_path || inlineNh.reader)) {
1406
+ nh = inlineNh;
1407
+ }
1408
+ }
1409
+ if (nh) {
1410
+ let reader: ((input: any) => any) | null = null;
1411
+ let format = 'spec';
1412
+
1413
+ if (nh.source) {
1414
+ format = `spec-${nh.source.kind}`;
1415
+ reader = (input: any) => executeNativeHistory(nh, input);
1416
+ } else if (nh.override_path) {
1417
+ const overrideFile = path.resolve(providerDir, nh.override_path);
1418
+ if (fs.existsSync(overrideFile)) {
1419
+ try {
1420
+ registerProviderScriptRootSafely(path.dirname(path.dirname(providerDir)));
1421
+ delete require.cache[require.resolve(overrideFile)];
1422
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1423
+ const mod = require(overrideFile);
1424
+ const fn = typeof mod === 'function' ? mod : (mod && typeof mod.default === 'function' ? mod.default : null);
1425
+ if (fn) {
1426
+ format = 'spec-override';
1427
+ reader = (input: any) => fn(input);
1428
+ }
1429
+ } catch { /* fall through — leave native unavailable */ }
1411
1430
  }
1431
+ } else if (nh.reader) {
1432
+ const dispatch = createNativeHistoryDispatcher(nh.reader as ReaderId);
1433
+ format = nh.reader;
1434
+ reader = (input: any) => dispatch(input);
1435
+ }
1412
1436
 
1413
- if (reader) {
1414
- resolved.scripts = { ...(resolved.scripts || {}) };
1415
- (resolved.scripts as any).readNativeHistory = reader;
1416
- (resolved as any).nativeHistory = {
1417
- format,
1418
- watchPath: undefined,
1419
- scripts: { readSession: 'readNativeHistory' },
1420
- mode: 'native-source',
1421
- };
1422
- }
1437
+ if (reader) {
1438
+ resolved.scripts = { ...(resolved.scripts || {}) };
1439
+ (resolved.scripts as any).readNativeHistory = reader;
1440
+ (resolved as any).nativeHistory = {
1441
+ format,
1442
+ watchPath: undefined,
1443
+ scripts: { readSession: 'readNativeHistory' },
1444
+ mode: 'native-source',
1445
+ };
1423
1446
  }
1424
1447
  }
1425
1448
  } catch {
@@ -58,6 +58,19 @@ export interface TranscriptPtySpec {
58
58
  * so the plain assistant answer is the only assistant bubble left. Matched
59
59
  * against the raw pre-strip line; the visible text is still ANSI-stripped
60
60
  * for the bubble content.
61
+ *
62
+ * Value-agnostic mode: the sentinel "*" (or "48;*" / "bg") matches ANY
63
+ * background-color SGR — 256-color `48;5;<n>` OR truecolor `48;2;<r>;<g>;<b>`
64
+ * — instead of an allowlist of exact color numbers. This is the robust form:
65
+ * the earlier allowlist bug recurred every time a TUI changed its exact box
66
+ * color (cursor `48;5;233` guess, opencode rc.531), because a hard-coded
67
+ * color that no longer matched let the boxed user echo fall through to the
68
+ * permissive assistantPrefix. The sentinel keys off the *presence* of a
69
+ * background-color param group — the structural signal of a boxed turn —
70
+ * so a genuine assistant line (no background SGR) is still classified
71
+ * assistant. Chrome patterns are filtered BEFORE this check, so bg-colored
72
+ * chrome (e.g. the "→ Add a follow-up" composer footer) is already excluded
73
+ * and does not become a spurious user bubble.
61
74
  */
62
75
  userBackgroundSgr?: string[]
63
76
  }
@@ -223,7 +236,16 @@ export function buildParseSessionFromTui(spec: ParseSessionTuiSpec): (input: any
223
236
  const userBgList = Array.isArray(spec.transcriptPty.userBackgroundSgr)
224
237
  ? spec.transcriptPty.userBackgroundSgr.map(s => String(s).trim()).filter(Boolean)
225
238
  : []
239
+ // Value-agnostic sentinel: "*" / "48;*" / "bg" matches ANY background-color
240
+ // SGR (256-color `48;5;<n>` OR truecolor `48;2;<r>;<g>;<b>`) as the boxed-user
241
+ // signal, instead of an allowlist of exact colors that breaks whenever a TUI
242
+ // changes its box color. The param group must sit inside an SGR run terminated
243
+ // by `m`; 3-digit color indices / channels are matched structurally.
244
+ const ANY_BG_SENTINELS = new Set(['*', '48;*', 'bg', '48'])
226
245
  const userBgRes = userBgList.map(code => {
246
+ if (ANY_BG_SENTINELS.has(code.toLowerCase())) {
247
+ return new RegExp('\\x1b\\[[0-9;]*\\b48;(?:5;\\d{1,3}|2;\\d{1,3};\\d{1,3};\\d{1,3})[0-9;]*m')
248
+ }
227
249
  // Match the code inside an SGR run: `\x1b[<...>48;5;233<...>m`. The code
228
250
  // (e.g. "48;5;233") appears somewhere in the `;`-separated parameter list
229
251
  // terminated by `m`. Escape regex-special chars in the code first.
@@ -82,7 +82,7 @@
82
82
  "userBackgroundSgr": {
83
83
  "type": "array",
84
84
  "items": { "type": "string" },
85
- "description": "Optional SGR background-color codes (e.g. \"48;5;233\") that mark a USER turn. Some TUIs (cursor-agent) render the user's submitted message and composer echo inside a colored box while rendering the assistant answer as a plain no-background line. After ANSI stripping both are bare lines, so a permissive assistantPrefix grabs the user echo as an assistant bubble. When set, a raw line whose ANSI carries any of these background codes is classified as a user turn regardless of its stripped text, leaving the plain assistant answer as the only assistant bubble. Checked against the raw pre-strip line before prefix matching."
85
+ "description": "Optional SGR background-color codes (e.g. \"48;5;233\") that mark a USER turn. Some TUIs (cursor-agent) render the user's submitted message and composer echo inside a colored box while rendering the assistant answer as a plain no-background line. After ANSI stripping both are bare lines, so a permissive assistantPrefix grabs the user echo as an assistant bubble. When set, a raw line whose ANSI carries any of these background codes is classified as a user turn regardless of its stripped text, leaving the plain assistant answer as the only assistant bubble. Checked against the raw pre-strip line before prefix matching (chrome patterns are filtered first, so bg-colored chrome is not misclassified as a user turn).\n\nValue-agnostic sentinel: the entry \"*\" (also \"48;*\" / \"bg\") matches ANY background-color SGR — 256-color `48;5;<n>` OR truecolor `48;2;<r>;<g>;<b>` — instead of an exact-color allowlist. Prefer \"*\": a hard-coded color re-breaks every time the TUI changes its box color (cursor \"48;5;233\", opencode), whereas \"*\" keys off the structural presence of a bg box. A genuine assistant line carries no background SGR, so it still classifies as assistant."
86
86
  }
87
87
  }
88
88
  }
@@ -106,7 +106,12 @@ function executeJsonl(src: NativeHistoryJsonlSource, input: NativeHistoryInput):
106
106
  const mtime = safeMtimeMs(sourcePath);
107
107
  const lines = readJsonlLines(sourcePath);
108
108
  if (lines.length === 0) return null;
109
- const transcriptWorkspace = readSessionMetaWorkspace(lines);
109
+ // Prefer an in-transcript session_meta cwd; fall back to the input workspace
110
+ // only when the spec opts in AND the resolved file lives under that
111
+ // workspace's project slug (cursor-agent writes no session_meta and hides the
112
+ // workspace in the lossy on-disk slug — see workspace_from_input).
113
+ const transcriptWorkspace = readSessionMetaWorkspace(lines)
114
+ ?? (src.workspace_from_input ? workspaceFromInputIfSlugMatches(sourcePath, input) : undefined);
110
115
 
111
116
  // session id: filename uuid or extracted from first record
112
117
  let providerSessionId: string | undefined;
@@ -248,6 +253,50 @@ function readSessionMetaWorkspace(lines: any[]): string | undefined {
248
253
  return undefined;
249
254
  }
250
255
 
256
+ /**
257
+ * Return `input.workspace` when the resolved transcript file provably lives
258
+ * under that workspace's project-slug directory, else undefined.
259
+ *
260
+ * cursor-agent stores transcripts at `~/.cursor/projects/<slug>/…` where
261
+ * `<slug>` is the workspace realpath with every non-`[A-Za-z0-9_-]` char turned
262
+ * into `-` (the same transform claude uses, minus the leading dash from the root
263
+ * `/`). Long slugs are truncated and suffixed with a short hash
264
+ * (`<prefix>-<7hex>`). The transform is lossy, so we cannot reconstruct the real
265
+ * path from the slug — but we CAN verify a candidate workspace matches it. We
266
+ * compute the workspace's slug (both the claude form and the leading-`/`-stripped
267
+ * cursor form) and accept when a path segment of the file equals it OR is a
268
+ * truncated `<prefix>-<hash>` of it. On match the caller stamps the KNOWN real
269
+ * `input.workspace`, so downstream workspace comparison (path.resolve-based)
270
+ * still works; on mismatch we return undefined and the read fails closed rather
271
+ * than aliasing another workspace's transcript.
272
+ */
273
+ function workspaceFromInputIfSlugMatches(sourcePath: string, input: NativeHistoryInput): string | undefined {
274
+ const wsRaw = typeof input.workspace === 'string' ? input.workspace.trim() : '';
275
+ if (!wsRaw) return undefined;
276
+ let wsReal = wsRaw;
277
+ try { wsReal = fs.realpathSync(wsRaw); } catch { /* keep raw */ }
278
+ const slugs = new Set<string>();
279
+ for (const w of [wsReal, wsRaw]) {
280
+ if (!w) continue;
281
+ slugs.add(claudeProjectDirName(w)); // "-Users-…" (leading dash)
282
+ slugs.add(claudeProjectDirName(w.replace(/^\/+/, ''))); // cursor form, no leading dash
283
+ }
284
+ const segments = sourcePath.split(path.sep);
285
+ for (const seg of segments) {
286
+ if (!seg) continue;
287
+ for (const slug of slugs) {
288
+ if (!slug) continue;
289
+ if (seg === slug) return wsRaw;
290
+ // Truncated+hashed cursor slug: `<prefix>-<7+hex>` where prefix is a
291
+ // leading portion of the full slug. Require a non-trivial prefix so a
292
+ // short common head can't false-match an unrelated workspace.
293
+ const m = seg.match(/^(.*)-[0-9a-f]{6,}$/);
294
+ if (m && m[1] && m[1].length >= 8 && slug.startsWith(m[1])) return wsRaw;
295
+ }
296
+ }
297
+ return undefined;
298
+ }
299
+
251
300
  function readJsonlLines(p: string): any[] {
252
301
  let text: string;
253
302
  try { text = fs.readFileSync(p, 'utf8'); } catch { return []; }
@@ -95,6 +95,23 @@ export interface NativeHistoryJsonlSource {
95
95
  session_id_from?: 'filename_uuid' | 'first_record';
96
96
  session_id_path?: string;
97
97
  message_filter?: { where: string };
98
+ /**
99
+ * Fallback workspace attribution when the transcript carries no
100
+ * `session_meta` cwd record. Some stores (cursor-agent) do NOT write a
101
+ * session_meta line and keep the workspace only in the on-disk project-slug
102
+ * directory (`~/.cursor/projects/<slug>/…`), which is a lossy, sometimes
103
+ * truncated+hashed transform of the real path and cannot be reversed. When
104
+ * this flag is set and no session_meta workspace was found, the executor
105
+ * stamps `input.workspace` onto each message — but ONLY after confirming the
106
+ * resolved file actually lives under that workspace's project slug (a path
107
+ * segment matches the input workspace's cursor/claude slug, allowing for
108
+ * cursor's length-truncation). This closes the first-turn read gap (before a
109
+ * provider session id is pinned, the downstream hasSafeNativeHistoryMapping
110
+ * guard needs a per-message workspace) without risking cross-workspace
111
+ * aliasing: a slug mismatch leaves the workspace unset and the read fails
112
+ * closed as before.
113
+ */
114
+ workspace_from_input?: boolean;
98
115
  message_map: NativeHistoryMessageMap;
99
116
  }
100
117