@adhdev/daemon-core 0.9.82-rc.397 → 0.9.82-rc.399
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/commands/router.d.ts +23 -0
- package/dist/index.js +76 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +76 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/commands/router.ts +66 -0
- package/src/mesh/mesh-event-forwarding.ts +23 -0
- package/src/providers/cli-provider-instance.ts +29 -2
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.399",
|
|
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.399",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
package/src/commands/router.ts
CHANGED
|
@@ -727,6 +727,72 @@ export class DaemonCommandRouter {
|
|
|
727
727
|
return true;
|
|
728
728
|
}
|
|
729
729
|
|
|
730
|
+
/**
|
|
731
|
+
* WORKTREE-BOOTSTRAP-COORD-STATE: mark a worktree node's bootstrap as reaching a
|
|
732
|
+
* terminal state (complete / failed) in THIS daemon's mesh view.
|
|
733
|
+
*
|
|
734
|
+
* Root cause this fixes: clone_mesh_node forwards the clone+bootstrap to the
|
|
735
|
+
* source node's daemon (the worktree's machine). persistWorktreeSetupState
|
|
736
|
+
* therefore flips worktreeBootstrap.status to 'complete' on the WORKER daemon's
|
|
737
|
+
* mesh object — never on the coordinator's. The coordinator only ever holds the
|
|
738
|
+
* 'running' state it stamped from the forwarded clone reply. The claim path's
|
|
739
|
+
* bootstrap gate (mesh-event-forwarding agent:ready / mesh-queue-assignment)
|
|
740
|
+
* reads the coordinator's mesh via getMeshWithCache, sees status==='running'
|
|
741
|
+
* forever, and DEFERS every claim — so the worktree_bootstrap_complete re-fire
|
|
742
|
+
* (triggerMeshQueue) loops against a gate that never opens: claim never lands,
|
|
743
|
+
* the idle session is re-registered each tick, and auto-launch keeps spawning
|
|
744
|
+
* fresh sessions (runaway worktree-session multiplication).
|
|
745
|
+
*
|
|
746
|
+
* Called from the worktree_bootstrap_complete/_failed event handler BEFORE the
|
|
747
|
+
* queue re-fire so the gate sees the terminal state and the deferred claim can
|
|
748
|
+
* finally land. Updates the inline cache (clone worktree nodes are inline-only)
|
|
749
|
+
* and, when the node also exists in local config, persists there too; both paths
|
|
750
|
+
* invalidate the aggregate status cache. Best-effort and idempotent.
|
|
751
|
+
*/
|
|
752
|
+
public markWorktreeBootstrapTerminalState(meshId: string, nodeId: string, status: 'complete' | 'failed'): void {
|
|
753
|
+
if (!meshId || !nodeId) return;
|
|
754
|
+
const stamp = (mesh: any): boolean => {
|
|
755
|
+
if (!mesh || !Array.isArray(mesh.nodes)) return false;
|
|
756
|
+
const node = mesh.nodes.find((entry: any) => meshNodeIdMatches(entry, nodeId));
|
|
757
|
+
if (!node) return false;
|
|
758
|
+
const prev = (node.worktreeBootstrap && typeof node.worktreeBootstrap === 'object')
|
|
759
|
+
? node.worktreeBootstrap as Record<string, unknown>
|
|
760
|
+
: {};
|
|
761
|
+
if (prev.status === status) return false;
|
|
762
|
+
node.worktreeBootstrap = {
|
|
763
|
+
...prev,
|
|
764
|
+
status,
|
|
765
|
+
completedAt: prev.completedAt ?? new Date().toISOString(),
|
|
766
|
+
};
|
|
767
|
+
return true;
|
|
768
|
+
};
|
|
769
|
+
let changed = false;
|
|
770
|
+
// Inline cache (the authoritative view for inline-only clone worktree nodes).
|
|
771
|
+
try {
|
|
772
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
773
|
+
if (cached && stamp(cached)) {
|
|
774
|
+
cached.updatedAt = new Date().toISOString();
|
|
775
|
+
this.inlineMeshCache.set(meshId, cached);
|
|
776
|
+
changed = true;
|
|
777
|
+
}
|
|
778
|
+
} catch { /* best-effort */ }
|
|
779
|
+
if (changed) this.invalidateAggregateMeshStatus(meshId);
|
|
780
|
+
// Local config (a worktree node registered via addNode also lives here).
|
|
781
|
+
// Done in a detached dynamic-import chain so the method stays sync; both the
|
|
782
|
+
// stamp and the persist are best-effort, and the inline-cache stamp above is
|
|
783
|
+
// what the coordinator's claim gate reads.
|
|
784
|
+
void import('../config/mesh-config.js')
|
|
785
|
+
.then(({ getMesh, updateNode }) => {
|
|
786
|
+
const local = getMesh(meshId);
|
|
787
|
+
if (local && stamp(local)) {
|
|
788
|
+
const node = local.nodes.find((entry: any) => meshNodeIdMatches(entry, nodeId));
|
|
789
|
+
if (node) updateNode(meshId, node.id, { worktreeBootstrap: node.worktreeBootstrap } as any);
|
|
790
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
791
|
+
}
|
|
792
|
+
})
|
|
793
|
+
.catch(() => { /* persistence is best-effort */ });
|
|
794
|
+
}
|
|
795
|
+
|
|
730
796
|
private tombstoneRemovedInlineMeshNode(meshId: string, nodeId: string): void {
|
|
731
797
|
if (!nodeId) return;
|
|
732
798
|
let set = this.removedInlineMeshNodeIds.get(meshId);
|
|
@@ -1053,6 +1053,29 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1053
1053
|
// is drained in every topology. 'failed' is re-fired too so a deferred-then-failed bootstrap
|
|
1054
1054
|
// dispatches and fails loudly/visibly rather than stranding silently. Falls through to the
|
|
1055
1055
|
// coordinator broadcast below — the bootstrap event is still delivered to the coordinator.
|
|
1056
|
+
// WORKTREE-BOOTSTRAP-COORD-STATE: stamp the terminal bootstrap status onto the
|
|
1057
|
+
// COORDINATOR's mesh view BEFORE re-firing the queue. The clone+bootstrap ran on
|
|
1058
|
+
// the worker daemon (clone_mesh_node forwards to the source node's machine), so
|
|
1059
|
+
// persistWorktreeSetupState only flipped status→'complete' on the worker's mesh
|
|
1060
|
+
// object — the coordinator still holds the 'running' state it stamped from the
|
|
1061
|
+
// forwarded clone reply. Without this, the claim gate (agent:ready defer above +
|
|
1062
|
+
// mesh-queue-assignment) reads getMeshWithCache, sees 'running' forever, and
|
|
1063
|
+
// defers every claim — so this very re-fire loops against a gate that never opens
|
|
1064
|
+
// (claim never lands; idle session re-registered each tick; auto-launch spawns a
|
|
1065
|
+
// fresh session every cycle → runaway worktree-session multiplication). Stamping
|
|
1066
|
+
// the terminal state opens the gate so the deferred claim lands on this re-fire.
|
|
1067
|
+
const bootstrapNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
1068
|
+
if (bootstrapNodeId) {
|
|
1069
|
+
try {
|
|
1070
|
+
(components.router as any)?.markWorktreeBootstrapTerminalState?.(
|
|
1071
|
+
args.meshId,
|
|
1072
|
+
bootstrapNodeId,
|
|
1073
|
+
args.event === 'worktree_bootstrap_failed' ? 'failed' : 'complete',
|
|
1074
|
+
);
|
|
1075
|
+
} catch (e: any) {
|
|
1076
|
+
LOG.warn('MeshQueue', `Failed to stamp terminal bootstrap state for ${bootstrapNodeId} (mesh ${args.meshId}): ${e?.message || e}`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1056
1079
|
setImmediate(() => {
|
|
1057
1080
|
triggerMeshQueue(components, args.meshId).catch((e: any) => {
|
|
1058
1081
|
LOG.warn('MeshQueue', `Queue re-fire after ${args.event} failed (mesh ${args.meshId}): ${e?.message || e}`);
|
|
@@ -2035,11 +2035,38 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2035
2035
|
const previousStatus = this.lastStatus;
|
|
2036
2036
|
if (newStatus !== this.lastStatus) {
|
|
2037
2037
|
LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
|
|
2038
|
-
|
|
2038
|
+
// GENERATING-MISSING (win32 fresh-worktree first-turn): a freshly-launched session
|
|
2039
|
+
// is in 'starting' until its startup-grace settles to idle. When the FIRST inject
|
|
2040
|
+
// lands inside that grace window, the adapter can report status DIRECTLY
|
|
2041
|
+
// starting → generating without an intervening 'idle' frame for
|
|
2042
|
+
// detectStatusTransition() to observe. Previously only the idle→generating arm
|
|
2043
|
+
// armed the bookkeeping, so a starting→generating frame fell straight through to the
|
|
2044
|
+
// bare `this.lastStatus = newStatus` update: generatingStartedAt stayed 0 and no
|
|
2045
|
+
// generating_started was queued. The fast turn's generating→idle completion was then
|
|
2046
|
+
// suppressed by the startup-blip guard below (generatingStartedAt===0 &&
|
|
2047
|
+
// !generatingDebouncePending) — so NO generating_started AND NO generating_completed
|
|
2048
|
+
// ever fired and the mesh coordinator never learned the worker went idle.
|
|
2049
|
+
//
|
|
2050
|
+
// We extend the idle→generating arm to also fire on starting→generating, BUT ONLY
|
|
2051
|
+
// when a real turn is in flight. The adapter script can also report 'generating' from
|
|
2052
|
+
// pure startup PTY noise (no task dispatched) — antigravity/codex/hermes-cli all
|
|
2053
|
+
// exercise that benign starting→generating→idle blip, which must NOT emit a
|
|
2054
|
+
// completion (see "startup-phase spurious completion suppression" tests).
|
|
2055
|
+
// hasAdapterPendingResponse() is the discriminator: a genuine inject sets the
|
|
2056
|
+
// adapter's isWaitingForResponse / currentTurnScope (or leaves a partial response),
|
|
2057
|
+
// whereas startup repaint noise leaves all of them empty. So an armed
|
|
2058
|
+
// starting→generating means "the worker actually started its first turn", and a
|
|
2059
|
+
// bare one stays a suppressed blip via the existing fall-through.
|
|
2060
|
+
const startingToGeneratingWithActiveTurn = this.lastStatus === 'starting'
|
|
2061
|
+
&& newStatus === 'generating'
|
|
2062
|
+
&& this.hasAdapterPendingResponse();
|
|
2063
|
+
if (((this.lastStatus === 'idle' && newStatus === 'generating') || startingToGeneratingWithActiveTurn)) {
|
|
2039
2064
|
// If a completion event is already pending and the turn has ended
|
|
2040
2065
|
// (generatingStartedAt===0), the PTY is painting its prompt area
|
|
2041
2066
|
// after completing. Ignore this blip — do not cancel the pending
|
|
2042
|
-
// completion and do not advance lastStatus to generating.
|
|
2067
|
+
// completion and do not advance lastStatus to generating. (On a true
|
|
2068
|
+
// starting→generating the session is fresh: completedDebouncePending is
|
|
2069
|
+
// null, so this blip guard is a no-op and we arm normally below.)
|
|
2043
2070
|
if (this.completedDebouncePending && this.generatingStartedAt === 0) {
|
|
2044
2071
|
LOG.debug('CLI', `[${this.type}] ignoring post-completion PTY generating blip (generatingStartedAt=0)`);
|
|
2045
2072
|
return;
|