@adhdev/daemon-core 0.9.82-rc.530 → 0.9.82-rc.531
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 +34 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +34 -11
- package/dist/index.mjs.map +1 -1
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +14 -0
- package/dist/providers/spec/types.d.ts +13 -0
- package/package.json +3 -3
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +62 -3
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +5 -0
- package/src/providers/spec/native-history-executor.ts +34 -3
- package/src/providers/spec/types.ts +13 -0
|
@@ -39,6 +39,20 @@ export interface TranscriptPtySpec {
|
|
|
39
39
|
chromePatterns?: ChromePatternSpec[];
|
|
40
40
|
stripLeadingChrome?: boolean;
|
|
41
41
|
scope?: 'screen' | 'buffer' | 'tail';
|
|
42
|
+
/**
|
|
43
|
+
* Optional SGR background-color codes that mark a USER turn. Some TUIs
|
|
44
|
+
* (cursor-agent) render the user's submitted message and the composer echo
|
|
45
|
+
* inside a colored box but render the assistant answer as a plain line with
|
|
46
|
+
* no background. After ANSI stripping both look like bare 2-space lines, so
|
|
47
|
+
* a bg-less assistant answer and a boxed user echo are indistinguishable by
|
|
48
|
+
* text prefix alone — the user echo then leaks into an assistant bubble.
|
|
49
|
+
* When set, a raw line whose ANSI carries any of these background SGRs (e.g.
|
|
50
|
+
* "48;5;233") is classified as a user turn regardless of its stripped text,
|
|
51
|
+
* so the plain assistant answer is the only assistant bubble left. Matched
|
|
52
|
+
* against the raw pre-strip line; the visible text is still ANSI-stripped
|
|
53
|
+
* for the bubble content.
|
|
54
|
+
*/
|
|
55
|
+
userBackgroundSgr?: string[];
|
|
42
56
|
}
|
|
43
57
|
export interface SessionIdExtractionSpec {
|
|
44
58
|
$schema?: 'adhdev:tui/session-id-extraction@1';
|
|
@@ -114,6 +114,19 @@ export interface NativeHistoryMessageMap {
|
|
|
114
114
|
content_unwrap?: string[];
|
|
115
115
|
timestamp_ms?: string;
|
|
116
116
|
kind?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Optional jsonpath-lite path to a per-message workspace/cwd value. sqlite
|
|
119
|
+
* sources have no `session_meta` record to carry the workspace (that's a
|
|
120
|
+
* jsonl-only convention), so a native-source provider whose store keeps the
|
|
121
|
+
* session directory as a column (e.g. opencode's `session.directory`) can
|
|
122
|
+
* SELECT it into each message row and map it here. The read pipeline's
|
|
123
|
+
* `hasSafeNativeHistoryMapping` guard requires each message to declare a
|
|
124
|
+
* workspace when the read is workspace-scoped (no provider session id was
|
|
125
|
+
* captured from the TUI); without it a workspace-only lookup fails closed
|
|
126
|
+
* and every assistant bubble is dropped. Absent → messages carry no
|
|
127
|
+
* workspace (jsonl still fills it from session_meta).
|
|
128
|
+
*/
|
|
129
|
+
workspace?: string;
|
|
117
130
|
/**
|
|
118
131
|
* Declarative tool-bubble extraction. Without it the executor only emits
|
|
119
132
|
* the text-bearing parts of each record, so a turn that is purely a tool
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.531",
|
|
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.
|
|
51
|
-
"@adhdev/session-host-core": "0.9.82-rc.
|
|
50
|
+
"@adhdev/mesh-shared": "0.9.82-rc.531",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.531",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -46,6 +46,20 @@ export interface TranscriptPtySpec {
|
|
|
46
46
|
chromePatterns?: ChromePatternSpec[]
|
|
47
47
|
stripLeadingChrome?: boolean
|
|
48
48
|
scope?: 'screen' | 'buffer' | 'tail'
|
|
49
|
+
/**
|
|
50
|
+
* Optional SGR background-color codes that mark a USER turn. Some TUIs
|
|
51
|
+
* (cursor-agent) render the user's submitted message and the composer echo
|
|
52
|
+
* inside a colored box but render the assistant answer as a plain line with
|
|
53
|
+
* no background. After ANSI stripping both look like bare 2-space lines, so
|
|
54
|
+
* a bg-less assistant answer and a boxed user echo are indistinguishable by
|
|
55
|
+
* text prefix alone — the user echo then leaks into an assistant bubble.
|
|
56
|
+
* When set, a raw line whose ANSI carries any of these background SGRs (e.g.
|
|
57
|
+
* "48;5;233") is classified as a user turn regardless of its stripped text,
|
|
58
|
+
* so the plain assistant answer is the only assistant bubble left. Matched
|
|
59
|
+
* against the raw pre-strip line; the visible text is still ANSI-stripped
|
|
60
|
+
* for the bubble content.
|
|
61
|
+
*/
|
|
62
|
+
userBackgroundSgr?: string[]
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
export interface SessionIdExtractionSpec {
|
|
@@ -107,6 +121,22 @@ function splitLines(text: string): string[] {
|
|
|
107
121
|
.map(l => l.replace(/\s+$/, ''))
|
|
108
122
|
}
|
|
109
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Split into raw (pre-strip) lines paired with their stripped visible text so
|
|
126
|
+
* per-line ANSI (e.g. a user-turn background SGR) can be inspected before the
|
|
127
|
+
* color is discarded. The stripped column matches splitLines() line for line:
|
|
128
|
+
* same newline split, same trailing-space trim — only the ANSI is retained on
|
|
129
|
+
* the `raw` side. Splitting on newline keeps any leading SGR of a line attached
|
|
130
|
+
* to that line (cursor-agent writes `<bg-sgr> <text>` on one raw line), so a
|
|
131
|
+
* per-line background test is exact.
|
|
132
|
+
*/
|
|
133
|
+
function splitRawLines(text: string): Array<{ raw: string; text: string }> {
|
|
134
|
+
return String(text || '')
|
|
135
|
+
.split(/\r?\n/)
|
|
136
|
+
// eslint-disable-next-line no-control-regex
|
|
137
|
+
.map(rawLine => ({ raw: rawLine, text: stripAnsi(rawLine).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '').replace(/\s+$/, '') }))
|
|
138
|
+
}
|
|
139
|
+
|
|
110
140
|
function pickInputText(input: any, scope: TranscriptPtySpec['scope']): string {
|
|
111
141
|
if (!input) return ''
|
|
112
142
|
if (scope === 'screen') return String(input.screenText || input.screen?.text || '')
|
|
@@ -187,6 +217,19 @@ export function buildParseSessionFromTui(spec: ParseSessionTuiSpec): (input: any
|
|
|
187
217
|
const requireIndentForContinuation = spec.transcriptPty.continuationLine?.indented ?? false
|
|
188
218
|
const stripLeadingChrome = spec.transcriptPty.stripLeadingChrome ?? true
|
|
189
219
|
const scope = spec.transcriptPty.scope ?? 'buffer'
|
|
220
|
+
// Background-SGR user-turn markers. A raw line whose ANSI carries one of
|
|
221
|
+
// these `48;5;NNN`-style background codes is a user turn even when its
|
|
222
|
+
// stripped text is a bare line the assistantPrefix would otherwise grab.
|
|
223
|
+
const userBgList = Array.isArray(spec.transcriptPty.userBackgroundSgr)
|
|
224
|
+
? spec.transcriptPty.userBackgroundSgr.map(s => String(s).trim()).filter(Boolean)
|
|
225
|
+
: []
|
|
226
|
+
const userBgRes = userBgList.map(code => {
|
|
227
|
+
// Match the code inside an SGR run: `\x1b[<...>48;5;233<...>m`. The code
|
|
228
|
+
// (e.g. "48;5;233") appears somewhere in the `;`-separated parameter list
|
|
229
|
+
// terminated by `m`. Escape regex-special chars in the code first.
|
|
230
|
+
const esc = code.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
231
|
+
return new RegExp(`\\x1b\\[[0-9;]*${esc}[0-9;]*m`)
|
|
232
|
+
})
|
|
190
233
|
|
|
191
234
|
// Compose detect + approval helpers if their inputs are present.
|
|
192
235
|
const detectStatus = (spec.spinner || spec.settledPrompt || spec.modal || spec.dispatchOrder)
|
|
@@ -215,13 +258,16 @@ export function buildParseSessionFromTui(spec: ParseSessionTuiSpec): (input: any
|
|
|
215
258
|
const modal = parseApproval(input as any)
|
|
216
259
|
|
|
217
260
|
const text = pickInputText(input, scope)
|
|
218
|
-
|
|
261
|
+
// When a bg-SGR user marker is configured we need the raw (pre-strip)
|
|
262
|
+
// line to test the background color; otherwise the cheaper stripped
|
|
263
|
+
// split is enough. Both yield the same stripped `text` per line.
|
|
264
|
+
const rawLines = userBgRes.length > 0 ? splitRawLines(text) : splitLines(text).map(t => ({ raw: '', text: t }))
|
|
219
265
|
|
|
220
266
|
const messages: SynthesizedMessage[] = []
|
|
221
267
|
let seenFirstRoleLine = !stripLeadingChrome
|
|
222
268
|
|
|
223
|
-
for (const
|
|
224
|
-
const line =
|
|
269
|
+
for (const entry of rawLines) {
|
|
270
|
+
const line = entry.text
|
|
225
271
|
if (line.trim() === '') {
|
|
226
272
|
// Blank line: ends streaming continuation but doesn't add a message.
|
|
227
273
|
continue
|
|
@@ -234,6 +280,19 @@ export function buildParseSessionFromTui(spec: ParseSessionTuiSpec): (input: any
|
|
|
234
280
|
}
|
|
235
281
|
if (isChrome) continue
|
|
236
282
|
|
|
283
|
+
// Background-SGR user classification. Checked before prefix matching
|
|
284
|
+
// so a user turn rendered as a bg-boxed plain line (no distinctive
|
|
285
|
+
// glyph — cursor-agent) is attributed to the user instead of being
|
|
286
|
+
// grabbed by a permissive assistantPrefix. The visible text is the
|
|
287
|
+
// ANSI-stripped `line`.
|
|
288
|
+
if (userBgRes.length > 0 && entry.raw && userBgRes.some(re => re.test(entry.raw))) {
|
|
289
|
+
seenFirstRoleLine = true
|
|
290
|
+
// Strip a leading composer glyph (e.g. "→ ") the boxed echo may carry.
|
|
291
|
+
const content = line.replace(/^\s*[→>›❯]\s*/, '').trim()
|
|
292
|
+
if (content) messages.push({ role: 'user', kind: 'standard', content })
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
|
|
237
296
|
// Role detection
|
|
238
297
|
const userMatch = userRe ? line.match(userRe) : null
|
|
239
298
|
const toolMatch = toolRe ? line.match(toolRe) : null
|
|
@@ -78,6 +78,11 @@
|
|
|
78
78
|
"enum": ["screen", "buffer", "tail"],
|
|
79
79
|
"default": "buffer",
|
|
80
80
|
"description": "Which input field to parse: rendered screen text, full buffer (default), or tail string. Most providers use buffer because the screen alone loses scrollback."
|
|
81
|
+
},
|
|
82
|
+
"userBackgroundSgr": {
|
|
83
|
+
"type": "array",
|
|
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."
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
}
|
|
@@ -357,11 +357,28 @@ function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput
|
|
|
357
357
|
const sessionFloorSeconds = typeof input.sessionStartedAtMs === 'number'
|
|
358
358
|
? Math.floor(input.sessionStartedAtMs / 1000)
|
|
359
359
|
: 0;
|
|
360
|
+
// Workspace the daemon spawned this CLI in. A store that keeps
|
|
361
|
+
// the session directory as a column (opencode's
|
|
362
|
+
// `session.directory`) can scope the newest-session pick to this
|
|
363
|
+
// workspace so two concurrent sessions in different workspaces
|
|
364
|
+
// don't cross-bind — the time floor alone can't disambiguate
|
|
365
|
+
// when the OTHER workspace's session was touched more recently.
|
|
366
|
+
const workspaceHint = typeof input.workspace === 'string' ? input.workspace : '';
|
|
360
367
|
const stmt = db.prepare(src.session_query);
|
|
368
|
+
// Binding tiers, tried in order (better-sqlite3 throws when the
|
|
369
|
+
// statement declares params the bind object/args don't satisfy,
|
|
370
|
+
// so each tier is guarded):
|
|
371
|
+
// 1. named { floor, workspace } — spec references @floor/@workspace
|
|
372
|
+
// 2. positional (floor) — legacy single-`?` floor specs
|
|
373
|
+
// 3. no-arg — specs with no bound params
|
|
361
374
|
try {
|
|
362
|
-
sessionRow = stmt.get(sessionFloorSeconds);
|
|
375
|
+
sessionRow = stmt.get({ floor: sessionFloorSeconds, workspace: workspaceHint });
|
|
363
376
|
} catch {
|
|
364
|
-
|
|
377
|
+
try {
|
|
378
|
+
sessionRow = stmt.get(sessionFloorSeconds);
|
|
379
|
+
} catch {
|
|
380
|
+
sessionRow = stmt.get();
|
|
381
|
+
}
|
|
365
382
|
}
|
|
366
383
|
} catch { return ''; }
|
|
367
384
|
if (!sessionRow) return '';
|
|
@@ -406,12 +423,18 @@ function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput
|
|
|
406
423
|
}
|
|
407
424
|
if (messages.length === 0) return null;
|
|
408
425
|
|
|
426
|
+
// Surface the workspace at the result level too (mirrors the jsonl
|
|
427
|
+
// session_meta path) so callers that read result.workspace — not just
|
|
428
|
+
// per-message workspace — see the session directory.
|
|
429
|
+
const resultWorkspace = messages.find(m => m.workspace)?.workspace;
|
|
430
|
+
|
|
409
431
|
return {
|
|
410
432
|
messages,
|
|
411
433
|
providerSessionId: sessionId,
|
|
412
434
|
sourcePath: resolved,
|
|
413
435
|
sourceMtimeMs: mtime,
|
|
414
436
|
nativeHistoryCoverage: 'full',
|
|
437
|
+
...(resultWorkspace ? { workspace: resultWorkspace } : {}),
|
|
415
438
|
};
|
|
416
439
|
} finally {
|
|
417
440
|
try { db.close(); } catch { /* ignore */ }
|
|
@@ -1015,9 +1038,17 @@ function projectMessages(record: any, map: NativeHistoryMessageMap, index: numbe
|
|
|
1015
1038
|
}
|
|
1016
1039
|
}
|
|
1017
1040
|
|
|
1041
|
+
// Per-message workspace: sqlite sources have no `session_meta` record to
|
|
1042
|
+
// carry the cwd (jsonl-only), so a spec can SELECT the session directory
|
|
1043
|
+
// into each row and map it here. The downstream hasSafeNativeHistoryMapping
|
|
1044
|
+
// guard needs it to accept a workspace-scoped read (no provider session id
|
|
1045
|
+
// captured from the TUI); without it every assistant bubble is dropped.
|
|
1046
|
+
const workspaceRaw = map.workspace ? jsonPathGet(record, map.workspace) : undefined;
|
|
1047
|
+
const workspace = typeof workspaceRaw === 'string' && workspaceRaw.trim() ? workspaceRaw.trim() : undefined;
|
|
1048
|
+
|
|
1018
1049
|
const contentRaw = jsonPathGet(record, map.content);
|
|
1019
1050
|
const content = cleanContent(stringifyContent(contentRaw), map);
|
|
1020
|
-
if (content) out.push({ role, content, receivedAt, kind });
|
|
1051
|
+
if (content) out.push(workspace ? { role, content, receivedAt, kind, workspace } : { role, content, receivedAt, kind });
|
|
1021
1052
|
|
|
1022
1053
|
// Block-nested tool bubbles are ordered just after the text bubble of the
|
|
1023
1054
|
// same record by nudging receivedAt forward a millisecond per bubble, so a
|
|
@@ -135,6 +135,19 @@ export interface NativeHistoryMessageMap {
|
|
|
135
135
|
content_unwrap?: string[];
|
|
136
136
|
timestamp_ms?: string;
|
|
137
137
|
kind?: string;
|
|
138
|
+
/**
|
|
139
|
+
* Optional jsonpath-lite path to a per-message workspace/cwd value. sqlite
|
|
140
|
+
* sources have no `session_meta` record to carry the workspace (that's a
|
|
141
|
+
* jsonl-only convention), so a native-source provider whose store keeps the
|
|
142
|
+
* session directory as a column (e.g. opencode's `session.directory`) can
|
|
143
|
+
* SELECT it into each message row and map it here. The read pipeline's
|
|
144
|
+
* `hasSafeNativeHistoryMapping` guard requires each message to declare a
|
|
145
|
+
* workspace when the read is workspace-scoped (no provider session id was
|
|
146
|
+
* captured from the TUI); without it a workspace-only lookup fails closed
|
|
147
|
+
* and every assistant bubble is dropped. Absent → messages carry no
|
|
148
|
+
* workspace (jsonl still fills it from session_meta).
|
|
149
|
+
*/
|
|
150
|
+
workspace?: string;
|
|
138
151
|
/**
|
|
139
152
|
* Declarative tool-bubble extraction. Without it the executor only emits
|
|
140
153
|
* the text-bearing parts of each record, so a turn that is purely a tool
|