@adhdev/daemon-core 0.9.82-rc.213 → 0.9.82-rc.214

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.213",
3
+ "version": "0.9.82-rc.214",
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",
@@ -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
- && !(parsedStatus === 'idle' && !!lastParsedAssistant);
586
+ && !hasFinalCurrentTurnAssistant;
563
587
 
564
588
  if (shouldHoldGenerating) { this.applyHoldGenerating(ctx); return; }
565
589
  if (status === 'error') {
@@ -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(),