@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.
- package/dist/index.js +37 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +37 -21
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-utils.d.ts +19 -0
- package/package.json +2 -2
- package/src/cli-adapters/provider-cli-adapter.ts +43 -34
- package/src/mesh/mesh-events-coordinator.ts +15 -0
- package/src/mesh/mesh-events-utils.ts +46 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
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
|
-
//
|
|
1316
|
-
//
|
|
1317
|
-
//
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1320
|
-
//
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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(
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
//
|
|
1331
|
-
//
|
|
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>
|