@adhdev/daemon-core 0.7.45 → 0.8.0

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/cli-adapters/provider-cli-adapter.d.ts +34 -0
  2. package/dist/cli-adapters/pty-transport.d.ts +1 -0
  3. package/dist/cli-adapters/session-host-transport.d.ts +1 -0
  4. package/dist/commands/cli-manager.d.ts +11 -2
  5. package/dist/config/chat-history.d.ts +32 -2
  6. package/dist/config/config.d.ts +5 -1
  7. package/dist/config/recent-activity.d.ts +3 -1
  8. package/dist/config/saved-sessions.d.ts +22 -0
  9. package/dist/daemon/dev-auto-implement.d.ts +18 -2
  10. package/dist/daemon/dev-cli-debug.d.ts +82 -0
  11. package/dist/daemon/dev-server.d.ts +7 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +6122 -4038
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.mjs +6114 -4032
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/providers/cli-provider-instance.d.ts +29 -1
  18. package/dist/providers/contracts.d.ts +11 -0
  19. package/dist/providers/provider-instance.d.ts +1 -0
  20. package/dist/shared-types.d.ts +2 -0
  21. package/node_modules/@adhdev/session-host-core/dist/index.d.mts +12 -1
  22. package/node_modules/@adhdev/session-host-core/dist/index.d.ts +12 -1
  23. package/node_modules/@adhdev/session-host-core/dist/index.js +9 -0
  24. package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
  25. package/node_modules/@adhdev/session-host-core/dist/index.mjs +9 -0
  26. package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
  27. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  28. package/package.json +1 -1
  29. package/src/boot/daemon-lifecycle.ts +19 -15
  30. package/src/cli-adapters/provider-cli-adapter.ts +424 -7
  31. package/src/cli-adapters/pty-transport.ts +1 -0
  32. package/src/cli-adapters/session-host-transport.ts +32 -1
  33. package/src/commands/chat-commands.ts +36 -8
  34. package/src/commands/cli-manager.ts +259 -22
  35. package/src/commands/router.ts +52 -1
  36. package/src/config/chat-history.ts +197 -10
  37. package/src/config/config.d.ts +4 -0
  38. package/src/config/config.ts +8 -2
  39. package/src/config/recent-activity.ts +13 -2
  40. package/src/config/saved-sessions.ts +73 -0
  41. package/src/daemon/dev-auto-implement.ts +394 -43
  42. package/src/daemon/dev-cli-debug.ts +839 -0
  43. package/src/daemon/dev-server.ts +51 -5
  44. package/src/index.ts +2 -0
  45. package/src/providers/cli-provider-instance.ts +283 -4
  46. package/src/providers/contracts.ts +11 -0
  47. package/src/providers/provider-instance.d.ts +1 -0
  48. package/src/providers/provider-instance.ts +1 -0
  49. package/src/providers/provider-loader.ts +39 -0
  50. package/src/session-host/runtime-support.ts +1 -0
  51. package/src/shared-types.d.ts +2 -0
  52. package/src/shared-types.ts +2 -0
  53. package/src/status/builders.ts +1 -0
  54. package/src/status/snapshot.ts +1 -0
