@adhdev/daemon-core 0.9.82-rc.408 → 0.9.82-rc.409
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 +49 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +49 -15
- package/dist/index.mjs.map +1 -1
- package/dist/providers/chat-message-normalization.d.ts +18 -0
- package/package.json +2 -2
- package/src/commands/chat-commands-write.ts +11 -0
- package/src/providers/chat-message-normalization.ts +53 -5
- package/src/providers/cli-provider-instance.ts +49 -8
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import type { ChatMessage } from '../types.js';
|
|
2
2
|
export declare const DEFAULT_FINAL_SUMMARY_MAX_CHARS = 16000;
|
|
3
3
|
export declare function extractFinalSummaryFromMessages(messages: ChatMessage[] | null | undefined, maxChars?: number): string;
|
|
4
|
+
/**
|
|
5
|
+
* Turn-scoped variant of extractFinalSummaryFromMessages. Selects the last
|
|
6
|
+
* user-facing assistant/model bubble whose own timestamp is at/after the
|
|
7
|
+
* producing turn's start (`minTimestampMs`). This is the NOTIF Defect-B fix:
|
|
8
|
+
* a completion event's finalSummary must describe the turn THAT completed, not
|
|
9
|
+
* the prior task's last bubble. For native-source providers (claude-cli) the
|
|
10
|
+
* external transcript holds the ENTIRE session history filtered only by session
|
|
11
|
+
* start, so a completion debounce that fires before the producing turn's final
|
|
12
|
+
* assistant bubble has landed would otherwise echo the previous task's tail.
|
|
13
|
+
* A message whose timestamp predates the turn start is skipped; if no in-turn
|
|
14
|
+
* assistant bubble exists yet, returns '' (weak/empty) — never the stale tail.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors the reconcile path's transcriptAfterDispatch guard (mesh-events-stale):
|
|
17
|
+
* a bubble only counts if its timestamp proves it was produced after the turn began.
|
|
18
|
+
* When `minTimestampMs` is undefined the behaviour is identical to the unscoped
|
|
19
|
+
* extractor (no turn boundary known → no filtering).
|
|
20
|
+
*/
|
|
21
|
+
export declare function extractFinalSummaryFromMessagesAfter(messages: ChatMessage[] | null | undefined, minTimestampMs: number | undefined, maxChars?: number): string;
|
|
4
22
|
/**
|
|
5
23
|
* Like extractFinalSummaryFromMessages but also returns the ISO timestamp of the
|
|
6
24
|
* selected final assistant/model message. Completion reconciliation needs the
|
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.409",
|
|
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.409",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -752,6 +752,17 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
|
|
|
752
752
|
: status?.status;
|
|
753
753
|
LOG.info('Command', `[resolveAction] CLI PTY gate target=${String(args?.targetSessionId || '')} rawStatus=${String(status?.status || '')} effectiveStatus=${String(effectiveStatus || '')} statusModal=${statusModal ? 'yes' : 'no'} surfacedModal=${surfacedModal ? 'yes' : 'no'} parsedModal=${parsedModal ? 'yes' : 'no'} instance=${targetInstance ? 'yes' : 'no'}`);
|
|
754
754
|
if (!effectiveModal) {
|
|
755
|
+
// APPROVAL Defect-B (live re-probe race): the modal is gone because the worker
|
|
756
|
+
// already resolved this very approval moments ago (delegated auto-approve fired,
|
|
757
|
+
// or a prior resolveAction landed) and the coordinator's approve raced in just
|
|
758
|
+
// after. That is NOT a caller error — return a SOFT already_resolved result so the
|
|
759
|
+
// coordinator does not hard-fail the task on a benign race. Mirrors the in-modal
|
|
760
|
+
// idempotency guard below (isApprovalRecentlyResolved → stalePrompt). Only a session
|
|
761
|
+
// that never had a recently-resolved approval reports the hard 'Not in approval state'.
|
|
762
|
+
if (typeof adapter.isApprovalRecentlyResolved === 'function' && adapter.isApprovalRecentlyResolved()) {
|
|
763
|
+
LOG.info('Command', `[resolveAction] CLI PTY → already_resolved (modal gone, resolved within cooldown)`);
|
|
764
|
+
return { success: true, alreadyResolved: true, status: 'already_resolved' };
|
|
765
|
+
}
|
|
755
766
|
return { success: false, error: 'Not in approval state' };
|
|
756
767
|
}
|
|
757
768
|
const buttons: string[] = Array.isArray(effectiveModal.buttons) ? effectiveModal.buttons : [];
|
|
@@ -27,23 +27,71 @@ export function extractFinalSummaryFromMessages(
|
|
|
27
27
|
return '';
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function
|
|
30
|
+
function readChatMessageTimestampMs(message: ChatMessage | null | undefined): number | undefined {
|
|
31
31
|
if (!message) return undefined;
|
|
32
32
|
const record = message as ChatMessage & Record<string, unknown>;
|
|
33
|
-
for (const value of [record.timestamp, record.createdAt, record.created_at, record.updatedAt, record.time]) {
|
|
33
|
+
for (const value of [record.timestamp, record.createdAt, record.created_at, record.updatedAt, record.time, record.receivedAt]) {
|
|
34
34
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
35
35
|
// Heuristic seconds-vs-ms detection mirrors the mesh transcript reader.
|
|
36
|
-
|
|
37
|
-
return new Date(ms).toISOString();
|
|
36
|
+
return value > 10_000_000_000 ? value : value * 1000;
|
|
38
37
|
}
|
|
39
38
|
if (typeof value === 'string' && value.trim()) {
|
|
40
39
|
const ms = new Date(value.trim()).getTime();
|
|
41
|
-
if (Number.isFinite(ms)) return
|
|
40
|
+
if (Number.isFinite(ms)) return ms;
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
43
|
return undefined;
|
|
45
44
|
}
|
|
46
45
|
|
|
46
|
+
function readChatMessageTimestampIso(message: ChatMessage | null | undefined): string | undefined {
|
|
47
|
+
const ms = readChatMessageTimestampMs(message);
|
|
48
|
+
return typeof ms === 'number' ? new Date(ms).toISOString() : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Turn-scoped variant of extractFinalSummaryFromMessages. Selects the last
|
|
53
|
+
* user-facing assistant/model bubble whose own timestamp is at/after the
|
|
54
|
+
* producing turn's start (`minTimestampMs`). This is the NOTIF Defect-B fix:
|
|
55
|
+
* a completion event's finalSummary must describe the turn THAT completed, not
|
|
56
|
+
* the prior task's last bubble. For native-source providers (claude-cli) the
|
|
57
|
+
* external transcript holds the ENTIRE session history filtered only by session
|
|
58
|
+
* start, so a completion debounce that fires before the producing turn's final
|
|
59
|
+
* assistant bubble has landed would otherwise echo the previous task's tail.
|
|
60
|
+
* A message whose timestamp predates the turn start is skipped; if no in-turn
|
|
61
|
+
* assistant bubble exists yet, returns '' (weak/empty) — never the stale tail.
|
|
62
|
+
*
|
|
63
|
+
* Mirrors the reconcile path's transcriptAfterDispatch guard (mesh-events-stale):
|
|
64
|
+
* a bubble only counts if its timestamp proves it was produced after the turn began.
|
|
65
|
+
* When `minTimestampMs` is undefined the behaviour is identical to the unscoped
|
|
66
|
+
* extractor (no turn boundary known → no filtering).
|
|
67
|
+
*/
|
|
68
|
+
export function extractFinalSummaryFromMessagesAfter(
|
|
69
|
+
messages: ChatMessage[] | null | undefined,
|
|
70
|
+
minTimestampMs: number | undefined,
|
|
71
|
+
maxChars: number = DEFAULT_FINAL_SUMMARY_MAX_CHARS,
|
|
72
|
+
): string {
|
|
73
|
+
if (!Array.isArray(messages) || messages.length === 0) return '';
|
|
74
|
+
const hasBoundary = typeof minTimestampMs === 'number' && Number.isFinite(minTimestampMs);
|
|
75
|
+
|
|
76
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
77
|
+
const msg = messages[i];
|
|
78
|
+
if (!msg) continue;
|
|
79
|
+
if (hasBoundary) {
|
|
80
|
+
// A bubble carrying a timestamp BEFORE the producing turn's start belongs to
|
|
81
|
+
// a prior task — skip it. A bubble with no parseable timestamp cannot be
|
|
82
|
+
// proven stale, so it is kept (the unscoped fallback) rather than dropped.
|
|
83
|
+
const ts = readChatMessageTimestampMs(msg);
|
|
84
|
+
if (typeof ts === 'number' && ts < (minTimestampMs as number)) continue;
|
|
85
|
+
}
|
|
86
|
+
const classification = classifyChatMessageVisibility(msg);
|
|
87
|
+
if (classification.isUserFacing && (msg.role === 'assistant' || msg.role === 'model')) {
|
|
88
|
+
const text = flattenContent(msg.content).trim();
|
|
89
|
+
if (text) return text.slice(0, maxChars);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
|
|
47
95
|
/**
|
|
48
96
|
* Like extractFinalSummaryFromMessages but also returns the ISO timestamp of the
|
|
49
97
|
* selected final assistant/model message. Completion reconciliation needs the
|
|
@@ -29,7 +29,7 @@ import { formatAutoApprovalMessage, pickApprovalButton, hasNegativeApprovalOptio
|
|
|
29
29
|
import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
|
|
30
30
|
import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
|
|
31
31
|
import { normalizeProviderSessionId } from './provider-session-id.js';
|
|
32
|
-
import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind, extractFinalSummaryFromMessages } from './chat-message-normalization.js';
|
|
32
|
+
import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind, extractFinalSummaryFromMessages, extractFinalSummaryFromMessagesAfter } from './chat-message-normalization.js';
|
|
33
33
|
import { workingDirBasename } from './working-dir.js';
|
|
34
34
|
import { ManualAttendanceTracker } from './manual-attendance.js';
|
|
35
35
|
|
|
@@ -68,6 +68,13 @@ type CompletedDebouncePending = {
|
|
|
68
68
|
// already have started its own turn and overwritten engine.currentTurnTaskId — so the
|
|
69
69
|
// id must be snapshotted here, not re-read at flush time.
|
|
70
70
|
taskId?: string;
|
|
71
|
+
// NOTIF Defect-B: the wall-clock start of the turn that produced this (debounced)
|
|
72
|
+
// completion, snapshotted SYNCHRONOUSLY at the generating→idle transition (same
|
|
73
|
+
// reason as taskId — a follow-up turn moves engine.currentTurnStartedAt). The
|
|
74
|
+
// completion's finalSummary is turn-scoped to bubbles at/after this instant so a
|
|
75
|
+
// debounce that flushes before the producing turn's final assistant bubble lands
|
|
76
|
+
// in the native transcript never echoes the PRIOR task's last bubble.
|
|
77
|
+
turnStartedAt?: number;
|
|
71
78
|
};
|
|
72
79
|
|
|
73
80
|
function isIdleStatus(value: unknown): boolean {
|
|
@@ -1404,7 +1411,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1404
1411
|
};
|
|
1405
1412
|
}
|
|
1406
1413
|
|
|
1407
|
-
private completionFinalSummary(parsedMessages: unknown): string | undefined {
|
|
1414
|
+
private completionFinalSummary(parsedMessages: unknown, turnStartedAt?: number): string | undefined {
|
|
1408
1415
|
// For native-source providers (claude-cli: chatMessagesOwnedExternally), the PTY
|
|
1409
1416
|
// screen parse is NOT the source of truth for the final summary — the terminal
|
|
1410
1417
|
// wraps/scrolls/clips text, so a screen-parsed assistant message is often a partial
|
|
@@ -1413,6 +1420,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1413
1420
|
// to the parsed screen only when the transcript is unavailable. This is the real cause
|
|
1414
1421
|
// of the truncated finalSummary — independent of cloud vs standalone (it surfaces on
|
|
1415
1422
|
// any short, fast-completing task where screen parse wins the race).
|
|
1423
|
+
//
|
|
1424
|
+
// NOTIF Defect-B: `turnStartedAt` (the producing turn's start, snapshotted on the
|
|
1425
|
+
// completedDebouncePending record) turn-scopes the NATIVE transcript read. The native
|
|
1426
|
+
// transcript holds the WHOLE session filtered only by session start, so a debounce that
|
|
1427
|
+
// flushes before the producing turn's final assistant bubble has landed would otherwise
|
|
1428
|
+
// return the PRIOR task's last bubble (event taskId=B but summary=A). Filtering to bubbles
|
|
1429
|
+
// at/after turnStartedAt yields '' in that race instead of the stale tail; the weak/empty
|
|
1430
|
+
// summary is later upgraded by the mesh reconcile loop once the real bubble is written.
|
|
1416
1431
|
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1417
1432
|
const parsedSummary = extractFinalSummaryFromMessages(
|
|
1418
1433
|
(this.completionHasFinalAssistantMessage(parsedMessages)
|
|
@@ -1421,11 +1436,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1421
1436
|
);
|
|
1422
1437
|
if (adapterOwnsMessagesElsewhere) {
|
|
1423
1438
|
const externalMessages = this.readExternalCompletionMessages();
|
|
1439
|
+
// Turn-scope the external transcript: never return a bubble produced before this
|
|
1440
|
+
// turn started. With no boundary known (turnStartedAt falsy) behaviour is unchanged.
|
|
1424
1441
|
const externalSummary = externalMessages
|
|
1425
|
-
?
|
|
1442
|
+
? extractFinalSummaryFromMessagesAfter(externalMessages as any, turnStartedAt)
|
|
1426
1443
|
: '';
|
|
1427
1444
|
// The transcript is authoritative for native-source providers. Use it unless it is
|
|
1428
|
-
// empty (not yet written) — only then fall back to
|
|
1445
|
+
// empty (not yet written, or no in-turn bubble) — only then fall back to the screen
|
|
1446
|
+
// parse, which reflects the LIVE screen (this turn's output), not the stale tail.
|
|
1429
1447
|
if (externalSummary) return externalSummary;
|
|
1430
1448
|
return parsedSummary || undefined;
|
|
1431
1449
|
}
|
|
@@ -1804,8 +1822,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1804
1822
|
// stuck on the dispatched user task. If the parser DID surface assistant text,
|
|
1805
1823
|
// prefer it; only fall back to '' when no assistant summary can be derived.
|
|
1806
1824
|
finalSummary: blockReason.startsWith('parsed_status:')
|
|
1807
|
-
? (this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages) ?? '')
|
|
1808
|
-
: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1825
|
+
? (this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt) ?? '')
|
|
1826
|
+
: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt),
|
|
1809
1827
|
completionDiagnostic,
|
|
1810
1828
|
});
|
|
1811
1829
|
this.completedDebouncePending = null;
|
|
@@ -1827,7 +1845,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1827
1845
|
timestamp: pending.timestamp,
|
|
1828
1846
|
// ARCH-REFACTOR R1: attribute to the turn captured at idle-transition.
|
|
1829
1847
|
...(pending.taskId ? { taskId: pending.taskId } : {}),
|
|
1830
|
-
finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1848
|
+
finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt),
|
|
1831
1849
|
});
|
|
1832
1850
|
this.completedDebouncePending = null;
|
|
1833
1851
|
this.completedDebounceTimer = null;
|
|
@@ -2037,7 +2055,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2037
2055
|
*/
|
|
2038
2056
|
private recheckAutoApproveSettled(): void {
|
|
2039
2057
|
try {
|
|
2040
|
-
|
|
2058
|
+
// APPROVAL Defect-C (auto-approve gap): re-probe with a LIVE parse, not the cached
|
|
2059
|
+
// engine snapshot. This timer is the ONLY re-drive when the PTY goes silent after the
|
|
2060
|
+
// approval prompt finishes painting (its whole reason to exist) — but with
|
|
2061
|
+
// allowParse:false it read only engine.activeModal, which the engine's per-frame settle
|
|
2062
|
+
// pass can leave null/stale when the modal arrived between writes. The re-check then saw
|
|
2063
|
+
// no modal and never fired, so the delegated worker's transient/quiet approval was missed
|
|
2064
|
+
// and the coordinator had to step in with a manual mesh_approve. allowParse:true makes
|
|
2065
|
+
// getStatus re-run runDetectStatus/runParseApproval on the current screen buffer (the same
|
|
2066
|
+
// live re-probe the coordinator's resolveAction path uses), recovering the modal so
|
|
2067
|
+
// auto-approve fires on its own. The half-rendered-frame guard (buttons.length===0) and
|
|
2068
|
+
// the settle gate in maybeAutoApproveStatus still protect against firing on a partial modal.
|
|
2069
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: true });
|
|
2041
2070
|
this.maybeAutoApproveStatus(adapterStatus, Date.now());
|
|
2042
2071
|
} catch { /* adapter gone / transient — next frame retries */ }
|
|
2043
2072
|
}
|
|
@@ -2380,6 +2409,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2380
2409
|
// before any follow-up task's flush can start a new turn and move
|
|
2381
2410
|
// engine.currentTurnTaskId.
|
|
2382
2411
|
...(this.completingTurnTaskId() ? { taskId: this.completingTurnTaskId() } : {}),
|
|
2412
|
+
// NOTIF Defect-B: snapshot the producing turn's START instant NOW, for the
|
|
2413
|
+
// same reason as taskId — a follow-up turn moves engine.currentTurnStartedAt.
|
|
2414
|
+
// Prefer the engine's per-turn start (set at onTurnStarted, earliest reliable
|
|
2415
|
+
// anchor) and fall back to generatingStartedAt (when generating was observed).
|
|
2416
|
+
...((() => {
|
|
2417
|
+
const engineTurnStart = typeof (this.adapter as any)?.currentTurnStartedAt === 'number'
|
|
2418
|
+
&& Number.isFinite((this.adapter as any).currentTurnStartedAt)
|
|
2419
|
+
? (this.adapter as any).currentTurnStartedAt as number
|
|
2420
|
+
: 0;
|
|
2421
|
+
const turnStartedAt = engineTurnStart || this.generatingStartedAt || 0;
|
|
2422
|
+
return turnStartedAt ? { turnStartedAt } : {};
|
|
2423
|
+
})()),
|
|
2383
2424
|
};
|
|
2384
2425
|
const ownsExternalHistory = !!(this.adapter as any)?.chatMessagesOwnedExternally;
|
|
2385
2426
|
// (FALSEIDLE-BGCHILD-a) Native-history providers flush immediately (the
|