@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.
- package/dist/cli-adapters/provider-cli-adapter.d.ts +34 -0
- package/dist/cli-adapters/pty-transport.d.ts +1 -0
- package/dist/cli-adapters/session-host-transport.d.ts +1 -0
- package/dist/commands/cli-manager.d.ts +11 -2
- package/dist/config/chat-history.d.ts +32 -2
- package/dist/config/config.d.ts +5 -1
- package/dist/config/recent-activity.d.ts +3 -1
- package/dist/config/saved-sessions.d.ts +22 -0
- package/dist/daemon/dev-auto-implement.d.ts +18 -2
- package/dist/daemon/dev-cli-debug.d.ts +82 -0
- package/dist/daemon/dev-server.d.ts +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6122 -4038
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6114 -4032
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +29 -1
- package/dist/providers/contracts.d.ts +11 -0
- package/dist/providers/provider-instance.d.ts +1 -0
- package/dist/shared-types.d.ts +2 -0
- package/node_modules/@adhdev/session-host-core/dist/index.d.mts +12 -1
- package/node_modules/@adhdev/session-host-core/dist/index.d.ts +12 -1
- package/node_modules/@adhdev/session-host-core/dist/index.js +9 -0
- package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
- package/node_modules/@adhdev/session-host-core/dist/index.mjs +9 -0
- package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +19 -15
- package/src/cli-adapters/provider-cli-adapter.ts +424 -7
- package/src/cli-adapters/pty-transport.ts +1 -0
- package/src/cli-adapters/session-host-transport.ts +32 -1
- package/src/commands/chat-commands.ts +36 -8
- package/src/commands/cli-manager.ts +259 -22
- package/src/commands/router.ts +52 -1
- package/src/config/chat-history.ts +197 -10
- package/src/config/config.d.ts +4 -0
- package/src/config/config.ts +8 -2
- package/src/config/recent-activity.ts +13 -2
- package/src/config/saved-sessions.ts +73 -0
- package/src/daemon/dev-auto-implement.ts +394 -43
- package/src/daemon/dev-cli-debug.ts +839 -0
- package/src/daemon/dev-server.ts +51 -5
- package/src/index.ts +2 -0
- package/src/providers/cli-provider-instance.ts +283 -4
- package/src/providers/contracts.ts +11 -0
- package/src/providers/provider-instance.d.ts +1 -0
- package/src/providers/provider-instance.ts +1 -0
- package/src/providers/provider-loader.ts +39 -0
- package/src/session-host/runtime-support.ts +1 -0
- package/src/shared-types.d.ts +2 -0
- package/src/shared-types.ts +2 -0
- package/src/status/builders.ts +1 -0
- package/src/status/snapshot.ts +1 -0
package/src/daemon/dev-server.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
85
|
-
|
|
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 ||
|
|
206
|
+
title: parsedStatus?.title || dirName,
|
|
104
207
|
status: parsedStatus?.status || adapterStatus.status,
|
|
105
|
-
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 ─────────────────────────────────
|
|
@@ -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(() => {});
|
package/src/shared-types.d.ts
CHANGED
|
@@ -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;
|