@adhdev/daemon-core 0.9.82-rc.213 → 0.9.82-rc.215
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 +156 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +156 -4
- package/dist/index.mjs.map +1 -1
- package/dist/providers/spec/driver.d.ts +6 -0
- package/package.json +1 -1
- package/src/cli-adapters/cli-state-engine.ts +25 -1
- package/src/commands/router.ts +84 -0
- package/src/providers/native-history/codex-cli-transcript.ts +62 -2
- package/src/providers/spec/driver.ts +37 -1
|
@@ -146,6 +146,12 @@ export declare class SpecDriver {
|
|
|
146
146
|
private completionIdleKey;
|
|
147
147
|
/** Previous screen lines — passed to evaluate() for `changed` condition detection. */
|
|
148
148
|
private prevScreenLines;
|
|
149
|
+
/** Timestamp when idle was last committed (either direct or via idle_hold_ms).
|
|
150
|
+
* Used to suppress immediate idle → busy re-entry from transient `changed`
|
|
151
|
+
* condition blips (e.g. completion-marker counter "Completed for Xs" updating
|
|
152
|
+
* every second, which triggers cursor_above:changed and bounces back to busy
|
|
153
|
+
* right after an idle commit). */
|
|
154
|
+
private lastIdleCommittedAt;
|
|
149
155
|
/** Timestamp of the last PTY frame that changed the screen content.
|
|
150
156
|
* Used by screen_active_hold_ms to suppress idle downshifts while
|
|
151
157
|
* the terminal is still actively updating. */
|
package/package.json
CHANGED
|
@@ -555,11 +555,35 @@ export class CliStateEngine {
|
|
|
555
555
|
// The real completion gate is applyIdle's idleFinishCandidate +
|
|
556
556
|
// idleFinish timeout, which requires stable quiet AND a parsed
|
|
557
557
|
// assistant message.
|
|
558
|
+
//
|
|
559
|
+
// Fast-path exception: only release the hold if the parser shows a
|
|
560
|
+
// *current-turn* final standard assistant after the last user message,
|
|
561
|
+
// and it is not still streaming. Using !!lastParsedAssistant was too
|
|
562
|
+
// broad — it matched previous-turn assistant messages and caused
|
|
563
|
+
// false-idle between the first assistant text chunk and the first
|
|
564
|
+
// tool call (the agent outputs text, then immediately begins tool use;
|
|
565
|
+
// the gap between them hit this fast path and committed idle).
|
|
566
|
+
const hasFinalCurrentTurnAssistant = (() => {
|
|
567
|
+
if (parsedStatus !== 'idle') return false;
|
|
568
|
+
const msgs: any[] = Array.isArray(parsedMessages) ? parsedMessages : [];
|
|
569
|
+
let lastUserIdx = -1;
|
|
570
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
571
|
+
if (msgs[i]?.role === 'user') { lastUserIdx = i; break; }
|
|
572
|
+
}
|
|
573
|
+
// No user message visible: fall back to any non-streaming standard assistant.
|
|
574
|
+
const searchSlice = lastUserIdx >= 0 ? msgs.slice(lastUserIdx + 1) : msgs;
|
|
575
|
+
return searchSlice.some((m: any) => {
|
|
576
|
+
if (!m || m.role !== 'assistant') return false;
|
|
577
|
+
if (typeof m.content !== 'string' || !m.content.trim()) return false;
|
|
578
|
+
const kind = typeof m.kind === 'string' && m.kind.trim() ? m.kind.trim() : 'standard';
|
|
579
|
+
return kind === 'standard' && m.meta?.streaming !== true;
|
|
580
|
+
});
|
|
581
|
+
})();
|
|
558
582
|
const shouldHoldGenerating = status === 'idle'
|
|
559
583
|
&& this.isWaitingForResponse
|
|
560
584
|
&& !!this.currentTurnScope
|
|
561
585
|
&& !modal
|
|
562
|
-
&& !
|
|
586
|
+
&& !hasFinalCurrentTurnAssistant;
|
|
563
587
|
|
|
564
588
|
if (shouldHoldGenerating) { this.applyHoldGenerating(ctx); return; }
|
|
565
589
|
if (status === 'error') {
|
package/src/commands/router.ts
CHANGED
|
@@ -66,6 +66,7 @@ import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
|
66
66
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
67
67
|
import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
|
|
68
68
|
import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
69
|
+
import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
|
|
69
70
|
import { homedir, hostname as osHostname } from 'os';
|
|
70
71
|
import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
|
|
71
72
|
import * as fs from 'fs';
|
|
@@ -3333,6 +3334,7 @@ export class DaemonCommandRouter {
|
|
|
3333
3334
|
success: result.success === true,
|
|
3334
3335
|
result,
|
|
3335
3336
|
finalBranchConvergenceState: result.finalBranchConvergenceState,
|
|
3337
|
+
...(result.blockerContext ? { blockerContext: result.blockerContext } : {}),
|
|
3336
3338
|
} : {}),
|
|
3337
3339
|
},
|
|
3338
3340
|
});
|
|
@@ -3904,6 +3906,30 @@ export class DaemonCommandRouter {
|
|
|
3904
3906
|
};
|
|
3905
3907
|
}
|
|
3906
3908
|
|
|
3909
|
+
// Push logic: after a successful merge, either auto-push or surface push info
|
|
3910
|
+
// so coordinators don't need manual discovery after each refine.
|
|
3911
|
+
const requireApprovalForPush: boolean = (mesh as any)?.policy?.requireApprovalForPush ?? DEFAULT_MESH_POLICY.requireApprovalForPush;
|
|
3912
|
+
let pushResult: Record<string, unknown> | undefined;
|
|
3913
|
+
if (!requireApprovalForPush) {
|
|
3914
|
+
const pushStarted = Date.now();
|
|
3915
|
+
try {
|
|
3916
|
+
await execFileAsync('git', ['push', 'origin', baseBranch], { cwd: repoRoot, encoding: 'utf8' });
|
|
3917
|
+
pushResult = { pushed: true, remote: 'origin', branch: baseBranch, durationMs: Date.now() - pushStarted };
|
|
3918
|
+
recordMeshRefineStage(refineStages, 'push', 'passed', pushStarted, pushResult);
|
|
3919
|
+
finalBranchConvergenceState.status = 'merged_pushed';
|
|
3920
|
+
} catch (e: any) {
|
|
3921
|
+
pushResult = {
|
|
3922
|
+
pushed: false,
|
|
3923
|
+
remote: 'origin',
|
|
3924
|
+
branch: baseBranch,
|
|
3925
|
+
error: e?.message || String(e),
|
|
3926
|
+
stderr: e?.stderr,
|
|
3927
|
+
durationMs: Date.now() - pushStarted,
|
|
3928
|
+
};
|
|
3929
|
+
recordMeshRefineStage(refineStages, 'push', 'failed', pushStarted, pushResult);
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3907
3933
|
return {
|
|
3908
3934
|
success: true,
|
|
3909
3935
|
merged: true,
|
|
@@ -3918,6 +3944,14 @@ export class DaemonCommandRouter {
|
|
|
3918
3944
|
refineStages,
|
|
3919
3945
|
...(ledgerError ? { ledgerError } : {}),
|
|
3920
3946
|
finalBranchConvergenceState,
|
|
3947
|
+
// Push outcome or readiness info for coordinator.
|
|
3948
|
+
...(pushResult
|
|
3949
|
+
? { pushResult }
|
|
3950
|
+
: {
|
|
3951
|
+
pushReady: true,
|
|
3952
|
+
pushCommand: `git push origin ${baseBranch}`,
|
|
3953
|
+
pushNote: 'requireApprovalForPush is enabled — run the push command or obtain user approval before pushing.',
|
|
3954
|
+
}),
|
|
3921
3955
|
};
|
|
3922
3956
|
} catch (e: any) {
|
|
3923
3957
|
return { success: false, error: e.message, refineStages };
|
|
@@ -3952,9 +3986,59 @@ export class DaemonCommandRouter {
|
|
|
3952
3986
|
? 'cleanup_failed'
|
|
3953
3987
|
: 'merge_failed'; // fallback for unclassified failures
|
|
3954
3988
|
const isTerminalSuccess = refineTerminalKind === 'completed';
|
|
3989
|
+
|
|
3990
|
+
// Build structured blocker context for task_failed ledger entries so coordinators
|
|
3991
|
+
// can inspect the failure cause without parsing free-form error strings.
|
|
3992
|
+
const blockerContext: Record<string, unknown> | undefined = isTerminalSuccess ? undefined : (() => {
|
|
3993
|
+
const code = typeof result.code === 'string' ? result.code : refineTerminalKind;
|
|
3994
|
+
const stage = refineTerminalKind === 'validation_failed' ? 'validation'
|
|
3995
|
+
: refineTerminalKind === 'submodule_reachability_failed' ? 'submodule_reachability'
|
|
3996
|
+
: refineCode === 'patch_equivalence_failed' ? 'patch_equivalence'
|
|
3997
|
+
: refineCode === 'needs_rebase' || refineCode === 'needs_rebase_with_conflicts' ? 'patch_equivalence'
|
|
3998
|
+
: refineTerminalKind === 'merge_failed' ? 'merge'
|
|
3999
|
+
: refineTerminalKind === 'cleanup_failed' ? 'cleanup'
|
|
4000
|
+
: 'unknown';
|
|
4001
|
+
const ctx: Record<string, unknown> = {
|
|
4002
|
+
stage,
|
|
4003
|
+
reason: code,
|
|
4004
|
+
terminalKind: refineTerminalKind,
|
|
4005
|
+
};
|
|
4006
|
+
if (typeof result.error === 'string') ctx.error = result.error;
|
|
4007
|
+
if (typeof result.blockedReason === 'string') ctx.blockedReason = result.blockedReason;
|
|
4008
|
+
// Patch equivalence details
|
|
4009
|
+
if (stage === 'patch_equivalence' && result.patchEquivalence) {
|
|
4010
|
+
const pe = result.patchEquivalence as Record<string, unknown>;
|
|
4011
|
+
ctx.details = {
|
|
4012
|
+
expectedPatchId: pe.expectedPatchId,
|
|
4013
|
+
actualPatchId: pe.actualPatchId,
|
|
4014
|
+
status: pe.status,
|
|
4015
|
+
actionableHint: pe.actionableHint,
|
|
4016
|
+
error: pe.error,
|
|
4017
|
+
};
|
|
4018
|
+
}
|
|
4019
|
+
// Submodule reachability details
|
|
4020
|
+
if (stage === 'submodule_reachability' && Array.isArray(result.unreachableSubmoduleCommits)) {
|
|
4021
|
+
ctx.details = {
|
|
4022
|
+
unreachableCount: (result.unreachableSubmoduleCommits as unknown[]).length,
|
|
4023
|
+
paths: (result.unreachableSubmoduleCommits as Array<Record<string, unknown>>).map(e => e.path),
|
|
4024
|
+
autoPublishAllowed: (result.unreachableSubmoduleCommits as Array<Record<string, unknown>>)[0]?.autoPublishAllowed,
|
|
4025
|
+
};
|
|
4026
|
+
}
|
|
4027
|
+
// Validation details
|
|
4028
|
+
if (stage === 'validation' && result.validationSummary) {
|
|
4029
|
+
const vs = result.validationSummary as Record<string, unknown>;
|
|
4030
|
+
ctx.details = {
|
|
4031
|
+
failureCode: vs.failureCode,
|
|
4032
|
+
commandsRun: Array.isArray(vs.commandsRun) ? vs.commandsRun.length : undefined,
|
|
4033
|
+
};
|
|
4034
|
+
}
|
|
4035
|
+
return ctx;
|
|
4036
|
+
})();
|
|
4037
|
+
|
|
3955
4038
|
const normalizedResult = {
|
|
3956
4039
|
...result,
|
|
3957
4040
|
terminalKind: refineTerminalKind,
|
|
4041
|
+
...(blockerContext ? { blockerContext } : {}),
|
|
3958
4042
|
...(result.nextStep === undefined && !isTerminalSuccess ? {
|
|
3959
4043
|
nextStep: refineTerminalKind === 'blocked_review'
|
|
3960
4044
|
? 'Request user review/approval before attempting to merge again.'
|
|
@@ -159,6 +159,44 @@ function extractToolOutputContent(payload: Record<string, unknown>): string {
|
|
|
159
159
|
return '';
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
function hasAssistantStandardMessageSinceLastUser(records: NativeHistoryMessage[], content: string): boolean {
|
|
163
|
+
const normalized = content.trim();
|
|
164
|
+
if (!normalized) return false;
|
|
165
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
166
|
+
const record = records[i];
|
|
167
|
+
if (record.kind === 'session_start') continue;
|
|
168
|
+
if (record.role === 'user') return false;
|
|
169
|
+
if (record.role === 'assistant' && record.kind === 'standard' && record.content.trim() === normalized) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function pushAssistantStandardMessage(
|
|
177
|
+
records: NativeHistoryMessage[],
|
|
178
|
+
sessionId: string,
|
|
179
|
+
receivedAt: number,
|
|
180
|
+
content: string,
|
|
181
|
+
workspace?: string,
|
|
182
|
+
): void {
|
|
183
|
+
const text = content.trim();
|
|
184
|
+
if (!text) return;
|
|
185
|
+
if (hasAssistantStandardMessageSinceLastUser(records, text)) return;
|
|
186
|
+
|
|
187
|
+
const msg: NativeHistoryMessage = {
|
|
188
|
+
ts: new Date(receivedAt).toISOString(),
|
|
189
|
+
receivedAt,
|
|
190
|
+
role: 'assistant',
|
|
191
|
+
content: text,
|
|
192
|
+
kind: 'standard',
|
|
193
|
+
agent: 'codex-cli',
|
|
194
|
+
historySessionId: sessionId,
|
|
195
|
+
};
|
|
196
|
+
if (workspace) msg.workspace = workspace;
|
|
197
|
+
records.push(msg);
|
|
198
|
+
}
|
|
199
|
+
|
|
162
200
|
/**
|
|
163
201
|
* Read the first line of a Codex JSONL session file and parse the session_meta record.
|
|
164
202
|
* Returns the payload object (containing id, cwd, etc.) or null.
|
|
@@ -233,16 +271,38 @@ function parseSessionFile(
|
|
|
233
271
|
continue;
|
|
234
272
|
}
|
|
235
273
|
|
|
236
|
-
if (type !== 'response_item') continue;
|
|
237
|
-
|
|
238
274
|
const payloadType = String(payload.type ?? '').trim();
|
|
239
275
|
|
|
276
|
+
if (type === 'event_msg') {
|
|
277
|
+
if (payloadType === 'task_complete') {
|
|
278
|
+
pushAssistantStandardMessage(
|
|
279
|
+
records,
|
|
280
|
+
sessionId,
|
|
281
|
+
receivedAt,
|
|
282
|
+
flattenCodexContent(payload.last_agent_message),
|
|
283
|
+
detectedWorkspace,
|
|
284
|
+
);
|
|
285
|
+
} else if (payloadType === 'agent_message' && String(payload.phase ?? '').trim() === 'final_answer') {
|
|
286
|
+
pushAssistantStandardMessage(
|
|
287
|
+
records,
|
|
288
|
+
sessionId,
|
|
289
|
+
receivedAt,
|
|
290
|
+
flattenCodexContent(payload.message),
|
|
291
|
+
detectedWorkspace,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (type !== 'response_item') continue;
|
|
298
|
+
|
|
240
299
|
if (payloadType === 'message') {
|
|
241
300
|
const role = String(payload.role ?? '').trim();
|
|
242
301
|
if (role !== 'user' && role !== 'assistant') continue;
|
|
243
302
|
|
|
244
303
|
const content = flattenCodexContent(payload.content);
|
|
245
304
|
if (!content) continue;
|
|
305
|
+
if (role === 'assistant' && hasAssistantStandardMessageSinceLastUser(records, content)) continue;
|
|
246
306
|
|
|
247
307
|
const msg: NativeHistoryMessage = {
|
|
248
308
|
ts: new Date(receivedAt).toISOString(),
|
|
@@ -230,6 +230,12 @@ export class SpecDriver {
|
|
|
230
230
|
private completionIdleKey = '';
|
|
231
231
|
/** Previous screen lines — passed to evaluate() for `changed` condition detection. */
|
|
232
232
|
private prevScreenLines: string[] = [];
|
|
233
|
+
/** Timestamp when idle was last committed (either direct or via idle_hold_ms).
|
|
234
|
+
* Used to suppress immediate idle → busy re-entry from transient `changed`
|
|
235
|
+
* condition blips (e.g. completion-marker counter "Completed for Xs" updating
|
|
236
|
+
* every second, which triggers cursor_above:changed and bounces back to busy
|
|
237
|
+
* right after an idle commit). */
|
|
238
|
+
private lastIdleCommittedAt = 0;
|
|
233
239
|
/** Timestamp of the last PTY frame that changed the screen content.
|
|
234
240
|
* Used by screen_active_hold_ms to suppress idle downshifts while
|
|
235
241
|
* the terminal is still actively updating. */
|
|
@@ -303,7 +309,7 @@ export class SpecDriver {
|
|
|
303
309
|
case 'click_modal_button': this.handleClickModalButton(cmd.index); return;
|
|
304
310
|
case 'attach_image': this.handleAttachImage(cmd.blob, cmd.mime); return;
|
|
305
311
|
case 'resize': this.adapter.resize(cmd.cols, cmd.rows); return;
|
|
306
|
-
case 'cancel': this.adapter.send_keys('\x03'); return;
|
|
312
|
+
case 'cancel': this.lastIdleCommittedAt = 0; this.adapter.send_keys('\x03'); return;
|
|
307
313
|
case 'shutdown': this.shutdown(); return;
|
|
308
314
|
}
|
|
309
315
|
}
|
|
@@ -595,6 +601,29 @@ export class SpecDriver {
|
|
|
595
601
|
evState = this.lastBusyState ?? evState;
|
|
596
602
|
}
|
|
597
603
|
|
|
604
|
+
// Idle → busy re-entry hold: suppress false-busy transitions that fire
|
|
605
|
+
// immediately after an idle commit. The most common source is the
|
|
606
|
+
// `cursor_above: 3, changed: true` condition matching tiny updates
|
|
607
|
+
// (e.g. "✻ Completed for Ns" counter ticking once per second) right
|
|
608
|
+
// after the completion hold expired and idle was committed. Without
|
|
609
|
+
// this guard the dashboard bounces back to generating within 1s of
|
|
610
|
+
// seeing idle.
|
|
611
|
+
//
|
|
612
|
+
// Guard window: prefer screen_active_hold_ms (already the "micro-change
|
|
613
|
+
// suppressor" window), floor to 1500ms so specs that omit it still
|
|
614
|
+
// get protection. This is intentionally shorter than busy_hold_ms —
|
|
615
|
+
// a real new generation (user types a message) will produce many
|
|
616
|
+
// more decisive signals (spinner, token counter, esc-to-interrupt)
|
|
617
|
+
// that override the hold long before it expires.
|
|
618
|
+
const idleReentryHoldMs = Math.max(screenActiveMs, 1500);
|
|
619
|
+
if (evState.id === 'busy' && this.currentStateId === (this.spec.default_state ?? 'idle')
|
|
620
|
+
&& this.lastIdleCommittedAt > 0
|
|
621
|
+
&& (now - this.lastIdleCommittedAt) < idleReentryHoldMs) {
|
|
622
|
+
LOG.debug('SpecDriver', `[${this.opts.specPath.split('/').slice(-3).join('/')}] idle→busy suppressed (idle_reentry_hold ageMs=${now - this.lastIdleCommittedAt} holdMs=${idleReentryHoldMs})`);
|
|
623
|
+
this.scheduleBusyExpiry(idleReentryHoldMs - (now - this.lastIdleCommittedAt) + 50);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
598
627
|
if (evState.id === 'busy') {
|
|
599
628
|
this.lastBusyAt = Date.now();
|
|
600
629
|
this.lastBusyState = evState;
|
|
@@ -661,6 +690,7 @@ export class SpecDriver {
|
|
|
661
690
|
this.pendingIdleState = null;
|
|
662
691
|
if (!committed) return;
|
|
663
692
|
LOG.debug('SpecDriver', `[${this.opts.specPath.split('/').slice(-3).join('/')}] idleHold committed after ${idleHoldMs}ms`);
|
|
693
|
+
this.lastIdleCommittedAt = Date.now();
|
|
664
694
|
this.currentStateId = committed.id;
|
|
665
695
|
this.currentEval = capturedEv;
|
|
666
696
|
this.pushHistory(committed.id, committed.label, {
|
|
@@ -721,6 +751,9 @@ export class SpecDriver {
|
|
|
721
751
|
}
|
|
722
752
|
}
|
|
723
753
|
if (changed) {
|
|
754
|
+
if (evState.id === (this.spec.default_state ?? 'idle')) {
|
|
755
|
+
this.lastIdleCommittedAt = Date.now();
|
|
756
|
+
}
|
|
724
757
|
this.currentStateId = evState.id;
|
|
725
758
|
const matchedRules = extractMatchedRules(ev);
|
|
726
759
|
// Determine reason and debounce kind for this transition
|
|
@@ -799,6 +832,9 @@ export class SpecDriver {
|
|
|
799
832
|
this.pendingSends.push(text);
|
|
800
833
|
return;
|
|
801
834
|
}
|
|
835
|
+
// Clear the idle re-entry hold so a new generation starting
|
|
836
|
+
// immediately after idle doesn't get its busy state suppressed.
|
|
837
|
+
this.lastIdleCommittedAt = 0;
|
|
802
838
|
this.actuallySendMessage(text);
|
|
803
839
|
}
|
|
804
840
|
|