@@ -29,7 +29,7 @@ import { generateTemplate as genScaffoldTemplate, generateFiles as genScaffoldFi
29
29
  import { VersionArchive, detectAllVersions } from '../providers/version-archive.js';
30
30
  import { LOG } from '../logging/logger.js';
31
31
  import { handleCdpEvaluate, handleCdpClick, handleCdpDomQuery, handleScreenshot, handleScriptsRun, handleTypeAndSend, handleTypeAndSendAt, handleScriptHints, handleCdpTargets, handleDomInspect, handleDomChildren, handleDomAnalyze, handleFindCommon, handleFindByText, handleDomContext } from './dev-cdp-handlers.js';
32
- import { handleCliStatus, handleCliLaunch, handleCliSend, handleCliStop, handleCliDebug, handleCliResolve, handleCliRaw, handleCliSSE } from './dev-cli-debug.js';
32
+ import { handleCliStatus, handleCliLaunch, handleCliSend, handleCliStop, handleCliDebug, handleCliTrace, handleCliExercise, handleCliFixtureCapture, handleCliFixtureList, handleCliFixtureReplay, handleCliResolve, handleCliRaw, handleCliSSE } from './dev-cli-debug.js';
33
33
  import { handleAutoImplement, handleAutoImplCancel, handleAutoImplSSE } from './dev-auto-implement.js';
34
34
 
35
35
  export const DEV_SERVER_PORT = 19280;
@@ -102,11 +102,16 @@ export class DevServer implements DevServerContext {
102
102
  { method: 'GET', pattern: '/api/cli/status', handler: (q, s) => this.handleCliStatus(q, s) },
103
103
  { method: 'POST', pattern: '/api/cli/launch', handler: (q, s) => this.handleCliLaunch(q, s) },
104
104
  { method: 'POST', pattern: '/api/cli/send', handler: (q, s) => this.handleCliSend(q, s) },
105
+ { method: 'POST', pattern: '/api/cli/exercise', handler: (q, s) => this.handleCliExercise(q, s) },
106
+ { method: 'POST', pattern: '/api/cli/fixture/capture', handler: (q, s) => this.handleCliFixtureCapture(q, s) },
107
+ { method: 'POST', pattern: '/api/cli/fixture/replay', handler: (q, s) => this.handleCliFixtureReplay(q, s) },
105
108
  { method: 'POST', pattern: '/api/cli/resolve', handler: (q, s) => this.handleCliResolve(q, s) },
106
109
  { method: 'POST', pattern: '/api/cli/raw', handler: (q, s) => this.handleCliRaw(q, s) },
107
110
  { method: 'POST', pattern: '/api/cli/stop', handler: (q, s) => this.handleCliStop(q, s) },
108
111
  { method: 'GET', pattern: '/api/cli/events', handler: (q, s) => this.handleCliSSE(q, s) },
109
112
  { method: 'GET', pattern: /^\/api\/cli\/debug\/([^/]+)$/, handler: (q, s, p) => this.handleCliDebug(p![0], q, s) },
113
+ { method: 'GET', pattern: /^\/api\/cli\/trace\/([^/]+)$/, handler: (q, s, p) => this.handleCliTrace(p![0], q, s) },
114
+ { method: 'GET', pattern: /^\/api\/cli\/fixtures\/([^/]+)$/, handler: (q, s, p) => this.handleCliFixtureList(p![0], q, s) },
110
115
  // Dynamic routes (provider :type param)
111
116
  { method: 'POST', pattern: /^\/api\/providers\/([^/]+)\/script$/, handler: (q, s, p) => this.handleRunScript(p![0], q, s) },
112
117
  { method: 'GET', pattern: /^\/api\/providers\/([^/]+)\/files$/, handler: (q, s, p) => this.handleListFiles(p![0], q, s) },
@@ -1427,22 +1432,32 @@ export class DevServer implements DevServerContext {
1427
1432
  lines.push('| `detectStatus` | `{ tail, screenText, rawBuffer }` | `idle`, `generating`, `waiting_approval`, or `error` |');
1428
1433
  lines.push('| `parseApproval` | `{ buffer, rawBuffer, tail }` | `{ message, buttons }` or `null` |');
1429
1434
  lines.push('');
1435
+ lines.push('## Primary Source of Truth');
1436
+ lines.push('The runtime now provides a reliable current-screen snapshot. Treat `screenText` as the primary source of truth for the LIVE visible UI.');
1437
+ lines.push('That means:');
1438
+ lines.push('- Use `screenText` first for prompt detection, approval UI, status, and visible assistant content.');
1439
+ lines.push('- Use `rawBuffer` only as supporting evidence when ANSI/style/cursor cues matter.');
1440
+ lines.push('- Use `buffer` only when the visible screen does not contain enough text to recover the latest assistant answer.');
1441
+ lines.push('- Do NOT build the parser around stale transcript noise if the current screen already gives the answer.');
1442
+ lines.push('');
1430
1443
 
1431
1444
  lines.push('## Rules');
1432
1445
  lines.push('0. **🚫 SCOPE CONSTRAINT**: You may ONLY edit files marked ✏️ EDIT above. ALL other files are READ-ONLY. Do NOT modify, rewrite, refactor, or "improve" any file not explicitly marked as editable — even if you notice bugs or improvements. No exceptions.');
1433
1446
  lines.push('1. These scripts run in Node.js CommonJS, not in the browser. Do NOT use DOM APIs.');
1434
- lines.push('2. Prefer `screenText` for current visible UI state. That is the PTY equivalent of parsing the current IDE DOM.');
1447
+ lines.push('2. Prefer `screenText` for current visible UI state. It is now the PTY equivalent of a trustworthy live DOM snapshot.');
1435
1448
  lines.push('3. Use `messages` as prior transcript state so redraws do not duplicate old turns on every parse.');
1436
1449
  lines.push('4. Use `partialResponse` for the actively streaming assistant text when status is `generating`.');
1437
- lines.push('5. `detectStatus` must stay lightweight and tail-based. Do not scan the entire history there.');
1438
- lines.push('6. `parseApproval` should understand the live approval area and return clean button labels.');
1439
- lines.push('7. Use `rawBuffer` only when ANSI/control-sequence artifacts matter. Do not depend on raw escape noise unless necessary.');
1450
+ lines.push('5. `detectStatus` must stay lightweight and current-screen-oriented. Prefer the active bottom-of-screen region over stale history.');
1451
+ lines.push('6. `parseApproval` should understand the live approval area and return clean button labels from the CURRENT visible modal.');
1452
+ lines.push('7. Use `rawBuffer` only when ANSI/control-sequence artifacts or style cues matter. Do not depend on raw escape noise unless necessary.');
1440
1453
  lines.push('8. Keep exports compatible with the existing `scripts.js` router (`module.exports = function ...`).');
1441
1454
  lines.push('9. Do NOT modify ANY file not explicitly marked ✏️ EDIT above. No exceptions — no "tiny supporting changes" to other files.');
1442
1455
  lines.push('10. When the verification API returns `instanceId`, keep using that exact instance for follow-up `send`, `resolve`, `raw`, and `stop` calls. Do not assume type-only routing is safe if multiple sessions exist.');
1443
1456
  lines.push('11. Do NOT repeatedly dump the same target files. Read the target scripts once, reproduce the bug, then move directly to patching.');
1444
1457
  lines.push('12. If the user instructions include concrete screen text, raw PTY snippets, or a specific repro, treat that as the primary acceptance criteria.');
1445
1458
  lines.push('13. After the first successful live repro, stop broad diagnosis. Edit the scripts, reload, and verify. Do not burn tokens on repeated re-inspection without code changes.');
1459
+ lines.push('14. If the visible current screen is clean and sufficient, do NOT fall back to complex buffer heuristics. Simpler current-screen parsing is preferred.');
1460
+ lines.push('15. Before changing parser logic, verify whether `provider.json` submit/approval behavior (`sendDelayMs`, `approvalKeys`, submit strategy) is the simpler and more correct fix.');
1446
1461
  lines.push('');
1447
1462
 
1448
1463
  lines.push('## Task');
@@ -1462,9 +1477,16 @@ export class DevServer implements DevServerContext {
1462
1477
  lines.push('### 2. Inspect parsed + raw adapter state');
1463
1478
  lines.push('```bash');
1464
1479
  lines.push(`curl -sS http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/debug/${type}`);
1480
+ lines.push(`curl -sS http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/trace/${type}`);
1465
1481
  lines.push(`curl -sS http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/status`);
1466
1482
  lines.push('```');
1467
1483
  lines.push('');
1484
+ lines.push('The debug payload should be read in this priority order:');
1485
+ lines.push('1. `screenText` / current visible state');
1486
+ lines.push('2. parsed `status`, `messages`, `activeModal`');
1487
+ lines.push('3. `rawBuffer` only for style/control-sequence cues');
1488
+ lines.push('4. `buffer` only when the current screen is insufficient');
1489
+ lines.push('');
1468
1490
  lines.push('Extract the current `instanceId` from the launch or status response and keep using it below.');
1469
1491
  lines.push('');
1470
1492
  lines.push('### 3. Send a realistic approval-triggering prompt');
@@ -1511,6 +1533,8 @@ export class DevServer implements DevServerContext {
1511
1533
  lines.push('5. Confirm the Python file was actually created and executed, not just described in chat text.');
1512
1534
  lines.push('6. Confirm the final assistant transcript includes the exact Python output, including the working directory line and the five square numbers.');
1513
1535
  lines.push('7. Re-run the debug endpoints after edits. Do NOT finish until the parsed result looks correct.');
1536
+ lines.push('8. Confirm the parser still works after a redraw or scroll change without duplicating transcript history.');
1537
+ lines.push('9. Confirm the implementation prefers current-screen signals over stale history when both are present.');
1514
1538
  lines.push('');
1515
1539
 
1516
1540
  if (userComment) {
@@ -1600,6 +1624,19 @@ export class DevServer implements DevServerContext {
1600
1624
  return handleCliSend(this, req, res);
1601
1625
  }
1602
1626
 
1627
+ /** POST /api/cli/exercise — launch/send/approve/wait helper for provider-fix loops */
1628
+ private async handleCliExercise(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1629
+ return handleCliExercise(this, req, res);
1630
+ }
1631
+
1632
+ private async handleCliFixtureCapture(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1633
+ return handleCliFixtureCapture(this, req, res);
1634
+ }
1635
+
1636
+ private async handleCliFixtureReplay(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1637
+ return handleCliFixtureReplay(this, req, res);
1638
+ }
1639
+
1603
1640
  /** POST /api/cli/stop — stop a running CLI { type } */
1604
1641
  private async handleCliStop(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1605
1642
  return handleCliStop(this, req, res);
@@ -1622,6 +1659,15 @@ export class DevServer implements DevServerContext {
1622
1659
  return handleCliDebug(this, type, _req, res);
1623
1660
  }
1624
1661
 
1662
+ /** GET /api/cli/trace/:type — recent CLI trace timeline plus current debug snapshot */
1663
+ private async handleCliTrace(type: string, _req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1664
+ return handleCliTrace(this, type, _req, res);
1665
+ }
1666
+
1667
+ private async handleCliFixtureList(type: string, _req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1668
+ return handleCliFixtureList(this, type, _req, res);
1669
+ }
1670
+
1625
1671
  /** POST /api/cli/resolve — resolve an approval modal { type, buttonIndex } */
1626
1672
  private async handleCliResolve(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1627
1673
  return handleCliResolve(this, req, res);
package/src/index.ts CHANGED
@@ -66,6 +66,8 @@ export { loadConfig, saveConfig, resetConfig, isSetupComplete, markSetupComplete
66
66
  export { getWorkspaceState } from './config/workspaces.js';
67
67
  export { appendRecentActivity, getRecentActivity } from './config/recent-activity.js';
68
68
  export type { RecentActivityEntry } from './config/recent-activity.js';
69
+ export { getSavedProviderSessions, upsertSavedProviderSession } from './config/saved-sessions.js';
70
+ export type { SavedProviderSessionEntry } from './config/saved-sessions.js';
69
71
 
70
72
  // ── Detection ──
71
73
  export { detectIDEs } from './detection/ide-detector.js';
@@ -5,8 +5,11 @@
5
5
  * collectCliData() + status transition logic from daemon-status.ts moved here.
6
6
  */
7
7
 
8
+ import * as os from 'os';
8
9
  import * as path from 'path';
9
10
  import * as crypto from 'crypto';
11
+ import * as fs from 'fs';
12
+ import { createRequire } from 'node:module';
10
13
  import type { ProviderModule } from './contracts.js';
11
14
  import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
12
15
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
@@ -15,6 +18,27 @@ import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
15
18
  import { StatusMonitor } from './status-monitor.js';
16
19
  import { ChatHistoryWriter } from '../config/chat-history.js';
17
20
  import { LOG } from '../logging/logger.js';
21
+ import type { ChatMessage } from '../types.js';
22
+
23
+ let CachedDatabaseSync: (new (path: string, options?: { readOnly?: boolean }) => {
24
+ prepare(sql: string): { get(...params: Array<string | number>): unknown };
25
+ close(): void;
26
+ }) | null = null;
27
+
28
+ function getDatabaseSync() {
29
+ if (CachedDatabaseSync) return CachedDatabaseSync;
30
+ const requireFn = typeof require === 'function'
31
+ ? require
32
+ : createRequire(path.join(process.cwd(), '__adhdev_sqlite_loader__.js'));
33
+ const sqliteModule = requireFn(`node:${'sqlite'}`) as {
34
+ DatabaseSync: typeof CachedDatabaseSync;
35
+ };
36
+ CachedDatabaseSync = sqliteModule.DatabaseSync;
37
+ if (!CachedDatabaseSync) {
38
+ throw new Error('node:sqlite DatabaseSync unavailable');
39
+ }
40
+ return CachedDatabaseSync;
41
+ }
18
42
 
19
43
  export class CliProviderInstance implements ProviderInstance {
20
44
  readonly type: string;
@@ -31,9 +55,21 @@ export class CliProviderInstance implements ProviderInstance {
31
55
  private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
32
56
  private lastApprovalEventAt = 0;
33
57
  private historyWriter: ChatHistoryWriter;
58
+ private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
34
59
  readonly instanceId: string;
35
60
 
36
61
  private presentationMode: 'terminal' | 'chat';
62
+ private providerSessionId?: string;
63
+ private launchMode: 'new' | 'resume' | 'manual';
64
+ private readonly startedAt = Date.now();
65
+ private onProviderSessionResolved?: (info: {
66
+ instanceId: string;
67
+ providerType: string;
68
+ providerName: string;
69
+ workspace: string;
70
+ providerSessionId: string;
71
+ previousProviderSessionId?: string;
72
+ }) => void;
37
73
 
38
74
  constructor(
39
75
  private provider: ProviderModule,
@@ -41,10 +77,25 @@ export class CliProviderInstance implements ProviderInstance {
41
77
  private cliArgs: string[] = [],
42
78
  instanceId?: string,
43
79
  transportFactory?: PtyTransportFactory,
80
+ options?: {
81
+ providerSessionId?: string;
82
+ launchMode?: 'new' | 'resume' | 'manual';
83
+ onProviderSessionResolved?: (info: {
84
+ instanceId: string;
85
+ providerType: string;
86
+ providerName: string;
87
+ workspace: string;
88
+ providerSessionId: string;
89
+ previousProviderSessionId?: string;
90
+ }) => void;
91
+ },
44
92
  ) {
45
93
  this.type = provider.type;
46
94
  this.instanceId = instanceId || crypto.randomUUID();
47
95
  this.presentationMode = 'chat';
96
+ this.providerSessionId = options?.providerSessionId;
97
+ this.launchMode = options?.launchMode || 'new';
98
+ this.onProviderSessionResolved = options?.onProviderSessionResolved;
48
99
  this.adapter = new ProviderCliAdapter(provider as any as CliProviderModule, workingDir, cliArgs, transportFactory);
49
100
  this.monitor = new StatusMonitor();
50
101
  this.historyWriter = new ChatHistoryWriter();
@@ -78,20 +129,72 @@ export class CliProviderInstance implements ProviderInstance {
78
129
 
79
130
  // PTY spawn
80
131
  await this.adapter.spawn();
132
+ if (this.providerSessionId && this.launchMode === 'resume') {
133
+ const resumedAt = Date.now();
134
+ this.historyWriter.appendSystemMarker(
135
+ this.type,
136
+ `Resumed saved session at ${this.formatMarkerTimestamp(resumedAt)}`,
137
+ {
138
+ instanceId: this.instanceId,
139
+ historySessionId: this.providerSessionId,
140
+ dedupKey: `resume:${this.providerSessionId}:${resumedAt}`,
141
+ receivedAt: resumedAt,
142
+ },
143
+ );
144
+ }
81
145
  }
82
146
 
83
147
  async onTick(): Promise<void> {
84
- // CLI is event-based so tick is unnecessary
85
- // Health check etc here if needed
148
+ if (this.providerSessionId) return;
149
+
150
+ let probedSessionId: string | null = null;
151
+ if (this.type === 'opencode-cli') {
152
+ probedSessionId = this.probeOpenCodeSessionId();
153
+ } else if (this.type === 'codex-cli') {
154
+ probedSessionId = this.probeCodexSessionId();
155
+ } else if (this.type === 'goose-cli') {
156
+ probedSessionId = this.probeGooseSessionId();
157
+ }
158
+
159
+ if (probedSessionId) {
160
+ this.promoteProviderSessionId(probedSessionId);
161
+ }
86
162
  }
87
163
 
88
164
  getState(): ProviderState {
89
165
  const adapterStatus = this.adapter.getStatus();
90
166
  const parsedStatus = this.adapter.getScriptParsedStatus?.() || null;
167
+ const parsedProviderSessionId = typeof parsedStatus?.providerSessionId === 'string'
168
+ ? parsedStatus.providerSessionId.trim()
169
+ : '';
170
+ if (parsedProviderSessionId) {
171
+ this.promoteProviderSessionId(parsedProviderSessionId);
172
+ }
91
173
  const runtime = this.adapter.getRuntimeMetadata();
174
+ const parsedMessages = Array.isArray(parsedStatus?.messages) ? parsedStatus.messages : [];
175
+ const mergedMessages = this.mergeConversationMessages(parsedMessages);
92
176
 
93
177
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
94
178
 
179
+ if (parsedMessages.length > 0) {
180
+ let messagesToSave = parsedMessages;
181
+ if ((parsedStatus?.status === 'generating' || parsedStatus?.status === 'long_generating')) {
182
+ const lastIdx = messagesToSave.length - 1;
183
+ if (lastIdx >= 0 && messagesToSave[lastIdx]?.role === 'assistant') {
184
+ messagesToSave = messagesToSave.slice(0, lastIdx);
185
+ }
186
+ }
187
+ if (messagesToSave.length > 0) {
188
+ this.historyWriter.appendNewMessages(
189
+ this.type,
190
+ messagesToSave,
191
+ parsedStatus?.title || dirName,
192
+ this.instanceId,
193
+ this.providerSessionId,
194
+ );
195
+ }
196
+ }
197
+
95
198
  return {
96
199
  type: this.type,
97
200
  name: this.provider.name,
@@ -100,14 +203,15 @@ export class CliProviderInstance implements ProviderInstance {
100
203
  mode: this.presentationMode,
101
204
  activeChat: {
102
205
  id: `${this.type}_${this.workingDir}`,
103
- title: parsedStatus?.title || `${this.provider.name} · ${dirName}`,
206
+ title: parsedStatus?.title || dirName,
104
207
  status: parsedStatus?.status || adapterStatus.status,
105
- messages: Array.isArray(parsedStatus?.messages) ? parsedStatus.messages : [],
208
+ messages: mergedMessages,
106
209
  activeModal: parsedStatus?.activeModal ?? adapterStatus.activeModal,
107
210
  inputContent: '',
108
211
  },
109
212
  workspace: this.workingDir,
110
213
  instanceId: this.instanceId,
214
+ providerSessionId: this.providerSessionId,
111
215
  lastUpdated: Date.now(),
112
216
  settings: this.settings,
113
217
  pendingEvents: this.flushEvents(),
@@ -207,6 +311,11 @@ export class CliProviderInstance implements ProviderInstance {
207
311
  const approvalCooldown = 5000;
208
312
  if (this.lastStatus !== 'waiting_approval' && (!this.lastApprovalEventAt || now - this.lastApprovalEventAt > approvalCooldown)) {
209
313
  this.lastApprovalEventAt = now;
314
+ this.appendRuntimeSystemMessage(
315
+ this.formatApprovalRequestMessage(modal?.message, modal?.buttons),
316
+ `approval_request:${now}`,
317
+ now,
318
+ );
210
319
  this.pushEvent({
211
320
  event: 'agent:waiting_approval', chatTitle, timestamp: now,
212
321
  modalMessage: modal?.message,
@@ -274,4 +383,174 @@ export class CliProviderInstance implements ProviderInstance {
274
383
 
275
384
  get cliType(): string { return this.type; }
276
385
  get cliName(): string { return this.provider.name; }
386
+
387
+ recordApprovalSelection(buttonText: string): void {
388
+ const cleanButton = String(buttonText || '').trim();
389
+ if (!cleanButton) return;
390
+ const now = Date.now();
391
+ this.appendRuntimeSystemMessage(
392
+ `Approval selected: ${cleanButton}`,
393
+ `approval_selection:${now}:${cleanButton}`,
394
+ now,
395
+ );
396
+ }
397
+
398
+ private formatMarkerTimestamp(timestamp: number): string {
399
+ const date = new Date(timestamp);
400
+ const pad = (value: number) => String(value).padStart(2, '0');
401
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
402
+ }
403
+
404
+ private appendRuntimeSystemMessage(content: string, dedupKey: string, receivedAt = Date.now()): void {
405
+ const normalizedContent = String(content || '').trim();
406
+ if (!normalizedContent) return;
407
+ if (this.runtimeMessages.some((entry) => entry.key === dedupKey)) return;
408
+
409
+ this.runtimeMessages.push({
410
+ key: dedupKey,
411
+ message: {
412
+ role: 'system',
413
+ senderName: 'System',
414
+ content: normalizedContent,
415
+ receivedAt,
416
+ timestamp: receivedAt,
417
+ },
418
+ });
419
+ if (this.runtimeMessages.length > 50) {
420
+ this.runtimeMessages = this.runtimeMessages.slice(-50);
421
+ }
422
+
423
+ this.historyWriter.appendNewMessages(
424
+ this.type,
425
+ [{
426
+ role: 'system',
427
+ senderName: 'System',
428
+ content: normalizedContent,
429
+ receivedAt,
430
+ historyDedupKey: dedupKey,
431
+ }],
432
+ this.adapter.getScriptParsedStatus?.()?.title || this.workingDir.split('/').filter(Boolean).pop() || 'session',
433
+ this.instanceId,
434
+ this.providerSessionId,
435
+ );
436
+ }
437
+
438
+ private mergeConversationMessages(parsedMessages: any[]): ChatMessage[] {
439
+ if (this.runtimeMessages.length === 0) return parsedMessages;
440
+
441
+ return [...parsedMessages, ...this.runtimeMessages.map((entry) => entry.message)]
442
+ .map((message, index) => ({ message, index }))
443
+ .sort((a, b) => {
444
+ const aTime = a.message.receivedAt || a.message.timestamp || 0;
445
+ const bTime = b.message.receivedAt || b.message.timestamp || 0;
446
+ if (aTime !== bTime) return aTime - bTime;
447
+ return a.index - b.index;
448
+ })
449
+ .map((entry) => entry.message);
450
+ }
451
+
452
+ private formatApprovalRequestMessage(modalMessage?: string, buttons?: string[]): string {
453
+ const lines = ['Approval requested'];
454
+ const cleanMessage = String(modalMessage || '').trim();
455
+ if (cleanMessage) lines.push(cleanMessage);
456
+ const labels = (buttons || []).map((button) => String(button || '').trim()).filter(Boolean);
457
+ if (labels.length > 0) {
458
+ lines.push(labels.map((label) => `[${label}]`).join(' '));
459
+ }
460
+ return lines.join('\n');
461
+ }
462
+
463
+ private promoteProviderSessionId(sessionId: string): void {
464
+ const nextSessionId = String(sessionId || '').trim();
465
+ if (!nextSessionId || nextSessionId === this.providerSessionId) return;
466
+
467
+ const previousHistorySessionId = this.providerSessionId || this.instanceId;
468
+ const previousProviderSessionId = this.providerSessionId;
469
+ this.providerSessionId = nextSessionId;
470
+ this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
471
+ this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
472
+ this.onProviderSessionResolved?.({
473
+ instanceId: this.instanceId,
474
+ providerType: this.type,
475
+ providerName: this.provider.name,
476
+ workspace: this.workingDir,
477
+ providerSessionId: nextSessionId,
478
+ previousProviderSessionId,
479
+ });
480
+ LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
481
+ }
482
+
483
+ private probeOpenCodeSessionId(): string | null {
484
+ const dbPath = path.join(os.homedir(), '.local', 'share', 'opencode', 'opencode.db');
485
+ if (!fs.existsSync(dbPath)) return null;
486
+ const minCreatedAt = Math.max(0, this.startedAt - 60_000);
487
+ const directories = this.getProbeDirectories();
488
+ const query = `select id from session where directory in (${this.buildSqlPlaceholderList(directories.length)}) and time_created >= ? and time_archived is null order by time_updated desc limit 1;`;
489
+ return this.querySqliteText(dbPath, query, [...directories, minCreatedAt]);
490
+ }
491
+
492
+ private probeCodexSessionId(): string | null {
493
+ const dbPath = path.join(os.homedir(), '.codex', 'state_5.sqlite');
494
+ if (!fs.existsSync(dbPath)) return null;
495
+ const minCreatedAt = Math.max(0, Math.floor((this.startedAt - 60_000) / 1000));
496
+ const directories = this.getProbeDirectories();
497
+ const query = `select id from threads where cwd in (${this.buildSqlPlaceholderList(directories.length)}) and created_at >= ? and archived = 0 order by created_at desc limit 1;`;
498
+ return this.querySqliteText(dbPath, query, [...directories, minCreatedAt]);
499
+ }
500
+
501
+ private probeGooseSessionId(): string | null {
502
+ const dbPath = path.join(os.homedir(), '.local', 'share', 'goose', 'sessions', 'sessions.db');
503
+ if (!fs.existsSync(dbPath)) return null;
504
+ const minCreatedAtIso = new Date(Math.max(0, this.startedAt - 60_000)).toISOString().slice(0, 19).replace('T', ' ');
505
+ const directories = this.getProbeDirectories();
506
+ const query = `select id from sessions where working_dir in (${this.buildSqlPlaceholderList(directories.length)}) and created_at >= ? order by updated_at desc limit 1;`;
507
+ try {
508
+ return this.querySqliteText(dbPath, query, [...directories, minCreatedAtIso]);
509
+ } catch {
510
+ return null;
511
+ }
512
+ }
513
+
514
+ private getProbeDirectories(): string[] {
515
+ const dirs = new Set<string>();
516
+ const addDir = (value: string | null | undefined) => {
517
+ const normalized = typeof value === 'string' ? value.trim() : '';
518
+ if (normalized) dirs.add(normalized);
519
+ };
520
+
521
+ addDir(this.workingDir);
522
+ try {
523
+ addDir(fs.realpathSync.native(this.workingDir));
524
+ } catch {
525
+ // noop
526
+ }
527
+
528
+ return Array.from(dirs);
529
+ }
530
+
531
+ private buildSqlPlaceholderList(count: number): string {
532
+ return Array.from({ length: count }, () => '?').join(', ');
533
+ }
534
+
535
+ private querySqliteText(dbPath: string, query: string, params: Array<string | number>): string | null {
536
+ let db: {
537
+ prepare(sql: string): { get(...values: Array<string | number>): unknown };
538
+ close(): void;
539
+ } | null = null;
540
+ try {
541
+ const DatabaseSync = getDatabaseSync();
542
+ db = new DatabaseSync(dbPath, { readOnly: true });
543
+ const row = db.prepare(query).get(...params) as { id?: unknown } | undefined;
544
+ const sessionId = typeof row?.id === 'string' ? row.id.trim() : '';
545
+ return sessionId || null;
546
+ } catch {
547
+ return null;
548
+ } finally {
549
+ try {
550
+ db?.close();
551
+ } catch {
552
+ // noop
553
+ }
554
+ }
555
+ }
277
556
  }
@@ -399,6 +399,9 @@ export interface ProviderResumeCapability {
399
399
  stopCommand?: string;
400
400
  shutdownGraceMs?: number;
401
401
  resumeArgs?: string[];
402
+ resumeSessionArgs?: string[];
403
+ newSessionArgs?: string[];
404
+ sessionIdFormat?: 'uuid' | 'string';
402
405
  }
403
406
 
404
407
  // ─── ACP Auth Types ─────────────────────────────────
@@ -493,6 +496,14 @@ export interface ResolvedProvider extends ProviderModule {
493
496
  _resolvedVersion?: string;
494
497
  /** Warning when detected version is not in compatibility matrix */
495
498
  _versionWarning?: string;
499
+ /** On-disk provider directory selected by ProviderLoader */
500
+ _resolvedProviderDir?: string;
501
+ /** Script directory selected by compatibility/default resolution */
502
+ _resolvedScriptDir?: string;
503
+ /** scripts.js path or fallback script directory used to build runtime scripts */
504
+ _resolvedScriptsPath?: string;
505
+ /** Why this script selection was chosen */
506
+ _resolvedScriptsSource?: string;
496
507
  }
497
508
 
498
509
  // ─── Provider Settings ─────────────────────────────────
@@ -61,6 +61,7 @@ interface ProviderStateBase {
61
61
  errorReason?: ProviderErrorReason;
62
62
  /** meta */
63
63
  instanceId: string;
64
+ providerSessionId?: string;
64
65
  lastUpdated: number;
65
66
  settings: Record<string, any>;
66
67
  /** Event queue (cleared after daemon collects) */
@@ -76,6 +76,7 @@ interface ProviderStateBase {
76
76
  errorReason?: ProviderErrorReason;
77
77
  /** meta */
78
78
  instanceId: string;
79
+ providerSessionId?: string;
79
80
  lastUpdated: number;
80
81
  settings: Record<string, any>;
81
82
  /** Event queue (cleared after daemon collects) */
@@ -425,6 +425,7 @@ export class ProviderLoader {
425
425
  resolve(type: string, context?: { os?: string; version?: string }): ResolvedProvider | undefined {
426
426
  const base = this.providers.get(type);
427
427
  if (!base) return undefined;
428
+ const providerDir = this.findProviderDirInternal(type) || undefined;
428
429
 
429
430
  const currentOs = context?.os || process.platform;
430
431
  const currentVersion = context?.version ??
@@ -441,6 +442,9 @@ export class ProviderLoader {
441
442
  if (base.scripts) {
442
443
  resolved.scripts = { ...base.scripts };
443
444
  }
445
+ if (providerDir) {
446
+ resolved._resolvedProviderDir = providerDir;
447
+ }
444
448
 
445
449
  // 1. Apply OS override
446
450
  if (base.os?.[currentOs]) {
@@ -468,6 +472,14 @@ export class ProviderLoader {
468
472
  if (loaded) {
469
473
  resolved.scripts = loaded;
470
474
  this.log(` [compatibility] ${type} v${currentVersion} → ${entry.scriptDir}`);
475
+ resolved._resolvedScriptDir = entry.scriptDir;
476
+ resolved._resolvedScriptsSource = `compatibility:${entry.ideVersion}`;
477
+ if (providerDir) {
478
+ const fullDir = path.join(providerDir, entry.scriptDir);
479
+ resolved._resolvedScriptsPath = fs.existsSync(path.join(fullDir, 'scripts.js'))
480
+ ? path.join(fullDir, 'scripts.js')
481
+ : fullDir;
482
+ }
471
483
  matched = true;
472
484
  }
473
485
  break; // first match wins
@@ -480,6 +492,14 @@ export class ProviderLoader {
480
492
  if (loaded) {
481
493
  resolved.scripts = loaded;
482
494
  this.log(` [compatibility] ${type} v${currentVersion} → default: ${(base as any).defaultScriptDir}`);
495
+ resolved._resolvedScriptDir = (base as any).defaultScriptDir;
496
+ resolved._resolvedScriptsSource = 'defaultScriptDir:version_miss';
497
+ if (providerDir) {
498
+ const fullDir = path.join(providerDir, (base as any).defaultScriptDir);
499
+ resolved._resolvedScriptsPath = fs.existsSync(path.join(fullDir, 'scripts.js'))
500
+ ? path.join(fullDir, 'scripts.js')
501
+ : fullDir;
502
+ }
483
503
  }
484
504
  resolved._versionWarning = `Version ${currentVersion} not in compatibility matrix. Using default scripts.`;
485
505
  }
@@ -495,6 +515,14 @@ export class ProviderLoader {
495
515
  if (loaded) {
496
516
  resolved.scripts = loaded;
497
517
  this.log(` [version override] ${type} ${range} → ${dirOverride}`);
518
+ resolved._resolvedScriptDir = dirOverride;
519
+ resolved._resolvedScriptsSource = `versions:${range}`;
520
+ if (providerDir) {
521
+ const fullDir = path.join(providerDir, dirOverride);
522
+ resolved._resolvedScriptsPath = fs.existsSync(path.join(fullDir, 'scripts.js'))
523
+ ? path.join(fullDir, 'scripts.js')
524
+ : fullDir;
525
+ }
498
526
  }
499
527
  } else if (override.scripts) {
500
528
  resolved.scripts = { ...resolved.scripts, ...override.scripts };
@@ -507,6 +535,14 @@ export class ProviderLoader {
507
535
  if (loaded) {
508
536
  resolved.scripts = loaded;
509
537
  this.log(` [compatibility] ${type} no version detected → default: ${(base as any).defaultScriptDir}`);
538
+ resolved._resolvedScriptDir = (base as any).defaultScriptDir;
539
+ resolved._resolvedScriptsSource = 'defaultScriptDir:no_version';
540
+ if (providerDir) {
541
+ const fullDir = path.join(providerDir, (base as any).defaultScriptDir);
542
+ resolved._resolvedScriptsPath = fs.existsSync(path.join(fullDir, 'scripts.js'))
543
+ ? path.join(fullDir, 'scripts.js')
544
+ : fullDir;
545
+ }
510
546
  }
511
547
  }
512
548
 
@@ -583,6 +619,9 @@ export class ProviderLoader {
583
619
  });
584
620
 
585
621
  const handleChange = (filePath: string) => {
622
+ if (/[\/\\]fixtures[\/\\]/.test(filePath)) {
623
+ return;
624
+ }
586
625
  if (filePath.endsWith('.js') || filePath.endsWith('.json')) {
587
626
  this.log(`File changed: ${path.basename(filePath)}, reloading...`);
588
627
  this.reload();
@@ -64,6 +64,7 @@ export async function listHostedCliRuntimes(endpoint: SessionHostEndpoint): Prom
64
64
  cliType: record.providerType,
65
65
  workspace: record.workspace,
66
66
  cliArgs: Array.isArray(record.meta?.cliArgs) ? (record.meta.cliArgs as string[]) : [],
67
+ providerSessionId: typeof record.meta?.providerSessionId === 'string' ? String(record.meta.providerSessionId) : undefined,
67
68
  }));
68
69
  } finally {
69
70
  await client.close().catch(() => {});
@@ -41,6 +41,7 @@ export interface SessionEntry {
41
41
  parentId: string | null;
42
42
  providerType: string;
43
43
  providerName: string;
44
+ providerSessionId?: string;
44
45
  kind: SessionKind;
45
46
  transport: SessionTransport;
46
47
  status: 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting' | 'panel_hidden' | 'not_monitored' | 'disconnected';
@@ -152,6 +153,7 @@ export interface RecentLaunchEntry {
152
153
  providerType: string;
153
154
  providerName: string;
154
155
  kind: 'ide' | 'cli' | 'acp';
156
+ providerSessionId?: string;
155
157
  title?: string;
156
158
  workspace?: string | null;
157
159
  currentModel?: string;