@adhdev/daemon-core 0.9.82-rc.445 → 0.9.82-rc.447
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/config/mesh-config.d.ts +9 -0
- package/dist/git/git-commands.d.ts +1 -0
- package/dist/index.js +384 -287
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +388 -291
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-node-identity.d.ts +2 -0
- package/dist/repo-mesh-types.d.ts +10 -0
- package/package.json +2 -2
- package/src/commands/high-family/mesh-status.ts +15 -0
- package/src/config/mesh-config.ts +26 -1
- package/src/git/git-commands.ts +21 -2
- package/src/mesh/mesh-node-identity.ts +16 -6
- package/src/providers/cli-provider-instance.ts +78 -16
- package/src/providers/native-history/antigravity-cli-transcript.ts +91 -33
- package/src/repo-mesh-types.ts +10 -0
|
@@ -48,6 +48,7 @@ export declare function buildLivePeerGitConnection(connection: Record<string, un
|
|
|
48
48
|
export declare function recordInlineMeshDirectGitTruth(node: any, git: Record<string, unknown>, source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git'): {
|
|
49
49
|
reporterPlatform: string | null;
|
|
50
50
|
reporterArch: string | null;
|
|
51
|
+
reporterMachineNickname: string | null;
|
|
51
52
|
};
|
|
52
53
|
/**
|
|
53
54
|
* Persist the live self-reported platform/arch onto the local meshes.json node
|
|
@@ -63,6 +64,7 @@ export declare function recordInlineMeshDirectGitTruth(node: any, git: Record<st
|
|
|
63
64
|
export declare function persistNodeReporterPlatform(meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config', mesh: any, nodeId: string | undefined, reporter: {
|
|
64
65
|
reporterPlatform: string | null;
|
|
65
66
|
reporterArch: string | null;
|
|
67
|
+
reporterMachineNickname?: string | null;
|
|
66
68
|
}): void;
|
|
67
69
|
export declare function inlineMeshCarriesTransientNodeTruth(inlineMesh: any): boolean;
|
|
68
70
|
export declare function readInlineMeshNodeId(node: any): string;
|
|
@@ -580,6 +580,16 @@ export interface LocalMeshNodeEntry {
|
|
|
580
580
|
*/
|
|
581
581
|
reportedPlatform?: string;
|
|
582
582
|
reportedArch?: string;
|
|
583
|
+
/**
|
|
584
|
+
* The operator-set machine nickname (config.machineNickname) of the daemon
|
|
585
|
+
* that owns this node's workspace. The local coordinator stamps its own
|
|
586
|
+
* config value onto its self/base node; a remote member self-reports its
|
|
587
|
+
* value on the git_status envelope (reporterMachineNickname), which the
|
|
588
|
+
* coordinator persists here on each direct git probe. Feeds
|
|
589
|
+
* buildMeshNodeDisplayLabel so the mesh UI renders the friendly nickname
|
|
590
|
+
* instead of a raw daemonId/nodeId. Absent until set/first-reported.
|
|
591
|
+
*/
|
|
592
|
+
machineNickname?: string;
|
|
583
593
|
policy: RepoMeshNodePolicy;
|
|
584
594
|
/**
|
|
585
595
|
* Per-node instruction surfaced in the coordinator prompt so the LLM
|
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.447",
|
|
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.447",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -180,6 +180,13 @@ export const meshStatusHandlers: Record<string, HighFamilyHandler> = {
|
|
|
180
180
|
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
181
181
|
|
|
182
182
|
const localMachineId = loadConfig().machineId || '';
|
|
183
|
+
// Local daemon's operator-set nickname — stamped onto the self/base
|
|
184
|
+
// node at render time so the friendly label resolves even before the
|
|
185
|
+
// persisted node record (addNode) has been rewritten with it.
|
|
186
|
+
const localMachineNickname = (() => {
|
|
187
|
+
const nick = loadConfig().machineNickname;
|
|
188
|
+
return typeof nick === 'string' && nick.trim() ? nick.trim() : '';
|
|
189
|
+
})();
|
|
183
190
|
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
184
191
|
// Shared probe gate for this mesh_status call: the bootstrap
|
|
185
192
|
// hydrate below and the per-node render loop further down both
|
|
@@ -302,6 +309,14 @@ export const meshStatusHandlers: Record<string, HighFamilyHandler> = {
|
|
|
302
309
|
daemonId && (daemonIdsEquivalent(daemonId, localMachineId) || daemonIdsEquivalent(daemonId, ctx.deps.statusInstanceId)),
|
|
303
310
|
) || Boolean(meshRecord?.inline && nodeIndex === 0)
|
|
304
311
|
|| sparseConfiguredCoordinatorNode;
|
|
312
|
+
// The self/base node's friendly label comes from THIS daemon's
|
|
313
|
+
// local config.machineNickname. Stamp it onto the node record (if
|
|
314
|
+
// not already carried) so buildMeshNodeDisplayLabel below resolves
|
|
315
|
+
// to the nickname and labelSource reads 'explicit_metadata' even
|
|
316
|
+
// before addNode's persisted record has been rewritten.
|
|
317
|
+
if (isSelfNode && localMachineNickname && !readStringValue(node.machineNickname, node.machine_nickname)) {
|
|
318
|
+
node.machineNickname = localMachineNickname;
|
|
319
|
+
}
|
|
305
320
|
const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
|
|
306
321
|
localMachineId,
|
|
307
322
|
localDaemonId: ctx.deps.statusInstanceId,
|
|
@@ -10,7 +10,7 @@ import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
|
10
10
|
import { join } from 'path';
|
|
11
11
|
import { randomBytes, randomUUID } from 'crypto';
|
|
12
12
|
import { shortHash } from '../system/hash.js';
|
|
13
|
-
import { getConfigDir } from './config.js';
|
|
13
|
+
import { getConfigDir, loadConfig } from './config.js';
|
|
14
14
|
import type {
|
|
15
15
|
LocalMeshConfig,
|
|
16
16
|
LocalMeshEntry,
|
|
@@ -492,6 +492,11 @@ export interface AddNodeOptions {
|
|
|
492
492
|
clonedFromNodeId?: string;
|
|
493
493
|
worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'];
|
|
494
494
|
role?: RepoMeshDaemonRole;
|
|
495
|
+
/** Owning daemon's machine nickname. Defaults to this daemon's local
|
|
496
|
+
* config.machineNickname when omitted — a node is always added by (and on)
|
|
497
|
+
* the daemon that owns its workspace (self/base node or a local worktree
|
|
498
|
+
* clone), so the local config is the correct source. */
|
|
499
|
+
machineNickname?: string;
|
|
495
500
|
}
|
|
496
501
|
|
|
497
502
|
export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntry | undefined {
|
|
@@ -508,12 +513,27 @@ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntr
|
|
|
508
513
|
throw new Error('This workspace is already in the mesh');
|
|
509
514
|
}
|
|
510
515
|
|
|
516
|
+
// A node is always added by the daemon that owns its workspace (the self/base
|
|
517
|
+
// node, or a local worktree clone spawned from this daemon), so this daemon's
|
|
518
|
+
// config.machineNickname is the correct owner nickname. Explicit opt wins.
|
|
519
|
+
const machineNickname = (() => {
|
|
520
|
+
const explicit = typeof opts.machineNickname === 'string' ? opts.machineNickname.trim() : '';
|
|
521
|
+
if (explicit) return explicit;
|
|
522
|
+
try {
|
|
523
|
+
const local = loadConfig().machineNickname;
|
|
524
|
+
return typeof local === 'string' && local.trim() ? local.trim() : undefined;
|
|
525
|
+
} catch {
|
|
526
|
+
return undefined;
|
|
527
|
+
}
|
|
528
|
+
})();
|
|
529
|
+
|
|
511
530
|
const node: LocalMeshNodeEntry = {
|
|
512
531
|
id: `node_${randomUUID().replace(/-/g, '')}`,
|
|
513
532
|
workspace: opts.workspace.trim(),
|
|
514
533
|
repoRoot: opts.repoRoot,
|
|
515
534
|
daemonId: opts.daemonId,
|
|
516
535
|
machineId: opts.machineId,
|
|
536
|
+
...(machineNickname ? { machineNickname } : {}),
|
|
517
537
|
capabilities: normalizeCapabilityTags(opts.capabilities),
|
|
518
538
|
userOverrides: opts.userOverrides || {},
|
|
519
539
|
policy: opts.policy || {},
|
|
@@ -559,6 +579,10 @@ export function updateNode(
|
|
|
559
579
|
* operator intent) so capability-tag os=/arch= self-heals across loads. */
|
|
560
580
|
reportedPlatform?: string;
|
|
561
581
|
reportedArch?: string;
|
|
582
|
+
/** Owning daemon's self-reported machine nickname, carried on the
|
|
583
|
+
* git_status envelope. Persisted so the friendly label survives across
|
|
584
|
+
* coordinator restarts (mirrors reportedPlatform/reportedArch). */
|
|
585
|
+
reportedMachineNickname?: string;
|
|
562
586
|
},
|
|
563
587
|
): LocalMeshNodeEntry | undefined {
|
|
564
588
|
const config = loadMeshConfig();
|
|
@@ -571,6 +595,7 @@ export function updateNode(
|
|
|
571
595
|
if (opts.userOverrides) node.userOverrides = { ...node.userOverrides, ...opts.userOverrides };
|
|
572
596
|
if (opts.reportedPlatform && opts.reportedPlatform.trim()) node.reportedPlatform = opts.reportedPlatform.trim();
|
|
573
597
|
if (opts.reportedArch && opts.reportedArch.trim()) node.reportedArch = opts.reportedArch.trim();
|
|
598
|
+
if (opts.reportedMachineNickname && opts.reportedMachineNickname.trim()) node.machineNickname = opts.reportedMachineNickname.trim();
|
|
574
599
|
if (opts.policy) node.policy = { ...node.policy, ...opts.policy };
|
|
575
600
|
if (opts.worktreeBootstrap) node.worktreeBootstrap = opts.worktreeBootstrap;
|
|
576
601
|
if (Object.prototype.hasOwnProperty.call(opts, 'systemPrompt')) {
|
package/src/git/git-commands.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { getGitDiffSummary, getGitFileDiff } from './git-diff.js';
|
|
|
4
4
|
import { GitCommandError, isPathInside, resolveGitRepository, runGit } from './git-executor.js';
|
|
5
5
|
import { createGitSnapshotStore } from './git-snapshot-store.js';
|
|
6
6
|
import { getGitRepoStatus } from './git-status.js';
|
|
7
|
+
import { loadConfig } from '../config/config.js';
|
|
7
8
|
import type {
|
|
8
9
|
GitCommandName,
|
|
9
10
|
GitDiffSummary,
|
|
@@ -113,7 +114,9 @@ type GitCommandSuccess =
|
|
|
113
114
|
// reporterPlatform/reporterArch carry the responding daemon's process.platform/
|
|
114
115
|
// process.arch so a mesh coordinator probing this node over P2P can self-heal the
|
|
115
116
|
// node's userOverrides.platform/arch (the fields capability-tag routing reads).
|
|
116
|
-
|
|
117
|
+
// reporterMachineNickname carries the responding daemon's config.machineNickname
|
|
118
|
+
// so the coordinator can populate node.machineNickname → the friendly display label.
|
|
119
|
+
| { success: true; status: GitRepoStatus; reporterPlatform?: string; reporterArch?: string; reporterMachineNickname?: string }
|
|
117
120
|
| { success: true; diffSummary: GitDiffSummary }
|
|
118
121
|
| { success: true; diff: GitFileDiff }
|
|
119
122
|
| { success: true; snapshot: GitSnapshot }
|
|
@@ -315,7 +318,23 @@ export async function handleGitCommand(
|
|
|
315
318
|
// stamps it onto the node's userOverrides.platform/arch (the fields
|
|
316
319
|
// buildMeshNodeCapabilityTags reads). These are siblings of `status`, not
|
|
317
320
|
// part of GitRepoStatus, so the git payload shape is untouched.
|
|
318
|
-
|
|
321
|
+
// The machine nickname rides the same channel so the coordinator can render
|
|
322
|
+
// this node's friendly label instead of a raw daemonId/nodeId.
|
|
323
|
+
const reporterMachineNickname = (() => {
|
|
324
|
+
try {
|
|
325
|
+
const nick = loadConfig().machineNickname;
|
|
326
|
+
return typeof nick === 'string' && nick.trim() ? nick.trim() : undefined;
|
|
327
|
+
} catch {
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
})();
|
|
331
|
+
return {
|
|
332
|
+
success: true,
|
|
333
|
+
status,
|
|
334
|
+
reporterPlatform: process.platform,
|
|
335
|
+
reporterArch: process.arch,
|
|
336
|
+
...(reporterMachineNickname ? { reporterMachineNickname } : {}),
|
|
337
|
+
};
|
|
319
338
|
}
|
|
320
339
|
|
|
321
340
|
case 'git_diff_summary': {
|
|
@@ -319,9 +319,9 @@ export function recordInlineMeshDirectGitTruth(
|
|
|
319
319
|
node: any,
|
|
320
320
|
git: Record<string, unknown>,
|
|
321
321
|
source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
|
|
322
|
-
): { reporterPlatform: string | null; reporterArch: string | null } {
|
|
322
|
+
): { reporterPlatform: string | null; reporterArch: string | null; reporterMachineNickname: string | null } {
|
|
323
323
|
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
|
324
|
-
return { reporterPlatform: null, reporterArch: null };
|
|
324
|
+
return { reporterPlatform: null, reporterArch: null, reporterMachineNickname: null };
|
|
325
325
|
}
|
|
326
326
|
const checkedAt = readNumberValue(git.lastCheckedAt) ?? Date.now();
|
|
327
327
|
const updatedAt = new Date(checkedAt).toISOString();
|
|
@@ -359,7 +359,14 @@ export function recordInlineMeshDirectGitTruth(
|
|
|
359
359
|
// for an inline/cache mesh this keeps the runtime object self-consistent.
|
|
360
360
|
if (reporterPlatform) node.reportedPlatform = reporterPlatform;
|
|
361
361
|
if (reporterArch) node.reportedArch = reporterArch;
|
|
362
|
-
|
|
362
|
+
// Machine nickname: only a remote member self-reports it (reporterMachineNickname
|
|
363
|
+
// rides the git_status envelope). For a local_source probe the workspace lives on
|
|
364
|
+
// THIS machine, but the self/base node already carries the local config nickname
|
|
365
|
+
// (addNode stamps it), so we only stamp from an explicit report here — never
|
|
366
|
+
// overwrite an existing nickname with an empty value.
|
|
367
|
+
const reporterMachineNickname = readStringValue(git.reporterMachineNickname) ?? null;
|
|
368
|
+
if (reporterMachineNickname) node.machineNickname = reporterMachineNickname;
|
|
369
|
+
return { reporterPlatform, reporterArch, reporterMachineNickname };
|
|
363
370
|
}
|
|
364
371
|
|
|
365
372
|
/**
|
|
@@ -396,16 +403,17 @@ export function persistNodeReporterPlatform(
|
|
|
396
403
|
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config',
|
|
397
404
|
mesh: any,
|
|
398
405
|
nodeId: string | undefined,
|
|
399
|
-
reporter: { reporterPlatform: string | null; reporterArch: string | null },
|
|
406
|
+
reporter: { reporterPlatform: string | null; reporterArch: string | null; reporterMachineNickname?: string | null },
|
|
400
407
|
): void {
|
|
401
408
|
if (meshSource !== 'local_config') return;
|
|
402
409
|
const meshId = readStringValue(mesh?.id);
|
|
403
410
|
if (!meshId || !nodeId) return;
|
|
404
411
|
const reportedPlatform = reporter.reporterPlatform ?? undefined;
|
|
405
412
|
const reportedArch = reporter.reporterArch ?? undefined;
|
|
406
|
-
|
|
413
|
+
const reportedMachineNickname = reporter.reporterMachineNickname ?? undefined;
|
|
414
|
+
if (!reportedPlatform && !reportedArch && !reportedMachineNickname) return;
|
|
407
415
|
void import('../config/mesh-config.js')
|
|
408
|
-
.then(({ updateNode }) => updateNode(meshId, nodeId, { reportedPlatform, reportedArch }))
|
|
416
|
+
.then(({ updateNode }) => updateNode(meshId, nodeId, { reportedPlatform, reportedArch, reportedMachineNickname }))
|
|
409
417
|
.catch(() => { /* best-effort self-heal; never block status assembly */ });
|
|
410
418
|
}
|
|
411
419
|
|
|
@@ -1409,9 +1417,11 @@ async function probeRemoteMeshGitStatus(args: {
|
|
|
1409
1417
|
// persist them to node.userOverrides without touching the git status shape.
|
|
1410
1418
|
const reporterPlatform = readStringValue(remoteResult?.reporterPlatform);
|
|
1411
1419
|
const reporterArch = readStringValue(remoteResult?.reporterArch);
|
|
1420
|
+
const reporterMachineNickname = readStringValue(remoteResult?.reporterMachineNickname);
|
|
1412
1421
|
const git = remoteGit as Record<string, unknown>;
|
|
1413
1422
|
if (reporterPlatform) git.reporterPlatform = reporterPlatform;
|
|
1414
1423
|
if (reporterArch) git.reporterArch = reporterArch;
|
|
1424
|
+
if (reporterMachineNickname) git.reporterMachineNickname = reporterMachineNickname;
|
|
1415
1425
|
return git;
|
|
1416
1426
|
}
|
|
1417
1427
|
|
|
@@ -2564,28 +2564,91 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2564
2564
|
if (shortFinalSummary) {
|
|
2565
2565
|
this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now - shortDurationMs });
|
|
2566
2566
|
}
|
|
2567
|
+
// FALSE-IDLE short-gen settle: snapshot the producing turn's start + taskId NOW,
|
|
2568
|
+
// before `generatingStartedAt` is reset below — the settle-arm path (mesh sessions,
|
|
2569
|
+
// see the mesh branch further down) needs the same turn anchor the normal
|
|
2570
|
+
// completedDebounce branch captures, and generatingStartedAt is the fallback for it.
|
|
2571
|
+
const shortEngineTurnStart = typeof (this.adapter as any)?.currentTurnStartedAt === 'number'
|
|
2572
|
+
&& Number.isFinite((this.adapter as any).currentTurnStartedAt)
|
|
2573
|
+
? (this.adapter as any).currentTurnStartedAt as number
|
|
2574
|
+
: 0;
|
|
2575
|
+
const shortTurnStartedAt = shortEngineTurnStart || this.generatingStartedAt || 0;
|
|
2576
|
+
const shortTaskId = this.completingTurnTaskId();
|
|
2567
2577
|
this.generatingDebouncePending = null;
|
|
2568
2578
|
this.generatingStartedAt = 0;
|
|
2569
|
-
|
|
2579
|
+
// FALSE-IDLE short-gen: a short-generating completion with NO transcript
|
|
2580
|
+
// backing at all (shortEvidenceSource === 'unavailable': both the screen parse
|
|
2581
|
+
// AND the external-native transcript failed to yield a final assistant) is just
|
|
2582
|
+
// as unproven as an 'external-native' source that returned no final assistant.
|
|
2583
|
+
// Fold 'unavailable' into the missing-evidence predicate so a zero-evidence dip
|
|
2584
|
+
// (the mid-turn point-sample that triggered this whole false-idle bug) is treated
|
|
2585
|
+
// as weak/held, not fired as a genuine completion. A real shortFinalSummary being
|
|
2586
|
+
// present still clears the gate (the !shortFinalSummary guard is unchanged).
|
|
2587
|
+
const missingEvidence = ((this.provider as any).requiresFinalAssistantBeforeIdle === true
|
|
2588
|
+
|| shortEvidenceSource === 'external-native'
|
|
2589
|
+
|| shortEvidenceSource === 'unavailable') && !shortFinalSummary;
|
|
2570
2590
|
if (missingEvidence) {
|
|
2571
2591
|
LOG.warn('CLI', `[${this.type}] short completion missing final assistant evidence (source=${shortEvidenceSource})`);
|
|
2572
2592
|
}
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
//
|
|
2582
|
-
//
|
|
2583
|
-
//
|
|
2584
|
-
|
|
2585
|
-
//
|
|
2593
|
+
if (this.isAutonomousMeshSession()) {
|
|
2594
|
+
// FALSE-IDLE short-gen settle (the core fix): for an autonomously-progressing
|
|
2595
|
+
// mesh session (delegated worker OR self-coordinator), the short-generating
|
|
2596
|
+
// branch was a POINT-SAMPLE — a single idle read from getStatus({allowParse:false})
|
|
2597
|
+
// that fires the completion INLINE with zero continuity backing. When the worker
|
|
2598
|
+
// is merely mid-turn (a sub-3s dip between two tool calls), this synchronously
|
|
2599
|
+
// emitted a false agent:generating_completed the coordinator can never correct.
|
|
2600
|
+
//
|
|
2601
|
+
// Route it through the SAME settle + continuity machinery as the normal
|
|
2602
|
+
// completedDebounce branch: arm completedDebouncePending capturing busyEpochAtArm
|
|
2603
|
+
// and lastOutputAtArm, then scheduleCompletedDebounceFlush(NATIVE_HISTORY_MESH_IDLE_SETTLE_MS)
|
|
2604
|
+
// so flushCompletedDebounceIfFinalized() re-verifies CONTINUOUS idle before emitting.
|
|
2605
|
+
// A busy re-entry (busyEpoch bump) or new PTY output within the settle window —
|
|
2606
|
+
// exactly what happens when the worker resumes its next tool call — CANCELS the
|
|
2607
|
+
// false completion. Genuine completions (real final assistant) still emit at the
|
|
2608
|
+
// end of the (short, 4s) settle window; missingEvidence flows through the
|
|
2609
|
+
// finalization block (missing_final_assistant → CANON-C weak/held) rather than
|
|
2610
|
+
// being frozen as a genuine inline fire.
|
|
2611
|
+
this.completedDebouncePending = {
|
|
2612
|
+
chatTitle,
|
|
2613
|
+
duration: Math.round(shortDurationMs / 1000),
|
|
2614
|
+
timestamp: now,
|
|
2615
|
+
firstObservedAt: now,
|
|
2616
|
+
// Short-gen enters from generating→idle (or waiting_approval→idle); the
|
|
2617
|
+
// completedDebounce finalization gate treats previousStatus for its
|
|
2618
|
+
// approval-resolution / inter-approval-valley handling. lastStatus is the
|
|
2619
|
+
// status we transitioned FROM here.
|
|
2620
|
+
previousStatus: this.lastStatus,
|
|
2621
|
+
...(shortTaskId ? { taskId: shortTaskId } : {}),
|
|
2622
|
+
...(shortTurnStartedAt ? { turnStartedAt: shortTurnStartedAt } : {}),
|
|
2623
|
+
// FALSE-IDLE continuity: same arm-time snapshots as the normal branch so the
|
|
2624
|
+
// flush guard can prove continuous idle across the settle window.
|
|
2625
|
+
busyEpochAtArm: this.busyEpoch,
|
|
2626
|
+
...(typeof adapterStatus?.lastOutputAt === 'number' && Number.isFinite(adapterStatus.lastOutputAt)
|
|
2627
|
+
? { lastOutputAtArm: adapterStatus.lastOutputAt as number }
|
|
2628
|
+
: {}),
|
|
2629
|
+
};
|
|
2630
|
+
LOG.info('CLI', `[${this.type}] short-generating routed through settle window (${shortDurationMs}ms, source=${shortEvidenceSource}, missingEvidence=${missingEvidence}) — arming completedDebouncePending instead of inline fire`);
|
|
2631
|
+
// EVTTRACE: now traces the settle ARM (not an inline fire) for mesh sessions,
|
|
2632
|
+
// so logs show the short-gen path deferring to continuity re-check.
|
|
2586
2633
|
if (this.isMeshWorkerSession()) {
|
|
2587
|
-
traceMeshEventStage('
|
|
2634
|
+
traceMeshEventStage('arm', this.meshTraceCtx(), `short-generating settle-arm (source=${shortEvidenceSource}, missingEvidence=${missingEvidence})`);
|
|
2588
2635
|
}
|
|
2636
|
+
this.scheduleCompletedDebounceFlush(NATIVE_HISTORY_MESH_IDLE_SETTLE_MS);
|
|
2637
|
+
} else if (missingEvidence) {
|
|
2638
|
+
// NON-MESH, missing evidence: suppress the completion event entirely (the
|
|
2639
|
+
// original !hasMeshContext suppression). A genuinely non-mesh session has no
|
|
2640
|
+
// coordinator to notify, and firing a completion with no confirmed final
|
|
2641
|
+
// assistant would just surface an empty/unconfirmed turn. Leave
|
|
2642
|
+
// completedDebouncePending null — the session is now idle with no confirmed
|
|
2643
|
+
// turn, matching the startup-blip suppression semantics. (No EvtTrace: not a
|
|
2644
|
+
// mesh session, so nothing routes to a coordinator.)
|
|
2645
|
+
LOG.info('CLI', `[${this.type}] short completion suppressed: missing final assistant evidence, non-mesh session (source=${shortEvidenceSource})`);
|
|
2646
|
+
} else {
|
|
2647
|
+
// NON-MESH interactive fast-path with a CONFIRMED summary: keep the existing
|
|
2648
|
+
// inline fire. The dashboard UX reason for the short path (a fast turn should
|
|
2649
|
+
// surface its completion promptly without a 4s settle) still holds, and a
|
|
2650
|
+
// non-mesh session has no coordinator to be falsely notified — the false-idle
|
|
2651
|
+
// bug being fixed is specifically the mesh worker/coordinator misfire above.
|
|
2589
2652
|
this.pushEvent({
|
|
2590
2653
|
event: 'agent:generating_completed',
|
|
2591
2654
|
chatTitle,
|
|
@@ -2596,7 +2659,6 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2596
2659
|
reason: 'short_generating_suppressed',
|
|
2597
2660
|
shortDurationMs,
|
|
2598
2661
|
finalAssistantEvidenceSource: shortEvidenceSource,
|
|
2599
|
-
...(missingEvidence ? { blockReason: 'missing_final_assistant' } : {}),
|
|
2600
2662
|
},
|
|
2601
2663
|
});
|
|
2602
2664
|
}
|
|
@@ -556,54 +556,112 @@ interface AgyDbStepRow {
|
|
|
556
556
|
* Parse a per-session conversations/<uuid>.db (SQLite) into NativeHistoryMessages.
|
|
557
557
|
* Returns null when the db is unreadable, empty, or yields no chat messages.
|
|
558
558
|
*/
|
|
559
|
+
/**
|
|
560
|
+
* True when an error thrown by better-sqlite3 open/read is a transient
|
|
561
|
+
* SQLITE_BUSY / "database is locked" condition rather than a permanent one.
|
|
562
|
+
*
|
|
563
|
+
* On win32 antigravity holds a mandatory WAL write/checkpoint lock while it
|
|
564
|
+
* persists a step; a readonly open racing that lock throws SQLITE_BUSY. That
|
|
565
|
+
* is transient — the answer IS already on disk — so it must be retried, NOT
|
|
566
|
+
* collapsed to "no session" (which erases the just-written assistant answer on
|
|
567
|
+
* a chat_history re-query). macOS advisory locking + WAL reader-doesn't-block-
|
|
568
|
+
* writer masks this, which is why it is win32-specific.
|
|
569
|
+
*/
|
|
570
|
+
function isSqliteBusyError(err: unknown): boolean {
|
|
571
|
+
if (!err) return false;
|
|
572
|
+
const code = (err as any).code;
|
|
573
|
+
if (typeof code === 'string' && code.includes('SQLITE_BUSY')) return true;
|
|
574
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
575
|
+
return /SQLITE_BUSY|database is locked|database table is locked/i.test(msg);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const AGY_DB_BUSY_TIMEOUT_MS = 3000;
|
|
579
|
+
const AGY_DB_MAX_ATTEMPTS = 4;
|
|
580
|
+
const AGY_DB_RETRY_BACKOFF_MS = [50, 100, 150];
|
|
581
|
+
|
|
582
|
+
function sleepBusy(ms: number): void {
|
|
583
|
+
// Synchronous busy-wait: parseConversationDb is a sync function called from a
|
|
584
|
+
// sync read path, and better-sqlite3 itself is synchronous. The waits are
|
|
585
|
+
// tiny (≤150ms) and only occur under genuine lock contention, so a short
|
|
586
|
+
// spin-sleep is acceptable and keeps the call site synchronous.
|
|
587
|
+
const end = Date.now() + ms;
|
|
588
|
+
while (Date.now() < end) { /* spin */ }
|
|
589
|
+
}
|
|
590
|
+
|
|
559
591
|
function parseConversationDb(
|
|
560
592
|
filePath: string,
|
|
561
593
|
sessionId: string,
|
|
562
594
|
workspace?: string,
|
|
563
595
|
): NativeHistoryMessage[] | null {
|
|
564
|
-
let
|
|
596
|
+
let Database: any;
|
|
565
597
|
try {
|
|
566
|
-
|
|
567
|
-
db = new Database(filePath, { readonly: true, fileMustExist: true });
|
|
598
|
+
Database = loadBetterSqlite3();
|
|
568
599
|
} catch (err) {
|
|
569
|
-
// better-sqlite3 unavailable (ABI mismatch / not
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
// reader still degrades gracefully (returns null → dispatcher falls back to
|
|
575
|
-
// brain/.pb), but the operator now knows WHY the .db path produced nothing.
|
|
600
|
+
// better-sqlite3 binding genuinely unavailable (ABI mismatch / not built
|
|
601
|
+
// into this bundle). This is the only true "cannot read at all" case — a
|
|
602
|
+
// real load failure, distinct from transient lock contention below. Warn
|
|
603
|
+
// once at WARN so it is greppable; the reader degrades gracefully (returns
|
|
604
|
+
// null → dispatcher falls back to brain/.pb).
|
|
576
605
|
LOG.warn(
|
|
577
606
|
'NativeHistory',
|
|
578
|
-
`antigravity .db reader could not
|
|
607
|
+
`antigravity .db reader could not load better-sqlite3 for ${path.basename(filePath)}: ${err instanceof Error ? err.message : String(err)} (native binding unavailable — assistant answers in this .db will not surface)`,
|
|
579
608
|
);
|
|
580
609
|
return null;
|
|
581
610
|
}
|
|
582
611
|
|
|
583
|
-
let rows: AgyDbStepRow[];
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
612
|
+
let rows: AgyDbStepRow[] | null = null;
|
|
613
|
+
let lastBusyErr: unknown;
|
|
614
|
+
|
|
615
|
+
for (let attempt = 1; attempt <= AGY_DB_MAX_ATTEMPTS; attempt++) {
|
|
616
|
+
let db: any;
|
|
617
|
+
try {
|
|
618
|
+
db = new Database(filePath, { readonly: true, fileMustExist: true });
|
|
619
|
+
// Ask SQLite itself to wait (rather than failing fast) if the WAL
|
|
620
|
+
// lock is momentarily held by antigravity. Set as early as possible
|
|
621
|
+
// after open so the prepare/all below inherits the wait.
|
|
622
|
+
try { db.pragma(`busy_timeout = ${AGY_DB_BUSY_TIMEOUT_MS}`); } catch { /* ignore */ }
|
|
623
|
+
rows = db
|
|
624
|
+
.prepare(
|
|
625
|
+
`SELECT idx, step_type, step_payload
|
|
626
|
+
FROM steps
|
|
627
|
+
WHERE step_type IN (${AGY_STEP_TYPE_USER}, ${AGY_STEP_TYPE_MODEL})
|
|
628
|
+
ORDER BY idx ASC`,
|
|
629
|
+
)
|
|
630
|
+
.all() as AgyDbStepRow[];
|
|
631
|
+
break; // success
|
|
632
|
+
} catch (err) {
|
|
633
|
+
if (isSqliteBusyError(err)) {
|
|
634
|
+
// Transient WAL lock contention. Do NOT collapse to null on the first
|
|
635
|
+
// failure — the assistant answer is already persisted; treating a busy
|
|
636
|
+
// lock as "no session" is exactly what erased answers on re-query.
|
|
637
|
+
// Retry with a small backoff; only give up after attempts exhausted.
|
|
638
|
+
lastBusyErr = err;
|
|
639
|
+
if (attempt < AGY_DB_MAX_ATTEMPTS) {
|
|
640
|
+
sleepBusy(AGY_DB_RETRY_BACKOFF_MS[attempt - 1] ?? 150);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
LOG.warn(
|
|
644
|
+
'NativeHistory',
|
|
645
|
+
`antigravity .db ${path.basename(filePath)} stayed locked (SQLITE_BUSY) after ${AGY_DB_MAX_ATTEMPTS} attempts: ${err instanceof Error ? err.message : String(err)} (WAL write/checkpoint lock contention — assistant answers may transiently not surface this read)`,
|
|
646
|
+
);
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
// `steps` table absent / unexpected schema, or a genuine open/parse
|
|
650
|
+
// failure that is not lock contention — a real (but recoverable) shape
|
|
651
|
+
// mismatch. Log at debug so a schema drift in a future antigravity
|
|
652
|
+
// release is diagnosable without spamming logs for every legacy db.
|
|
653
|
+
LOG.debug(
|
|
654
|
+
'NativeHistory',
|
|
655
|
+
`antigravity .db ${path.basename(filePath)} not readable: ${err instanceof Error ? err.message : String(err)}`,
|
|
656
|
+
);
|
|
657
|
+
return null;
|
|
658
|
+
} finally {
|
|
659
|
+
try { db?.close(); } catch { /* ignore */ }
|
|
660
|
+
}
|
|
605
661
|
}
|
|
606
662
|
|
|
663
|
+
void lastBusyErr; // referenced only for retry bookkeeping above
|
|
664
|
+
|
|
607
665
|
if (!Array.isArray(rows) || rows.length === 0) return null;
|
|
608
666
|
|
|
609
667
|
const normalizedWorkspace = typeof workspace === 'string' ? workspace.trim() : '';
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -849,6 +849,16 @@ export interface LocalMeshNodeEntry {
|
|
|
849
849
|
*/
|
|
850
850
|
reportedPlatform?: string;
|
|
851
851
|
reportedArch?: string;
|
|
852
|
+
/**
|
|
853
|
+
* The operator-set machine nickname (config.machineNickname) of the daemon
|
|
854
|
+
* that owns this node's workspace. The local coordinator stamps its own
|
|
855
|
+
* config value onto its self/base node; a remote member self-reports its
|
|
856
|
+
* value on the git_status envelope (reporterMachineNickname), which the
|
|
857
|
+
* coordinator persists here on each direct git probe. Feeds
|
|
858
|
+
* buildMeshNodeDisplayLabel so the mesh UI renders the friendly nickname
|
|
859
|
+
* instead of a raw daemonId/nodeId. Absent until set/first-reported.
|
|
860
|
+
*/
|
|
861
|
+
machineNickname?: string;
|
|
852
862
|
policy: RepoMeshNodePolicy;
|
|
853
863
|
/**
|
|
854
864
|
* Per-node instruction surfaced in the coordinator prompt so the LLM
|