@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.
Files changed (80) hide show
  1. package/dist/chat/subscription-updates.d.ts +1 -0
  2. package/dist/cli-adapters/provider-cli-adapter.d.ts +21 -0
  3. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  4. package/dist/cli-adapters/provider-cli-shared.d.ts +14 -0
  5. package/dist/commands/router.d.ts +22 -0
  6. package/dist/config/chat-history.d.ts +4 -0
  7. package/dist/config/mesh-config.d.ts +68 -1
  8. package/dist/git/git-commands.d.ts +5 -1
  9. package/dist/index.d.ts +15 -5
  10. package/dist/index.js +7029 -1368
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +6984 -1351
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/installer.d.ts +1 -4
  15. package/dist/launch.d.ts +1 -1
  16. package/dist/logging/async-batch-writer.d.ts +10 -0
  17. package/dist/mesh/beads-db.d.ts +18 -0
  18. package/dist/mesh/mesh-active-work.d.ts +73 -0
  19. package/dist/mesh/mesh-events.d.ts +26 -5
  20. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  21. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  22. package/dist/mesh/mesh-ledger.d.ts +38 -1
  23. package/dist/mesh/mesh-refine-status.d.ts +27 -0
  24. package/dist/mesh/mesh-work-queue.d.ts +27 -5
  25. package/dist/mesh/preview-freshness.d.ts +18 -0
  26. package/dist/mesh/refine-config.d.ts +193 -0
  27. package/dist/mesh/worktree-bootstrap-config.d.ts +115 -0
  28. package/dist/providers/chat-message-normalization.d.ts +1 -0
  29. package/dist/providers/cli-provider-instance.d.ts +4 -1
  30. package/dist/repo-mesh-types.d.ts +62 -0
  31. package/dist/status/reporter.d.ts +2 -0
  32. package/package.json +3 -1
  33. package/src/boot/daemon-lifecycle.ts +1 -0
  34. package/src/chat/subscription-updates.ts +5 -1
  35. package/src/cli-adapters/provider-cli-adapter.ts +453 -17
  36. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  37. package/src/cli-adapters/provider-cli-parse.ts +4 -0
  38. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  39. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  40. package/src/cli-adapters/provider-cli-shared.ts +32 -10
  41. package/src/commands/chat-commands.ts +730 -30
  42. package/src/commands/cli-manager.ts +129 -1
  43. package/src/commands/handler.ts +8 -1
  44. package/src/commands/mesh-coordinator.ts +13 -143
  45. package/src/commands/router.ts +3239 -430
  46. package/src/config/chat-history.ts +37 -9
  47. package/src/config/mesh-config.ts +249 -2
  48. package/src/config/recent-activity.ts +8 -2
  49. package/src/daemon/dev-cli-debug.ts +10 -1
  50. package/src/detection/ide-detector.ts +26 -16
  51. package/src/git/git-commands.ts +17 -5
  52. package/src/index.ts +41 -4
  53. package/src/installer.d.ts +1 -1
  54. package/src/installer.ts +8 -6
  55. package/src/launch.d.ts +1 -1
  56. package/src/launch.ts +37 -28
  57. package/src/logging/async-batch-writer.ts +55 -0
  58. package/src/logging/logger.ts +2 -1
  59. package/src/mesh/beads-db.ts +176 -0
  60. package/src/mesh/coordinator-prompt.ts +31 -8
  61. package/src/mesh/mesh-active-work.ts +292 -0
  62. package/src/mesh/mesh-events.ts +389 -47
  63. package/src/mesh/mesh-fast-forward.ts +430 -0
  64. package/src/mesh/mesh-host-ownership.ts +73 -0
  65. package/src/mesh/mesh-ledger.ts +138 -1
  66. package/src/mesh/mesh-refine-status.ts +145 -0
  67. package/src/mesh/mesh-work-queue.ts +199 -137
  68. package/src/mesh/preview-freshness.ts +118 -0
  69. package/src/mesh/refine-config.ts +366 -0
  70. package/src/mesh/worktree-bootstrap-config.ts +234 -0
  71. package/src/providers/approval-utils.ts +12 -5
  72. package/src/providers/chat-message-normalization.ts +7 -12
  73. package/src/providers/cli-provider-instance.ts +201 -28
  74. package/src/providers/ide-provider-instance.ts +17 -3
  75. package/src/providers/provider-loader.ts +10 -4
  76. package/src/providers/read-chat-contract.ts +1 -1
  77. package/src/providers/version-archive.ts +38 -20
  78. package/src/repo-mesh-types.ts +67 -0
  79. package/src/status/reporter.ts +15 -0
  80. 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: 0, label: 'Approve' };
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: 0, label: labels[0] };
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 = 500,
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
- // Fallback: last user-facing message of any role
22
- for (let i = messages.length - 1; i >= 0; i--) {
23
- const msg = messages[i];
24
- if (!msg) continue;
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
- this.errorMessage = undefined;
496
- this.errorReason = undefined;
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
- if (parsedProviderSessionId) {
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
- const runtime = this.adapter.getRuntimeMetadata();
518
- this.maybeAppendRuntimeRecoveryMessage(runtime);
519
- let parsedMessages = Array.isArray(parsedStatus?.messages)
520
- ? parsedStatus.messages
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.syncCanonicalSavedHistoryIfNeeded();
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(parsedStatus, { phase: 'immediate' });
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: `${this.type}_${this.workingDir}`,
651
+ id: activeChatId,
596
652
  title: parsedStatus?.title || dirName,
597
653
  status: activeChatStatus,
598
- messages: mergedMessages,
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 getCompletedFinalizationBlockReason(latestVisibleStatus: string): string | null {
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') return `parsed_status:${parsedStatus}`;
792
- if (parsed?.activeModal || parsed?.modal) return 'parsed_modal_active';
793
- if (!this.completionHasFinalAssistantMessage(parsed?.messages)) return 'missing_final_assistant';
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 blockReason = this.getCompletedFinalizationBlockReason(latestVisibleStatus);
821
- if (blockReason) {
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
- LOG.warn('CLI', `[${this.type}] suppressed completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
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.restorePersistedHistoryFromCurrentSession();
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;