@adhdev/daemon-core 0.9.82-rc.174 → 0.9.82-rc.176

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.
@@ -104,6 +104,9 @@ export declare class CliProviderInstance implements ProviderInstance {
104
104
  private completedDebouncePending;
105
105
  private enforceFreshSessionLaunchIfNeeded;
106
106
  private completionHasFinalAssistantMessage;
107
+ private readExternalCompletionMessages;
108
+ private completionFinalAssistantEvidence;
109
+ private completionFinalSummary;
107
110
  private buildCompletedFinalizationDiagnostic;
108
111
  private hasAdapterPendingResponse;
109
112
  private shouldSuppressStaleParsedBusyStatus;
@@ -86,6 +86,16 @@ export interface SpecDriverOpts {
86
86
  */
87
87
  extraCliArgs?: string[];
88
88
  }
89
+ /**
90
+ * Pick the effective delay (ms) to wait between writing the prompt body and
91
+ * writing the submit_key. Exported for tests; production callers go through
92
+ * actuallySendMessage. Floors to SUBMIT_DELAY_FLOOR_MS so specs that omit
93
+ * delay_ms_before_submit (e.g. claude, antigravity) still get baseline
94
+ * protection against the paste→submit race. Adds line-count bonus so
95
+ * multi-line prompts get more settling time on TUIs that re-render per
96
+ * embedded `\n`.
97
+ */
98
+ export declare function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text: string): number;
89
99
  export declare class SpecDriver {
90
100
  private readonly opts;
91
101
  private spec;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.174",
3
+ "version": "0.9.82-rc.176",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -64,6 +64,12 @@ type CompletedFinalizationBlock = {
64
64
  terminal?: boolean;
65
65
  };
66
66
 
67
+ type CompletionFinalAssistantEvidence = {
68
+ present: boolean;
69
+ messages: unknown[];
70
+ source: 'parsed' | 'external-native' | 'unavailable';
71
+ };
72
+
67
73
  const COMPLETED_FINALIZATION_RETRY_MS = 1000;
68
74
  const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
69
75
 
@@ -890,6 +896,56 @@ export class CliProviderInstance implements ProviderInstance {
890
896
  return true;
891
897
  }
892
898
 
899
+ private readExternalCompletionMessages(): unknown[] | null {
900
+ const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
901
+ if (!adapterOwnsMessagesElsewhere) return null;
902
+ if (!this.providerSessionId) return null;
903
+ if (!isNativeSourceCanonicalHistory(this.provider.nativeHistory)) return null;
904
+
905
+ const restoredHistory = readProviderChatHistory(this.type, {
906
+ canonicalHistory: this.provider.nativeHistory,
907
+ historySessionId: this.providerSessionId,
908
+ workspace: this.workingDir,
909
+ offset: 0,
910
+ limit: Number.MAX_SAFE_INTEGER,
911
+ historyBehavior: this.provider.historyBehavior,
912
+ scripts: this.provider.scripts as any,
913
+ sessionStartedAtMs: this.startedAt,
914
+ });
915
+ if (restoredHistory.source !== 'provider-native') return null;
916
+ return restoredHistory.messages;
917
+ }
918
+
919
+ private completionFinalAssistantEvidence(parsedMessages: unknown): CompletionFinalAssistantEvidence {
920
+ if (this.completionHasFinalAssistantMessage(parsedMessages)) {
921
+ return {
922
+ present: true,
923
+ messages: Array.isArray(parsedMessages) ? parsedMessages : [],
924
+ source: 'parsed',
925
+ };
926
+ }
927
+
928
+ const externalMessages = this.readExternalCompletionMessages();
929
+ if (externalMessages) {
930
+ return {
931
+ present: this.completionHasFinalAssistantMessage(externalMessages),
932
+ messages: externalMessages,
933
+ source: 'external-native',
934
+ };
935
+ }
936
+
937
+ return {
938
+ present: false,
939
+ messages: Array.isArray(parsedMessages) ? parsedMessages : [],
940
+ source: 'unavailable',
941
+ };
942
+ }
943
+
944
+ private completionFinalSummary(parsedMessages: unknown): string | undefined {
945
+ const evidence = this.completionFinalAssistantEvidence(parsedMessages);
946
+ return extractFinalSummaryFromMessages(evidence.messages as any);
947
+ }
948
+
893
949
  private buildCompletedFinalizationDiagnostic(args: {
894
950
  blockReason: string;
895
951
  latestStatus?: any;
@@ -906,7 +962,8 @@ export class CliProviderInstance implements ProviderInstance {
906
962
  parseError = error?.message || String(error);
907
963
  }
908
964
 
909
- const visibleMessages = (Array.isArray(parsed?.messages) ? parsed.messages : [])
965
+ const evidence = this.completionFinalAssistantEvidence(parsed?.messages);
966
+ const visibleMessages = (Array.isArray(evidence.messages) ? evidence.messages : [])
910
967
  .filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
911
968
  const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
912
969
  const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
@@ -926,7 +983,8 @@ export class CliProviderInstance implements ProviderInstance {
926
983
  latestVisibleStatus: args.latestVisibleStatus,
927
984
  parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
928
985
  parseError: parseError || undefined,
929
- finalAssistantPresent: this.completionHasFinalAssistantMessage(parsed?.messages),
986
+ finalAssistantPresent: evidence.present,
987
+ finalAssistantEvidenceSource: evidence.source,
930
988
  visibleMessageCount: visibleMessages.length,
931
989
  lastVisibleRole,
932
990
  lastVisibleKind,
@@ -1003,14 +1061,26 @@ export class CliProviderInstance implements ProviderInstance {
1003
1061
  return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
1004
1062
  }
1005
1063
  if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
1006
- // SpecCliAdapter never populates parsed.messages — chat history flows
1007
- // through the daemon's native-history pipeline, not the status hook.
1008
- // Skipping the final-assistant gate avoids a 30s stall on every turn
1009
- // for spec-routed providers (agy / codex / claude / hermes).
1010
1064
  const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
1011
- if (!adapterOwnsMessagesElsewhere
1012
- && !this.completionHasFinalAssistantMessage(parsed?.messages)) {
1013
- return { reason: 'missing_final_assistant' };
1065
+ const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
1066
+ if (!finalAssistantEvidence.present) {
1067
+ if (adapterOwnsMessagesElsewhere) {
1068
+ if (finalAssistantEvidence.source === 'external-native') {
1069
+ return { reason: 'missing_final_assistant', terminal: true };
1070
+ }
1071
+ // SpecCliAdapter never populates parsed.messages — chat history flows
1072
+ // through the daemon's native-history pipeline, not the status hook.
1073
+ // If that pipeline is unavailable, keep the old skip behavior for
1074
+ // providers that have not opted into strict final-assistant evidence.
1075
+ if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
1076
+ return { reason: 'missing_final_assistant', terminal: true };
1077
+ }
1078
+ } else {
1079
+ return {
1080
+ reason: 'missing_final_assistant',
1081
+ terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
1082
+ };
1083
+ }
1014
1084
  }
1015
1085
 
1016
1086
  // Guard: if the screen still shows an approval/choice prompt as the last visible text,
@@ -1082,7 +1152,7 @@ export class CliProviderInstance implements ProviderInstance {
1082
1152
  timestamp: pending.timestamp,
1083
1153
  finalSummary: blockReason.startsWith('parsed_status:')
1084
1154
  ? ''
1085
- : extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
1155
+ : this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
1086
1156
  completionDiagnostic,
1087
1157
  });
1088
1158
  this.completedDebouncePending = null;
@@ -1098,7 +1168,7 @@ export class CliProviderInstance implements ProviderInstance {
1098
1168
  chatTitle: pending.chatTitle,
1099
1169
  duration: pending.duration,
1100
1170
  timestamp: pending.timestamp,
1101
- finalSummary: extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
1171
+ finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
1102
1172
  });
1103
1173
  this.completedDebouncePending = null;
1104
1174
  this.completedDebounceTimer = null;
@@ -1255,8 +1325,14 @@ export class CliProviderInstance implements ProviderInstance {
1255
1325
  // Emit completion for mesh task association even though the UI generating
1256
1326
  // started/completed pair is suppressed (too short for visible UI update).
1257
1327
  let shortFinalSummary: string | undefined;
1258
- try { shortFinalSummary = extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages); } catch { /* best-effort */ }
1259
- if ((this.provider as any).requiresFinalAssistantBeforeIdle === true && !shortFinalSummary) {
1328
+ let shortEvidenceSource: CompletionFinalAssistantEvidence['source'] = 'unavailable';
1329
+ try {
1330
+ const parsedMessages = this.adapter?.getScriptParsedStatus()?.messages;
1331
+ const evidence = this.completionFinalAssistantEvidence(parsedMessages);
1332
+ shortEvidenceSource = evidence.source;
1333
+ shortFinalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
1334
+ } catch { /* best-effort */ }
1335
+ if (((this.provider as any).requiresFinalAssistantBeforeIdle === true || shortEvidenceSource === 'external-native') && !shortFinalSummary) {
1260
1336
  LOG.info('CLI', `[${this.type}] suppressed short completion without final assistant evidence`);
1261
1337
  } else {
1262
1338
  this.pushEvent({
@@ -1268,6 +1344,7 @@ export class CliProviderInstance implements ProviderInstance {
1268
1344
  completionDiagnostic: {
1269
1345
  reason: 'short_generating_suppressed',
1270
1346
  shortDurationMs,
1347
+ finalAssistantEvidenceSource: shortEvidenceSource,
1271
1348
  },
1272
1349
  });
1273
1350
  }
@@ -102,6 +102,36 @@ const STARTUP_GRACE_MS = 2500;
102
102
  * the post-turn idle indication noticeably. */
103
103
  const BUSY_HOLD_MS = 6000;
104
104
 
105
+ /** Minimum delay between the prompt body and the submit_key in send_message.
106
+ * Claude / antigravity specs ship without an explicit delay_ms_before_submit
107
+ * and their TUIs sometimes drop the `\r` event when it arrives in the same
108
+ * PTY chunk as the text body — the prompt sits visible in the input field
109
+ * but is never submitted until the user hits Enter manually. 200ms matches
110
+ * codex's explicit setting and is barely perceptible to a human caller. */
111
+ const SUBMIT_DELAY_FLOOR_MS = 200;
112
+
113
+ function countNewlines(s: string): number {
114
+ let n = 0;
115
+ for (let i = 0; i < s.length; i += 1) if (s.charCodeAt(i) === 10) n += 1;
116
+ return n;
117
+ }
118
+
119
+ /**
120
+ * Pick the effective delay (ms) to wait between writing the prompt body and
121
+ * writing the submit_key. Exported for tests; production callers go through
122
+ * actuallySendMessage. Floors to SUBMIT_DELAY_FLOOR_MS so specs that omit
123
+ * delay_ms_before_submit (e.g. claude, antigravity) still get baseline
124
+ * protection against the paste→submit race. Adds line-count bonus so
125
+ * multi-line prompts get more settling time on TUIs that re-render per
126
+ * embedded `\n`.
127
+ */
128
+ export function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text: string): number {
129
+ const lines = countNewlines(text);
130
+ const linesBonus = Math.min(800, lines * 80);
131
+ const spec = typeof specBeforeSubmit === 'number' && specBeforeSubmit > 0 ? specBeforeSubmit : 0;
132
+ return Math.max(spec, SUBMIT_DELAY_FLOOR_MS + linesBonus);
133
+ }
134
+
105
135
  export class SpecDriver {
106
136
  private spec!: CliSpec;
107
137
  private adapter!: TerminalAdapter;
@@ -371,7 +401,15 @@ export class SpecDriver {
371
401
  private actuallySendMessage(text: string): void {
372
402
  const sm = this.spec.send_message;
373
403
  const perChar = sm.delay_ms_per_char ?? 0;
374
- const beforeSubmit = sm.delay_ms_before_submit ?? 0;
404
+ // Floor the gap between text and submit_key. Without this, claude /
405
+ // antigravity specs (which leave delay_ms_before_submit unset)
406
+ // race: text bytes and `\r` arrive back-to-back, the TUI processes
407
+ // the `\r` while still digesting the text input, and the prompt
408
+ // sits visible in the input field but never submits — the user has
409
+ // to press Enter manually. Scale with line count so multi-line
410
+ // pastes (which take longer for the TUI to render) get more
411
+ // settling time.
412
+ const beforeSubmit = resolveSubmitDelayMs(sm.delay_ms_before_submit, text);
375
413
  if (perChar === 0) {
376
414
  this.adapter.send_keys(text);
377
415
  if (beforeSubmit > 0) setTimeout(() => this.adapter.send_keys(sm.submit_key), beforeSubmit);