@adhdev/daemon-core 0.9.82-rc.335 → 0.9.82-rc.337

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.
@@ -39,6 +39,25 @@ export declare function readRefineJobId(event: {
39
39
  metadataEvent?: Record<string, unknown>;
40
40
  } | Record<string, unknown>): string;
41
41
  export declare function readWorkerResultMetadata(event: Record<string, unknown>): Record<string, unknown> | undefined;
42
+ /**
43
+ * A coordinator that surfaces a REMOTE worker's mesh session has no local instance for
44
+ * it, so the status snapshot's getLastDisplayMessage has nothing to read and the only
45
+ * preview the coordinator-mirrored copy could ever carry comes from the worker's
46
+ * completion event. The worker's latest assistant reply rides on that event as
47
+ * `finalSummary` (and as `workerResult.summary` / `result.summary` on some paths).
48
+ *
49
+ * This resolves that assistant text into a preview the coordinator can stamp onto its
50
+ * mirror entry so the mobile inbox shows the worker's latest assistant response instead
51
+ * of being stuck on the first dispatched user task (which is the only message the
52
+ * coordinator-side transcript ever holds). Returns undefined when the event carries no
53
+ * assistant text — non-completion lifecycle events (generating_started / ready without a
54
+ * summary) must NOT clobber a previously surfaced preview.
55
+ */
56
+ export declare function resolveMeshSurfacedSessionPreview(metadataEvent: Record<string, unknown>): {
57
+ preview: string;
58
+ role: 'assistant';
59
+ receivedAt: number;
60
+ } | undefined;
42
61
  export declare function buildMeshSystemMessage(args: {
43
62
  event: string;
44
63
  nodeLabel: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.335",
3
+ "version": "0.9.82-rc.337",
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",
@@ -46,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.335",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.337",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -120,17 +120,29 @@ export function appendBoundedText(current: string, chunk: string, maxChars: numb
120
120
 
121
121
  // Force-send (mesh coordinator dispatch / reconcile redelivery) writes raw
122
122
  // keystrokes straight into the PTY, bypassing the normal echo-wait submit
123
- // pipeline. Historically it wrote `text + sendKey` in a single PTY write with
124
- // zero settle time. On TUI input boxes that re-render the pasted text, the
125
- // trailing submit key could arrive before the input line had stabilized and be
126
- // swallowed by the input handler leaving the prompt typed but never submitted
127
- // ("text injected, Enter not pressed"). We now split inject from submit and let
128
- // the input settle first. FORCE_SUBMIT_SETTLE_MS is the minimum gap after the
129
- // text write before the submit key is sent; FORCE_SUBMIT_MAX_WAIT_MS caps the
130
- // echo-gated wait so the submit always fires even if the echo never appears.
123
+ // pipeline.
124
+ //
125
+ // History of this path's failure modes:
126
+ // 1. Originally it wrote `text + sendKey` in a single PTY write with zero
127
+ // settle time. Injected into an idle TUI input box that was still entering
128
+ // its input-accepting state, the trailing submit key could be swallowed
129
+ // prompt typed but never submitted ("text injected, Enter not pressed").
130
+ // 2. The first fix split inject from submit: write(text) echo-gated settle
131
+ // write(sendKey) as a *separate* PTY write. That broke win32: ConPTY/TUI
132
+ // does not recognize a lone '\r' that arrives in its own PTY chunk
133
+ // 150ms+ after the text as a submit key, and the win32 echo gate never
134
+ // matched (ConPTY echo/parsing differences), so the cap was burned and the
135
+ // same detached lone CR was emitted — adding latency while the Enter still
136
+ // got swallowed.
137
+ //
138
+ // Current behavior: keep a fixed settle gap so the input handler is in its
139
+ // input-accepting state, then write `content + sendKey` as a SINGLE atomic PTY
140
+ // write — the same verified mechanism the normal `immediate` submit strategy
141
+ // uses (submitImmediatePrompt). With the Enter in the same write unit as the
142
+ // text, win32 ConPTY always sees it as a submit and the race that the split was
143
+ // trying to solve cannot happen (the Enter can never be separated from the
144
+ // text). FORCE_SUBMIT_SETTLE_MS is that minimum pre-submit gap.
131
145
  const FORCE_SUBMIT_SETTLE_MS = 150;
132
- const FORCE_SUBMIT_MAX_WAIT_MS = 1500;
133
- const FORCE_SUBMIT_POLL_MS = 50;
134
146
 
135
147
  // ─── Adapter ────────────────────────────────────────
136
148
 
@@ -1312,34 +1324,31 @@ export class ProviderCliAdapter implements CliAdapter {
1312
1324
  return;
1313
1325
  }
1314
1326
  LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.engine.currentStatus}`);
1315
- // Split inject from submit. Writing `content + sendKey` in one PTY write
1316
- // can race a TUI input box that is still absorbing/redrawing the pasted
1317
- // text, so the trailing submit key gets eaten and the prompt sits typed
1318
- // but unsent. Write the text first, let the input line settle (echo-gated
1319
- // when we can verify it, otherwise a fixed minimum gap), then write the
1320
- // submit key separately so it lands as a clean Enter on a stable prompt.
1321
- await this.writeToPty(content);
1322
- await this.waitForForceSubmitSettle(content);
1323
- await this.writeToPty(this.sendKey);
1327
+ // Settle, then submit atomically. The previous split-write fix
1328
+ // (write text settle write a separate '\r') regressed win32:
1329
+ // ConPTY does not treat a lone CR arriving in its own PTY chunk after a
1330
+ // delay as a submit key, so the Enter was swallowed and the prompt sat
1331
+ // typed-but-unsent. Instead, honor a fixed settle gap so the TUI input
1332
+ // handler is in its input-accepting state, then write `content + sendKey`
1333
+ // as ONE PTY write — the same atomic submit the normal `immediate`
1334
+ // strategy uses. Keeping the Enter in the same write unit as the text is
1335
+ // the invariant that makes win32 ConPTY recognize the submit, and it
1336
+ // also makes the original inject↔submit race impossible (the Enter can
1337
+ // never be separated from the text it submits). The settle still guards
1338
+ // the original concern — that a force-dispatch injected into a freshly
1339
+ // idle session lands before the TUI is ready to accept input.
1340
+ await this.waitForForceSubmitSettle();
1341
+ await this.writeToPty(content + this.sendKey);
1324
1342
  this.onStatusChange?.();
1325
1343
  }
1326
1344
 
1327
- private async waitForForceSubmitSettle(content: string): Promise<void> {
1328
- const startedAt = Date.now();
1329
- const normalizedPromptSnippet = normalizePromptText(extractPromptRetrySnippet(content));
1330
- // Always honor a minimum settle so the input handler has time to finish
1331
- // ingesting the pasted text before the submit key arrives.
1345
+ private async waitForForceSubmitSettle(): Promise<void> {
1346
+ // A fixed minimum gap so the input handler is ready to accept the paste.
1347
+ // We deliberately do NOT echo-gate here: the submit key is written in the
1348
+ // same atomic PTY write as the text (forceSendMessage), so there is no
1349
+ // separate Enter that could race the echo, and on win32 the echo gate
1350
+ // never reliably matched anyway.
1332
1351
  await new Promise<void>(resolve => setTimeout(resolve, FORCE_SUBMIT_SETTLE_MS));
1333
- if (!normalizedPromptSnippet) return;
1334
- // Beyond the minimum gap, keep waiting (up to the cap) until the prompt
1335
- // echo is visible on screen — that is the strongest signal the input line
1336
- // has stabilized and the Enter will be applied to the typed prompt.
1337
- while (Date.now() - startedAt < FORCE_SUBMIT_MAX_WAIT_MS) {
1338
- if (!this.ptyProcess) return;
1339
- const screenText = this.terminalScreen.getText();
1340
- if (promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
1341
- await new Promise<void>(resolve => setTimeout(resolve, FORCE_SUBMIT_POLL_MS));
1342
- }
1343
1352
  }
1344
1353
 
1345
1354
  private enqueuePendingOutboundMessage(text: string, reason: string): void {
@@ -30,6 +30,7 @@ import {
30
30
  resolveEventSessionId,
31
31
  readRefineJobId,
32
32
  readWorkerResultMetadata,
33
+ resolveMeshSurfacedSessionPreview,
33
34
  } from './mesh-events-utils.js';
34
35
 
35
36
  // The set of coordinator-daemon ids this daemon answers to when draining the
@@ -1394,6 +1395,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1394
1395
  // across standalone and cloud.
1395
1396
  if (components.onMeshCoordinatorEventForwarded) {
1396
1397
  try {
1398
+ // T: the coordinator surfaces a remote worker's session but holds no local
1399
+ // instance for it, so the status snapshot can't derive a preview and the
1400
+ // mirror would stay stuck on the first dispatched user task. Resolve the
1401
+ // worker's latest assistant reply (carried on the completion event's
1402
+ // finalSummary / workerResult) into a preview the mirror can stamp, so the
1403
+ // mobile inbox reflects the assistant response. Only completion-style events
1404
+ // carry assistant text; for everything else this is undefined and the prior
1405
+ // surfaced preview is preserved downstream (no clobber).
1406
+ const surfacedPreview = resolveMeshSurfacedSessionPreview(args.metadataEvent);
1397
1407
  components.onMeshCoordinatorEventForwarded({
1398
1408
  event: args.event,
1399
1409
  meshId: args.meshId,
@@ -1405,6 +1415,11 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1405
1415
  workspace: readNonEmptyString(args.metadataEvent.workspace)
1406
1416
  || readNonEmptyString(args.metadataEvent.workspaceName)
1407
1417
  || undefined,
1418
+ ...(surfacedPreview ? {
1419
+ meshSessionLastMessagePreview: surfacedPreview.preview,
1420
+ meshSessionLastMessageRole: surfacedPreview.role,
1421
+ meshSessionLastMessageAt: surfacedPreview.receivedAt || undefined,
1422
+ } : {}),
1408
1423
  });
1409
1424
  } catch { /* dashboard metadata sync is best-effort */ }
1410
1425
  }
@@ -107,6 +107,52 @@ export function readWorkerResultMetadata(event: Record<string, unknown>): Record
107
107
  return readRecord(event.workerResult) || readRecord(event.meshWorkerResult) || readRecord(event.structuredResult);
108
108
  }
109
109
 
110
+ const MESH_SURFACED_PREVIEW_MAX_CHARS = 512;
111
+
112
+ /**
113
+ * A coordinator that surfaces a REMOTE worker's mesh session has no local instance for
114
+ * it, so the status snapshot's getLastDisplayMessage has nothing to read and the only
115
+ * preview the coordinator-mirrored copy could ever carry comes from the worker's
116
+ * completion event. The worker's latest assistant reply rides on that event as
117
+ * `finalSummary` (and as `workerResult.summary` / `result.summary` on some paths).
118
+ *
119
+ * This resolves that assistant text into a preview the coordinator can stamp onto its
120
+ * mirror entry so the mobile inbox shows the worker's latest assistant response instead
121
+ * of being stuck on the first dispatched user task (which is the only message the
122
+ * coordinator-side transcript ever holds). Returns undefined when the event carries no
123
+ * assistant text — non-completion lifecycle events (generating_started / ready without a
124
+ * summary) must NOT clobber a previously surfaced preview.
125
+ */
126
+ export function resolveMeshSurfacedSessionPreview(
127
+ metadataEvent: Record<string, unknown>,
128
+ ): { preview: string; role: 'assistant'; receivedAt: number } | undefined {
129
+ const workerResult = readWorkerResultMetadata(metadataEvent);
130
+ const resultRecord = readRecord(metadataEvent.result);
131
+ const summaryText = readNonEmptyString(metadataEvent.finalSummary)
132
+ || readNonEmptyString(workerResult?.summary)
133
+ || readNonEmptyString(workerResult?.finalSummary)
134
+ || readNonEmptyString(resultRecord?.summary)
135
+ || readNonEmptyString(resultRecord?.finalSummary);
136
+ if (!summaryText) return undefined;
137
+ const truncationSuffix = '...[truncated]';
138
+ const preview = summaryText.length > MESH_SURFACED_PREVIEW_MAX_CHARS
139
+ ? `${summaryText.slice(0, MESH_SURFACED_PREVIEW_MAX_CHARS - truncationSuffix.length)}${truncationSuffix}`
140
+ : summaryText;
141
+ const timestamp = readEventTimestampValue(metadataEvent.timestamp);
142
+ return { preview, role: 'assistant', receivedAt: timestamp };
143
+ }
144
+
145
+ function readEventTimestampValue(value: unknown): number {
146
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
147
+ if (typeof value === 'string' && value.trim()) {
148
+ const parsed = Number(value);
149
+ if (Number.isFinite(parsed)) return parsed;
150
+ const dateMs = Date.parse(value);
151
+ if (Number.isFinite(dateMs)) return dateMs;
152
+ }
153
+ return 0;
154
+ }
155
+
110
156
  function formatCompletionMetadata(event: Record<string, unknown>): string {
111
157
  const completionDiagnostic = event.completionDiagnostic && typeof event.completionDiagnostic === 'object'
112
158
  ? event.completionDiagnostic as Record<string, unknown>