@adhdev/daemon-core 0.9.82-rc.12 → 0.9.82-rc.121
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/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +21 -0
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +14 -0
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/chat-history.d.ts +4 -0
- package/dist/config/mesh-config.d.ts +68 -1
- package/dist/git/git-commands.d.ts +5 -1
- package/dist/index.d.ts +15 -5
- package/dist/index.js +7029 -1368
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6984 -1351
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +18 -0
- package/dist/mesh/mesh-active-work.d.ts +73 -0
- package/dist/mesh/mesh-events.d.ts +26 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +38 -1
- package/dist/mesh/mesh-refine-status.d.ts +27 -0
- package/dist/mesh/mesh-work-queue.d.ts +27 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +193 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +115 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +4 -1
- package/dist/repo-mesh-types.d.ts +62 -0
- package/dist/status/reporter.d.ts +2 -0
- package/package.json +3 -1
- package/src/boot/daemon-lifecycle.ts +1 -0
- package/src/chat/subscription-updates.ts +5 -1
- package/src/cli-adapters/provider-cli-adapter.ts +453 -17
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +4 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +32 -10
- package/src/commands/chat-commands.ts +730 -30
- package/src/commands/cli-manager.ts +129 -1
- package/src/commands/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +3239 -430
- package/src/config/chat-history.ts +37 -9
- package/src/config/mesh-config.ts +249 -2
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +17 -5
- package/src/index.ts +41 -4
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +176 -0
- package/src/mesh/coordinator-prompt.ts +31 -8
- package/src/mesh/mesh-active-work.ts +292 -0
- package/src/mesh/mesh-events.ts +389 -47
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +138 -1
- package/src/mesh/mesh-refine-status.ts +145 -0
- package/src/mesh/mesh-work-queue.ts +199 -137
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +366 -0
- package/src/mesh/worktree-bootstrap-config.ts +234 -0
- package/src/providers/approval-utils.ts +12 -5
- package/src/providers/chat-message-normalization.ts +7 -12
- package/src/providers/cli-provider-instance.ts +201 -28
- package/src/providers/ide-provider-instance.ts +17 -3
- package/src/providers/provider-loader.ts +10 -4
- package/src/providers/read-chat-contract.ts +1 -1
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +67 -0
- package/src/status/reporter.ts +15 -0
- package/src/system/host-memory.ts +29 -12
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join, resolve as pathResolve } from 'path';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import * as yaml from 'js-yaml';
|
|
6
|
+
import {
|
|
7
|
+
isMeshConfigRecord,
|
|
8
|
+
normalizeMeshCommandConfig,
|
|
9
|
+
type MeshRefineValidationCommandPlan,
|
|
10
|
+
type RepoMeshRefineValidationCommandConfig,
|
|
11
|
+
} from './refine-config.js';
|
|
12
|
+
|
|
13
|
+
export type WorktreeBootstrapStatus = 'ready' | 'running' | 'failed' | 'not_configured' | 'disabled' | 'stale';
|
|
14
|
+
|
|
15
|
+
export interface RepoMeshWorktreeBootstrapConfig {
|
|
16
|
+
version: 1;
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
runOnClone?: boolean;
|
|
19
|
+
required?: boolean;
|
|
20
|
+
commands?: RepoMeshRefineValidationCommandConfig[];
|
|
21
|
+
staleInputs?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WorktreeBootstrapState {
|
|
25
|
+
status: WorktreeBootstrapStatus;
|
|
26
|
+
required: boolean;
|
|
27
|
+
configSource?: string;
|
|
28
|
+
configSourceType?: 'repo_file' | 'mesh_policy' | 'unavailable' | 'invalid';
|
|
29
|
+
startedAt?: string;
|
|
30
|
+
completedAt?: string;
|
|
31
|
+
lastCommand?: string;
|
|
32
|
+
exitCode?: number | null;
|
|
33
|
+
error?: string;
|
|
34
|
+
commandsRun?: Array<Record<string, unknown>>;
|
|
35
|
+
staleInputs?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface WorktreeBootstrapConfigLoadResult {
|
|
39
|
+
config?: RepoMeshWorktreeBootstrapConfig;
|
|
40
|
+
source: string;
|
|
41
|
+
sourceType: 'repo_file' | 'mesh_policy' | 'unavailable' | 'invalid';
|
|
42
|
+
path?: string;
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS = [
|
|
47
|
+
'.adhdev/worktree_bootstrap.json',
|
|
48
|
+
'.adhdev/worktree_bootstrap.yaml',
|
|
49
|
+
'.adhdev/worktree_bootstrap.yml',
|
|
50
|
+
'.adhdev/worktree-bootstrap.json',
|
|
51
|
+
'.adhdev/worktree-bootstrap.yaml',
|
|
52
|
+
'.adhdev/worktree-bootstrap.yml',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export const MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA = {
|
|
56
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
57
|
+
title: 'ADHDev Repo Mesh Worktree Bootstrap Config',
|
|
58
|
+
type: 'object',
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
required: ['version'],
|
|
61
|
+
properties: {
|
|
62
|
+
version: { const: 1 },
|
|
63
|
+
enabled: { type: 'boolean', default: true },
|
|
64
|
+
runOnClone: { type: 'boolean', default: true },
|
|
65
|
+
required: { type: 'boolean', default: true },
|
|
66
|
+
staleInputs: { type: 'array', maxItems: 16, items: { type: 'string', minLength: 1 } },
|
|
67
|
+
commands: {
|
|
68
|
+
type: 'array',
|
|
69
|
+
minItems: 1,
|
|
70
|
+
maxItems: 4,
|
|
71
|
+
items: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
required: ['command'],
|
|
75
|
+
properties: {
|
|
76
|
+
command: { type: 'string', minLength: 1 },
|
|
77
|
+
args: { type: 'array', items: { type: 'string' } },
|
|
78
|
+
category: { enum: ['typecheck', 'test', 'lint', 'build', 'custom'] },
|
|
79
|
+
cwd: { type: 'string' },
|
|
80
|
+
timeoutMs: { type: 'number', minimum: 1000, maximum: 600000 },
|
|
81
|
+
outputLimitBytes: { type: 'number', minimum: 1024, maximum: 1048576 },
|
|
82
|
+
env: { type: 'object', additionalProperties: { type: 'string' } },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
} as const;
|
|
88
|
+
|
|
89
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
90
|
+
const DEFAULT_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
91
|
+
const OUTPUT_SUMMARY_CHARS = 2_000;
|
|
92
|
+
|
|
93
|
+
function parseConfigText(path: string, text: string): unknown {
|
|
94
|
+
if (/\.json$/i.test(path)) return JSON.parse(text);
|
|
95
|
+
return yaml.load(text);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function truncateOutput(value: unknown): string {
|
|
99
|
+
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
100
|
+
if (text.length <= OUTPUT_SUMMARY_CHARS) return text;
|
|
101
|
+
return `${text.slice(0, OUTPUT_SUMMARY_CHARS)}\n[truncated ${text.length - OUTPUT_SUMMARY_CHARS} chars]`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function validateMeshWorktreeBootstrapConfig(config: unknown, source = 'inline'): {
|
|
105
|
+
valid: boolean;
|
|
106
|
+
errors: string[];
|
|
107
|
+
commands: MeshRefineValidationCommandPlan[];
|
|
108
|
+
rejectedCommands: Array<Record<string, unknown>>;
|
|
109
|
+
} {
|
|
110
|
+
const errors: string[] = [];
|
|
111
|
+
const commands: MeshRefineValidationCommandPlan[] = [];
|
|
112
|
+
const rejectedCommands: Array<Record<string, unknown>> = [];
|
|
113
|
+
if (!isMeshConfigRecord(config)) return { valid: false, errors: ['config must be an object'], commands, rejectedCommands };
|
|
114
|
+
if (config.version !== 1) errors.push('version must be 1');
|
|
115
|
+
if (config.enabled !== undefined && typeof config.enabled !== 'boolean') errors.push('enabled must be a boolean when provided');
|
|
116
|
+
if (config.runOnClone !== undefined && typeof config.runOnClone !== 'boolean') errors.push('runOnClone must be a boolean when provided');
|
|
117
|
+
if (config.required !== undefined && typeof config.required !== 'boolean') errors.push('required must be a boolean when provided');
|
|
118
|
+
if (config.staleInputs !== undefined && (!Array.isArray(config.staleInputs) || !config.staleInputs.every(input => typeof input === 'string' && input.trim()))) {
|
|
119
|
+
errors.push('staleInputs must be an array of non-empty strings when provided');
|
|
120
|
+
}
|
|
121
|
+
if (config.commands !== undefined && !Array.isArray(config.commands)) errors.push('commands must be an array');
|
|
122
|
+
if (Array.isArray(config.commands)) {
|
|
123
|
+
config.commands.forEach((entry, index) => {
|
|
124
|
+
const normalized = normalizeMeshCommandConfig(entry, `${source}:commands[${index}]`);
|
|
125
|
+
if (normalized.command) commands.push(normalized.command);
|
|
126
|
+
if (normalized.rejected) rejectedCommands.push(normalized.rejected);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (config.enabled !== false && config.runOnClone !== false && commands.length === 0) errors.push('commands must contain at least one command when bootstrap is enabled');
|
|
130
|
+
if (rejectedCommands.length) errors.push('one or more bootstrap commands are invalid');
|
|
131
|
+
return { valid: errors.length === 0, errors, commands, rejectedCommands };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function loadMeshWorktreeBootstrapConfig(mesh: any, workspace: string): WorktreeBootstrapConfigLoadResult {
|
|
135
|
+
const inline = mesh?.worktreeBootstrapConfig || mesh?.policy?.worktreeBootstrapConfig || mesh?.policy?.worktreeBootstrap;
|
|
136
|
+
if (inline !== undefined) {
|
|
137
|
+
const validation = validateMeshWorktreeBootstrapConfig(inline, 'mesh.policy.worktreeBootstrapConfig');
|
|
138
|
+
if (!validation.valid) return { source: 'mesh.policy.worktreeBootstrapConfig', sourceType: 'invalid', error: String(validation.rejectedCommands[0]?.reason || validation.errors.join('; ')) };
|
|
139
|
+
return { config: inline as RepoMeshWorktreeBootstrapConfig, source: 'mesh.policy.worktreeBootstrapConfig', sourceType: 'mesh_policy' };
|
|
140
|
+
}
|
|
141
|
+
for (const relative of MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS) {
|
|
142
|
+
const configPath = join(workspace, relative);
|
|
143
|
+
if (!existsSync(configPath)) continue;
|
|
144
|
+
try {
|
|
145
|
+
const parsed = parseConfigText(configPath, readFileSync(configPath, 'utf-8'));
|
|
146
|
+
const validation = validateMeshWorktreeBootstrapConfig(parsed, relative);
|
|
147
|
+
if (!validation.valid) return { source: relative, sourceType: 'invalid', path: configPath, error: String(validation.rejectedCommands[0]?.reason || validation.errors.join('; ')) };
|
|
148
|
+
return { config: parsed as RepoMeshWorktreeBootstrapConfig, source: relative, sourceType: 'repo_file', path: configPath };
|
|
149
|
+
} catch (error: any) {
|
|
150
|
+
return { source: relative, sourceType: 'invalid', path: configPath, error: error?.message || String(error) };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { source: 'unavailable', sourceType: 'unavailable', error: `No worktree bootstrap config found. Checked: ${MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS.join(', ')}` };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function runMeshWorktreeBootstrap(mesh: any, workspace: string): Promise<WorktreeBootstrapState> {
|
|
157
|
+
const loaded = loadMeshWorktreeBootstrapConfig(mesh, workspace);
|
|
158
|
+
if (!loaded.config) {
|
|
159
|
+
return { status: 'not_configured', required: false, configSource: loaded.source, configSourceType: loaded.sourceType, error: loaded.error };
|
|
160
|
+
}
|
|
161
|
+
const required = loaded.config.required !== false;
|
|
162
|
+
if (loaded.config.enabled === false || loaded.config.runOnClone === false) {
|
|
163
|
+
return { status: 'disabled', required, configSource: loaded.path || loaded.source, configSourceType: loaded.sourceType };
|
|
164
|
+
}
|
|
165
|
+
const validation = validateMeshWorktreeBootstrapConfig(loaded.config, loaded.source);
|
|
166
|
+
if (!validation.valid) {
|
|
167
|
+
return { status: 'failed', required, configSource: loaded.path || loaded.source, configSourceType: 'invalid', error: String(validation.rejectedCommands[0]?.reason || validation.errors.join('; ')), commandsRun: [] };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const execFileAsync = promisify(execFile);
|
|
171
|
+
const state: WorktreeBootstrapState = {
|
|
172
|
+
status: 'running',
|
|
173
|
+
required,
|
|
174
|
+
configSource: loaded.path || loaded.source,
|
|
175
|
+
configSourceType: loaded.sourceType,
|
|
176
|
+
startedAt: new Date().toISOString(),
|
|
177
|
+
commandsRun: [],
|
|
178
|
+
staleInputs: loaded.config.staleInputs,
|
|
179
|
+
};
|
|
180
|
+
for (const command of validation.commands) {
|
|
181
|
+
const cwd = command.cwd ? pathResolve(workspace, command.cwd) : workspace;
|
|
182
|
+
const startedAt = Date.now();
|
|
183
|
+
state.lastCommand = command.displayCommand;
|
|
184
|
+
try {
|
|
185
|
+
const result = await execFileAsync(command.command, command.args, {
|
|
186
|
+
cwd,
|
|
187
|
+
encoding: 'utf8',
|
|
188
|
+
timeout: command.timeoutMs || DEFAULT_TIMEOUT_MS,
|
|
189
|
+
maxBuffer: command.outputLimitBytes || DEFAULT_OUTPUT_LIMIT_BYTES,
|
|
190
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(command.env || {}) },
|
|
191
|
+
windowsHide: true,
|
|
192
|
+
});
|
|
193
|
+
state.commandsRun?.push({
|
|
194
|
+
command: command.command,
|
|
195
|
+
args: command.args,
|
|
196
|
+
displayCommand: command.displayCommand,
|
|
197
|
+
category: command.category,
|
|
198
|
+
source: command.source,
|
|
199
|
+
cwd,
|
|
200
|
+
passed: true,
|
|
201
|
+
durationMs: Date.now() - startedAt,
|
|
202
|
+
exitCode: 0,
|
|
203
|
+
stdout: truncateOutput(result.stdout),
|
|
204
|
+
stderr: truncateOutput(result.stderr),
|
|
205
|
+
});
|
|
206
|
+
} catch (error: any) {
|
|
207
|
+
const exitCode = typeof error?.code === 'number' ? error.code : null;
|
|
208
|
+
state.status = 'failed';
|
|
209
|
+
state.exitCode = exitCode;
|
|
210
|
+
state.error = error?.message || String(error);
|
|
211
|
+
state.completedAt = new Date().toISOString();
|
|
212
|
+
state.commandsRun?.push({
|
|
213
|
+
command: command.command,
|
|
214
|
+
args: command.args,
|
|
215
|
+
displayCommand: command.displayCommand,
|
|
216
|
+
category: command.category,
|
|
217
|
+
source: command.source,
|
|
218
|
+
cwd,
|
|
219
|
+
passed: false,
|
|
220
|
+
durationMs: Date.now() - startedAt,
|
|
221
|
+
exitCode,
|
|
222
|
+
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
223
|
+
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
224
|
+
stdout: truncateOutput(error?.stdout),
|
|
225
|
+
stderr: truncateOutput(error?.stderr || error?.message),
|
|
226
|
+
});
|
|
227
|
+
return state;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
state.status = 'ready';
|
|
231
|
+
state.exitCode = 0;
|
|
232
|
+
state.completedAt = new Date().toISOString();
|
|
233
|
+
return state;
|
|
234
|
+
}
|
|
@@ -24,6 +24,13 @@ function normalizeApprovalLabel(value: string): string {
|
|
|
24
24
|
.trim();
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function isNegativeApprovalLabel(value: string): boolean {
|
|
28
|
+
const label = normalizeApprovalLabel(value);
|
|
29
|
+
return /^(no|deny|reject|cancel|skip|exit|stop)\b/.test(label)
|
|
30
|
+
|| /\bwithout\b/.test(label)
|
|
31
|
+
|| /\bdo not\b/.test(label);
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
export function getApprovalPositiveHints(provider?: Pick<ProviderModule, 'approvalPositiveHints'> | null): string[] {
|
|
28
35
|
const customHints = Array.isArray(provider?.approvalPositiveHints)
|
|
29
36
|
? provider.approvalPositiveHints
|
|
@@ -39,24 +46,24 @@ export function pickApprovalButton(
|
|
|
39
46
|
): { index: number; label: string } {
|
|
40
47
|
const labels = (buttons || []).map((button) => String(button || '').trim()).filter(Boolean);
|
|
41
48
|
if (labels.length === 0) {
|
|
42
|
-
return { index:
|
|
49
|
+
return { index: -1, label: '' };
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
const normalizedButtons = labels.map((label) => normalizeApprovalLabel(label));
|
|
46
53
|
const hints = getApprovalPositiveHints(provider);
|
|
47
54
|
|
|
48
55
|
for (const hint of hints) {
|
|
49
|
-
const exactIndex = normalizedButtons.findIndex((label) => label === hint);
|
|
56
|
+
const exactIndex = normalizedButtons.findIndex((label, index) => label === hint && !isNegativeApprovalLabel(labels[index]));
|
|
50
57
|
if (exactIndex >= 0) return { index: exactIndex, label: labels[exactIndex] };
|
|
51
58
|
|
|
52
|
-
const prefixIndex = normalizedButtons.findIndex((label) => label.startsWith(hint));
|
|
59
|
+
const prefixIndex = normalizedButtons.findIndex((label, index) => label.startsWith(hint) && !isNegativeApprovalLabel(labels[index]));
|
|
53
60
|
if (prefixIndex >= 0) return { index: prefixIndex, label: labels[prefixIndex] };
|
|
54
61
|
|
|
55
|
-
const includeIndex = normalizedButtons.findIndex((label) => label.includes(hint));
|
|
62
|
+
const includeIndex = normalizedButtons.findIndex((label, index) => label.includes(hint) && !isNegativeApprovalLabel(labels[index]));
|
|
56
63
|
if (includeIndex >= 0) return { index: includeIndex, label: labels[includeIndex] };
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
return { index:
|
|
66
|
+
return { index: -1, label: '' };
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
export function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ChatMessage } from '../types.js';
|
|
2
2
|
import { flattenContent } from './contracts.js';
|
|
3
3
|
|
|
4
|
+
export const DEFAULT_FINAL_SUMMARY_MAX_CHARS = 4_000;
|
|
5
|
+
|
|
4
6
|
export function extractFinalSummaryFromMessages(
|
|
5
7
|
messages: ChatMessage[] | null | undefined,
|
|
6
|
-
maxChars: number =
|
|
8
|
+
maxChars: number = DEFAULT_FINAL_SUMMARY_MAX_CHARS,
|
|
7
9
|
): string {
|
|
8
10
|
if (!Array.isArray(messages) || messages.length === 0) return '';
|
|
9
11
|
|
|
@@ -18,17 +20,10 @@ export function extractFinalSummaryFromMessages(
|
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const classification = classifyChatMessageVisibility(msg);
|
|
26
|
-
if (classification.isUserFacing) {
|
|
27
|
-
const text = flattenContent(msg.content).trim();
|
|
28
|
-
if (text) return text.slice(0, maxChars);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
23
|
+
// Completion summaries must describe the assistant/model result. If no
|
|
24
|
+
// user-facing assistant/model message exists yet (for example, only the
|
|
25
|
+
// dispatched user prompt is visible), return empty instead of echoing the
|
|
26
|
+
// prompt as a misleading finalSummary.
|
|
32
27
|
return '';
|
|
33
28
|
}
|
|
34
29
|
|
|
@@ -43,6 +43,23 @@ type CompletedDebouncePending = {
|
|
|
43
43
|
loggedBlockReason?: string;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
function isIdleStatus(value: unknown): boolean {
|
|
47
|
+
const status = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
48
|
+
return !status || status === 'idle' || status === 'ready';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getMessageTime(message: unknown): number {
|
|
52
|
+
if (!message || typeof message !== 'object') return 0;
|
|
53
|
+
const record = message as { receivedAt?: unknown; timestamp?: unknown };
|
|
54
|
+
const value = Number(record.receivedAt ?? record.timestamp ?? 0);
|
|
55
|
+
return Number.isFinite(value) ? value : 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type CompletedFinalizationBlock = {
|
|
59
|
+
reason: string;
|
|
60
|
+
terminal?: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
46
63
|
const COMPLETED_FINALIZATION_RETRY_MS = 1000;
|
|
47
64
|
const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
|
|
48
65
|
|
|
@@ -412,7 +429,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
412
429
|
await this.adapter.spawn();
|
|
413
430
|
await this.enforceFreshSessionLaunchIfNeeded();
|
|
414
431
|
this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
|
|
415
|
-
if (this.providerSessionId) {
|
|
432
|
+
if (this.providerSessionId && this.shouldHydrateExistingProviderHistory()) {
|
|
416
433
|
this.restorePersistedHistoryFromCurrentSession();
|
|
417
434
|
}
|
|
418
435
|
if (this.providerSessionId && this.launchMode === 'resume') {
|
|
@@ -492,8 +509,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
492
509
|
if (typeof this.adapter.getScriptParsedStatus === 'function') {
|
|
493
510
|
try {
|
|
494
511
|
parsedStatus = this.adapter.getScriptParsedStatus() || null;
|
|
495
|
-
|
|
496
|
-
|
|
512
|
+
const parsedErrorMessage = typeof parsedStatus?.errorMessage === 'string' && parsedStatus.errorMessage.trim()
|
|
513
|
+
? parsedStatus.errorMessage.trim()
|
|
514
|
+
: undefined;
|
|
515
|
+
const parsedErrorReason = typeof parsedStatus?.errorReason === 'string' && parsedStatus.errorReason.trim()
|
|
516
|
+
? parsedStatus.errorReason.trim() as ProviderErrorReason
|
|
517
|
+
: undefined;
|
|
518
|
+
this.errorMessage = parsedErrorMessage;
|
|
519
|
+
this.errorReason = parsedErrorReason;
|
|
497
520
|
} catch (error: any) {
|
|
498
521
|
parseErrorMessage = error?.message || String(error);
|
|
499
522
|
this.errorMessage = parseErrorMessage;
|
|
@@ -503,22 +526,39 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
503
526
|
this.errorMessage = undefined;
|
|
504
527
|
this.errorReason = undefined;
|
|
505
528
|
}
|
|
529
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
530
|
+
this.provider,
|
|
531
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
532
|
+
);
|
|
506
533
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
|
|
507
|
-
const visibleStatus = parseErrorMessage
|
|
534
|
+
const visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
|
|
508
535
|
? 'error'
|
|
509
536
|
: (autoApproveActive ? 'generating' : adapterStatus.status);
|
|
537
|
+
const runtime = this.adapter.getRuntimeMetadata();
|
|
538
|
+
this.maybeAppendRuntimeRecoveryMessage(runtime);
|
|
539
|
+
let parsedMessages = Array.isArray(parsedStatus?.messages)
|
|
540
|
+
? parsedStatus.messages
|
|
541
|
+
: [];
|
|
510
542
|
const parsedProviderSessionId = normalizeProviderSessionId(
|
|
511
543
|
this.provider,
|
|
512
544
|
typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
|
|
513
545
|
);
|
|
514
|
-
|
|
546
|
+
const suppressFreshLaunchStartupReplay = this.shouldSuppressFreshLaunchStartupReplay(
|
|
547
|
+
parsedMessages,
|
|
548
|
+
parsedStatus,
|
|
549
|
+
adapterStatus,
|
|
550
|
+
parsedProviderSessionId,
|
|
551
|
+
);
|
|
552
|
+
if (adapterProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
553
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
554
|
+
}
|
|
555
|
+
if (parsedProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
515
556
|
this.promoteProviderSessionId(parsedProviderSessionId);
|
|
516
557
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
: [];
|
|
558
|
+
if (suppressFreshLaunchStartupReplay) {
|
|
559
|
+
parsedMessages = [];
|
|
560
|
+
}
|
|
561
|
+
const activeChatId = this.providerSessionId || runtime?.runtimeId || this.instanceId;
|
|
522
562
|
const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
|
|
523
563
|
? Math.max(0, Number(parsedStatus.historyMessageCount))
|
|
524
564
|
: null;
|
|
@@ -528,7 +568,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
528
568
|
: [];
|
|
529
569
|
}
|
|
530
570
|
const mergedMessages = this.mergeConversationMessages(parsedMessages);
|
|
531
|
-
const canonicalBackedHistory = this.
|
|
571
|
+
const canonicalBackedHistory = this.shouldHydrateExistingProviderHistory()
|
|
572
|
+
? this.syncCanonicalSavedHistoryIfNeeded()
|
|
573
|
+
: false;
|
|
574
|
+
const statusMessages = canonicalBackedHistory && this.lastPersistedHistoryMessages.length > 0
|
|
575
|
+
? this.lastPersistedHistoryMessages.map((message) => ({
|
|
576
|
+
role: message.role,
|
|
577
|
+
content: message.content,
|
|
578
|
+
kind: message.kind,
|
|
579
|
+
senderName: message.senderName,
|
|
580
|
+
receivedAt: message.receivedAt,
|
|
581
|
+
}))
|
|
582
|
+
: mergedMessages;
|
|
532
583
|
|
|
533
584
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
534
585
|
const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
|
|
@@ -572,7 +623,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
572
623
|
}
|
|
573
624
|
}
|
|
574
625
|
|
|
575
|
-
this.applyProviderResponse(
|
|
626
|
+
this.applyProviderResponse(
|
|
627
|
+
suppressFreshLaunchStartupReplay && parsedStatus && typeof parsedStatus === 'object'
|
|
628
|
+
? { ...parsedStatus, providerSessionId: undefined }
|
|
629
|
+
: parsedStatus,
|
|
630
|
+
{ phase: 'immediate' },
|
|
631
|
+
);
|
|
576
632
|
const surface = resolveProviderStateSurface({
|
|
577
633
|
summaryMetadata: this.summaryMetadata as any,
|
|
578
634
|
controlValues: this.controlValues,
|
|
@@ -592,10 +648,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
592
648
|
status: visibleStatus,
|
|
593
649
|
mode: this.presentationMode,
|
|
594
650
|
activeChat: {
|
|
595
|
-
id:
|
|
651
|
+
id: activeChatId,
|
|
596
652
|
title: parsedStatus?.title || dirName,
|
|
597
653
|
status: activeChatStatus,
|
|
598
|
-
messages:
|
|
654
|
+
messages: statusMessages,
|
|
599
655
|
activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
600
656
|
inputContent: '',
|
|
601
657
|
},
|
|
@@ -743,6 +799,55 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
743
799
|
return role === 'assistant' && !!content;
|
|
744
800
|
}
|
|
745
801
|
|
|
802
|
+
private buildCompletedFinalizationDiagnostic(args: {
|
|
803
|
+
blockReason: string;
|
|
804
|
+
latestStatus?: any;
|
|
805
|
+
latestVisibleStatus: string;
|
|
806
|
+
waitedMs: number;
|
|
807
|
+
pending: CompletedDebouncePending;
|
|
808
|
+
emittedAfterFinalizationTimeout: boolean;
|
|
809
|
+
}): Record<string, unknown> {
|
|
810
|
+
let parsed: any = null;
|
|
811
|
+
let parseError: string | undefined;
|
|
812
|
+
try {
|
|
813
|
+
parsed = this.adapter.getScriptParsedStatus();
|
|
814
|
+
} catch (error: any) {
|
|
815
|
+
parseError = error?.message || String(error);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const visibleMessages = (Array.isArray(parsed?.messages) ? parsed.messages : [])
|
|
819
|
+
.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
820
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
821
|
+
const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
|
|
822
|
+
const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
|
|
823
|
+
const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
|
|
824
|
+
|
|
825
|
+
return {
|
|
826
|
+
providerType: this.type,
|
|
827
|
+
sessionId: this.instanceId,
|
|
828
|
+
providerSessionId: this.providerSessionId || null,
|
|
829
|
+
workspace: this.workingDir,
|
|
830
|
+
blockReason: args.blockReason,
|
|
831
|
+
emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
|
|
832
|
+
waitedMs: args.waitedMs,
|
|
833
|
+
maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
|
|
834
|
+
adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
|
|
835
|
+
latestVisibleStatus: args.latestVisibleStatus,
|
|
836
|
+
parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
|
|
837
|
+
parseError: parseError || undefined,
|
|
838
|
+
finalAssistantPresent: this.completionHasFinalAssistantMessage(parsed?.messages),
|
|
839
|
+
visibleMessageCount: visibleMessages.length,
|
|
840
|
+
lastVisibleRole,
|
|
841
|
+
lastVisibleKind,
|
|
842
|
+
lastVisibleContentLength,
|
|
843
|
+
pendingStartedAt: this.generatingStartedAt || null,
|
|
844
|
+
pendingFirstObservedAt: args.pending.firstObservedAt,
|
|
845
|
+
pendingTimestamp: args.pending.timestamp,
|
|
846
|
+
pendingDurationSec: args.pending.duration,
|
|
847
|
+
previousBlockReason: args.pending.loggedBlockReason || null,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
746
851
|
private hasAdapterPendingResponse(): boolean {
|
|
747
852
|
const adapterAny = this.adapter as any;
|
|
748
853
|
if (adapterAny?.isWaitingForResponse === true) return true;
|
|
@@ -768,29 +873,34 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
768
873
|
return !this.hasAdapterPendingResponse();
|
|
769
874
|
}
|
|
770
875
|
|
|
771
|
-
private
|
|
772
|
-
if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}
|
|
876
|
+
private getCompletedFinalizationBlock(latestVisibleStatus: string): CompletedFinalizationBlock | null {
|
|
877
|
+
if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
|
|
773
878
|
|
|
774
879
|
const adapterAny = this.adapter as any;
|
|
775
|
-
if (adapterAny?.isWaitingForResponse === true) return 'adapter_waiting_for_response';
|
|
776
|
-
if (adapterAny?.currentTurnScope) return 'adapter_turn_scope_active';
|
|
880
|
+
if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
|
|
881
|
+
if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
|
|
882
|
+
if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
|
|
777
883
|
|
|
778
884
|
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
779
885
|
? this.adapter.getPartialResponse()
|
|
780
886
|
: '';
|
|
781
|
-
if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
|
|
887
|
+
if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
|
|
782
888
|
|
|
783
889
|
let parsed: any;
|
|
784
890
|
try {
|
|
785
891
|
parsed = this.adapter.getScriptParsedStatus();
|
|
786
892
|
} catch (error: any) {
|
|
787
|
-
return `parse_error:${error?.message || String(error)}
|
|
893
|
+
return { reason: `parse_error:${error?.message || String(error)}` };
|
|
788
894
|
}
|
|
789
895
|
|
|
790
896
|
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
|
|
791
|
-
if (parsedStatus !== 'idle')
|
|
792
|
-
|
|
793
|
-
|
|
897
|
+
if (parsedStatus !== 'idle') {
|
|
898
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
899
|
+
if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
|
|
900
|
+
return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
|
|
901
|
+
}
|
|
902
|
+
if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
|
|
903
|
+
if (!this.completionHasFinalAssistantMessage(parsed?.messages)) return { reason: 'missing_final_assistant' };
|
|
794
904
|
|
|
795
905
|
return null;
|
|
796
906
|
}
|
|
@@ -817,10 +927,11 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
817
927
|
return;
|
|
818
928
|
}
|
|
819
929
|
|
|
820
|
-
const
|
|
821
|
-
if (
|
|
930
|
+
const block = this.getCompletedFinalizationBlock(latestVisibleStatus);
|
|
931
|
+
if (block) {
|
|
932
|
+
const blockReason = block.reason;
|
|
822
933
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
823
|
-
if (waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
934
|
+
if (block.terminal || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
824
935
|
if (pending.loggedBlockReason !== blockReason) {
|
|
825
936
|
LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
|
|
826
937
|
pending.loggedBlockReason = blockReason;
|
|
@@ -828,7 +939,25 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
828
939
|
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
829
940
|
return;
|
|
830
941
|
}
|
|
831
|
-
|
|
942
|
+
const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
|
|
943
|
+
blockReason,
|
|
944
|
+
latestStatus,
|
|
945
|
+
latestVisibleStatus,
|
|
946
|
+
waitedMs,
|
|
947
|
+
pending,
|
|
948
|
+
emittedAfterFinalizationTimeout: true,
|
|
949
|
+
});
|
|
950
|
+
LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
|
|
951
|
+
this.pushEvent({
|
|
952
|
+
event: 'agent:generating_completed',
|
|
953
|
+
chatTitle: pending.chatTitle,
|
|
954
|
+
duration: pending.duration,
|
|
955
|
+
timestamp: pending.timestamp,
|
|
956
|
+
finalSummary: blockReason.startsWith('parsed_status:')
|
|
957
|
+
? ''
|
|
958
|
+
: extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
|
|
959
|
+
completionDiagnostic,
|
|
960
|
+
});
|
|
832
961
|
this.completedDebouncePending = null;
|
|
833
962
|
this.completedDebounceTimer = null;
|
|
834
963
|
this.generatingStartedAt = 0;
|
|
@@ -876,6 +1005,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
876
1005
|
// during long-running CLI sessions. Keep this path on adapter-owned light
|
|
877
1006
|
// state only; rich provider parsing is reserved for getState/read_chat.
|
|
878
1007
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1008
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
1009
|
+
this.provider,
|
|
1010
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
1011
|
+
);
|
|
1012
|
+
if (adapterProviderSessionId) {
|
|
1013
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
1014
|
+
}
|
|
879
1015
|
const parsedStatus = null;
|
|
880
1016
|
const rawStatus = adapterStatus.status;
|
|
881
1017
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
|
|
@@ -957,6 +1093,23 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
957
1093
|
}
|
|
958
1094
|
} else if (newStatus === 'idle' && this.lastStatus === 'starting') {
|
|
959
1095
|
this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
|
|
1096
|
+
} else if (newStatus === 'error') {
|
|
1097
|
+
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
1098
|
+
this.generatingDebouncePending = null;
|
|
1099
|
+
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
1100
|
+
this.completedDebouncePending = null;
|
|
1101
|
+
this.errorMessage = adapterStatus.errorMessage || this.errorMessage;
|
|
1102
|
+
this.errorReason = (adapterStatus.errorReason as ProviderErrorReason) || this.errorReason;
|
|
1103
|
+
this.pushEvent({
|
|
1104
|
+
event: 'agent:stopped',
|
|
1105
|
+
chatTitle,
|
|
1106
|
+
timestamp: now,
|
|
1107
|
+
finalSummary: adapterStatus.errorMessage || adapterStatus.errorReason || 'Provider reported an error',
|
|
1108
|
+
completionDiagnostic: {
|
|
1109
|
+
reason: adapterStatus.errorReason || 'provider_error',
|
|
1110
|
+
errorMessage: adapterStatus.errorMessage || undefined,
|
|
1111
|
+
},
|
|
1112
|
+
});
|
|
960
1113
|
} else if (newStatus === 'stopped') {
|
|
961
1114
|
// Cancel any pending debounce
|
|
962
1115
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
@@ -1341,7 +1494,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1341
1494
|
this.providerSessionId = nextSessionId;
|
|
1342
1495
|
this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
|
|
1343
1496
|
this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
|
|
1344
|
-
this.
|
|
1497
|
+
if (this.shouldHydrateExistingProviderHistory()) {
|
|
1498
|
+
this.restorePersistedHistoryFromCurrentSession();
|
|
1499
|
+
}
|
|
1345
1500
|
this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
|
|
1346
1501
|
this.onProviderSessionResolved?.({
|
|
1347
1502
|
instanceId: this.instanceId,
|
|
@@ -1354,6 +1509,24 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1354
1509
|
LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
|
|
1355
1510
|
}
|
|
1356
1511
|
|
|
1512
|
+
private shouldHydrateExistingProviderHistory(): boolean {
|
|
1513
|
+
return this.launchMode === 'resume' || this.launchMode === 'manual';
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
private shouldSuppressFreshLaunchStartupReplay(parsedMessages: unknown[], parsedStatus: any, adapterStatus: any, parsedProviderSessionId = ''): boolean {
|
|
1517
|
+
if (this.launchMode !== 'new') return false;
|
|
1518
|
+
if (this.providerSessionId) return false;
|
|
1519
|
+
if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) return false;
|
|
1520
|
+
if (!isIdleStatus(adapterStatus?.status) || !isIdleStatus(parsedStatus?.status)) return false;
|
|
1521
|
+
if (parsedProviderSessionId) return true;
|
|
1522
|
+
|
|
1523
|
+
const newestMessageAt = parsedMessages.reduce<number>((newest, message) => Math.max(newest, getMessageTime(message)), 0);
|
|
1524
|
+
|
|
1525
|
+
// Untimestamped idle parser output during a fresh launch is usually the
|
|
1526
|
+
// provider's last workspace transcript before a new turn exists.
|
|
1527
|
+
return newestMessageAt === 0;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1357
1530
|
private syncCanonicalSavedHistoryIfNeeded(): boolean {
|
|
1358
1531
|
if (!this.providerSessionId) return false;
|
|
1359
1532
|
const canonicalHistory = this.provider.canonicalHistory;
|