@adhdev/daemon-core 0.9.82-rc.411 → 0.9.82-rc.413
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/med-family/mesh-crud.d.ts +27 -0
- package/dist/config/mesh-json-config.d.ts +31 -131
- package/dist/config/repo-settings.d.ts +77 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +817 -802
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +804 -791
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-ledger.d.ts +1 -1
- package/dist/mesh/mesh-task-inflight.d.ts +46 -0
- package/package.json +2 -2
- package/src/commands/high-family/mesh-coordinator-launch.ts +9 -4
- package/src/commands/med-family/mesh-crud.ts +85 -8
- package/src/commands/med-family/mesh-queue.ts +14 -0
- package/src/config/mesh-json-config.ts +40 -281
- package/src/config/repo-settings.ts +111 -0
- package/src/index.ts +6 -1
- package/src/mesh/mesh-ledger.ts +10 -0
- package/src/mesh/mesh-missions.ts +86 -1
- package/src/mesh/mesh-queue-assignment.ts +69 -61
- package/src/mesh/mesh-task-inflight.ts +70 -0
- package/src/mesh/mesh-work-queue.ts +25 -0
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { EventEmitter } from 'events';
|
|
16
16
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
17
|
-
export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'p2p_dispatch_failed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_joined' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled' | 'direct_fast_forward' | 'delivery_unroutable' | 'direct_dispatch_pruned' | 'event_held' | 'task_reclaimed' | 'coordinator_operating_note';
|
|
17
|
+
export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'p2p_dispatch_failed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_joined' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled' | 'direct_fast_forward' | 'delivery_unroutable' | 'direct_dispatch_pruned' | 'event_held' | 'task_reclaimed' | 'coordinator_operating_note' | 'mission_created' | 'mission_status_changed' | 'mission_goal_updated';
|
|
18
18
|
export interface MeshLedgerEntry {
|
|
19
19
|
id: string;
|
|
20
20
|
meshId: string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CANON-IDENTITY single-flight dispatch guard.
|
|
3
|
+
*
|
|
4
|
+
* A queue task is double-dispatched into two worker sessions when a SECOND dispatch
|
|
5
|
+
* path opens after the first has already claimed + dispatched the task. The two
|
|
6
|
+
* observed paths:
|
|
7
|
+
* - the idle-claim drain / auto-launch both funnel through tryAssignQueueTask,
|
|
8
|
+
* which is serialized by the atomic claim transaction (claimNextQueueTask) — so
|
|
9
|
+
* two concurrent claims can never both win; and
|
|
10
|
+
* - the operator requeue tool (mesh_queue_requeue → requeueTask), which flips an
|
|
11
|
+
* already-`assigned` row back to `pending` REGARDLESS of whether the worker
|
|
12
|
+
* holding it is still generating. A requeue issued while the worker is mid-turn
|
|
13
|
+
* re-opens the task for a SECOND session to claim — the live `ade8586d` race.
|
|
14
|
+
*
|
|
15
|
+
* The atomic claim already discriminates pending-vs-assigned, but it cannot tell a
|
|
16
|
+
* GENUINELY-in-flight assigned row (dispatched, worker generating) from a STALE
|
|
17
|
+
* assigned row (dead session, dispatch never confirmed) — and the requeue contract
|
|
18
|
+
* must still reopen the stale case. This module is that discriminator: a task id is
|
|
19
|
+
* registered here ONLY by the dispatch path at the moment it hands the claimed task
|
|
20
|
+
* to a transport, and cleared the moment the task leaves the `assigned` state
|
|
21
|
+
* (terminal completion/failure, dispatch-failure requeue, cancel, reclaim, or a
|
|
22
|
+
* forced requeue). requeueTask consults it and refuses (no-op) to reopen a task that
|
|
23
|
+
* is still in-flight unless the caller passes `force`.
|
|
24
|
+
*
|
|
25
|
+
* Dependency-free leaf (no imports) so both mesh-queue-assignment (begin) and
|
|
26
|
+
* mesh-work-queue (clear + the requeue guard) can import it without a cycle. The key
|
|
27
|
+
* is `${meshId}::${taskId}`; a task id is a single-form UUID, so no daemon-id
|
|
28
|
+
* normalization is needed on the key itself.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Mark a task as actively dispatched/generating (in-flight). Called by the dispatch
|
|
32
|
+
* path right after a successful claim, before/as the task is handed to a transport.
|
|
33
|
+
* Returns true when this call transitioned the task into the in-flight set, false
|
|
34
|
+
* when it was already in-flight (a redundant begin — the caller may treat that as a
|
|
35
|
+
* signal that a dispatch is already live for this task).
|
|
36
|
+
*/
|
|
37
|
+
export declare function beginTaskDispatchInFlight(meshId: string, taskId: string): boolean;
|
|
38
|
+
/** True while a task is actively dispatched/generating (registered by the dispatch
|
|
39
|
+
* path and not yet cleared by a terminal/requeue/cancel transition). */
|
|
40
|
+
export declare function isTaskDispatchInFlight(meshId: string, taskId: string): boolean;
|
|
41
|
+
/** Clear a task's in-flight mark. Called whenever the task leaves the `assigned`
|
|
42
|
+
* state (terminal completion/failure, dispatch-failure requeue, cancel, reclaim, or
|
|
43
|
+
* a forced requeue). Idempotent / safe to call when the task was never in-flight. */
|
|
44
|
+
export declare function endTaskDispatchInFlight(meshId: string, taskId: string): void;
|
|
45
|
+
/** Test-only: drop all in-flight marks so a fresh test starts from a clean guard. */
|
|
46
|
+
export declare function __resetTaskDispatchInFlightForTests(): void;
|
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.413",
|
|
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.413",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -177,10 +177,15 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
|
|
|
177
177
|
|
|
178
178
|
// OPSRULES — layer the repo-shared declarative mesh config
|
|
179
179
|
// (.adhdev/mesh.json in the coordinator node workspace, else
|
|
180
|
-
// the calling cwd) UNDER the machine-local mesh entry.
|
|
181
|
-
//
|
|
182
|
-
// is never
|
|
183
|
-
//
|
|
180
|
+
// the calling cwd) UNDER the machine-local mesh entry. Only the
|
|
181
|
+
// COORDINATOR zone (prompt override/append) is merged — policy is
|
|
182
|
+
// machine-local and is never sourced from the repo file. The merge
|
|
183
|
+
// is in-memory only: meshes.json on disk is never mutated. Coordinator
|
|
184
|
+
// launch needs only the mesh.json zone (coordinator + operatingNotes),
|
|
185
|
+
// so it reads that dedicated loader directly rather than the unified
|
|
186
|
+
// loadRepoSettings (which would also touch refine/bootstrap via the
|
|
187
|
+
// machine-local inline seam). The effective mesh feeds prompt
|
|
188
|
+
// building; operating notes are merged with the runtime ledger below.
|
|
184
189
|
const { loadRepoMeshJsonConfig, applyRepoMeshConfig, mergeEffectiveOperatingNotes } =
|
|
185
190
|
await import('../../config/mesh-json-config.js');
|
|
186
191
|
const repoMeshConfigLoad = loadRepoMeshJsonConfig(workspace);
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
runMeshWorktreeBootstrap,
|
|
17
17
|
type WorktreeBootstrapState,
|
|
18
18
|
} from '../../mesh/worktree-bootstrap-config.js';
|
|
19
|
+
import { loadRepoSettings } from '../../config/repo-settings.js';
|
|
19
20
|
import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../../mesh/mesh-events.js';
|
|
20
21
|
import { loadConfig } from '../../config/config.js';
|
|
21
22
|
import {
|
|
@@ -24,8 +25,61 @@ import {
|
|
|
24
25
|
readMeshNodeMachineId,
|
|
25
26
|
} from '../router.js';
|
|
26
27
|
import type { CommandRouterResult } from '../router.js';
|
|
28
|
+
import type { GitRepoIdentity } from '../../git/git-types.js';
|
|
27
29
|
import type { MedFamilyContext, MedFamilyHandler } from './types.js';
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Decision for syncing a freshly-cloned worktree's `oss` submodule to its clone
|
|
33
|
+
* source node, applying resolveWorktreeBaseStartPoint's origin-tip-priority
|
|
34
|
+
* policy to the submodule.
|
|
35
|
+
*
|
|
36
|
+
* On clone, `submodule update --init` checks the worktree's `oss` out at the
|
|
37
|
+
* gitlink recorded in the FRESH root base — which the root base-stale fix
|
|
38
|
+
* branches from origin/main, so it is the up-to-date origin tip. The clone
|
|
39
|
+
* source node's *working* `oss` SHA can lag that tip. The original sync blindly
|
|
40
|
+
* checked out the source SHA whenever it merely differed, which REWINDS the
|
|
41
|
+
* submodule back onto the stale source and re-introduces staleness.
|
|
42
|
+
*
|
|
43
|
+
* Guard policy (never rewind, mirror the base-start-point resolver):
|
|
44
|
+
* - source SHA == worktree SHA → `noop`
|
|
45
|
+
* - source SHA is an ancestor of worktree → `skip_rewind` (source is behind; keep fresh tip)
|
|
46
|
+
* - worktree SHA is an ancestor of source → `advance` (source strictly newer; safe fast-forward)
|
|
47
|
+
* - neither is an ancestor (diverged) → `skip_diverged` (keep fresh tip; coordinator reconciles)
|
|
48
|
+
*
|
|
49
|
+
* Both SHAs must already be resolvable in `ossCtx` (the caller fetches the
|
|
50
|
+
* source SHA first). A non-1 git exit (unresolvable SHA / real failure) is
|
|
51
|
+
* rethrown so the caller can fall back to keeping the fresh worktree HEAD.
|
|
52
|
+
*/
|
|
53
|
+
export type OssCloneSyncAction = 'noop' | 'advance' | 'skip_rewind' | 'skip_diverged';
|
|
54
|
+
|
|
55
|
+
export async function decideOssCloneSync(
|
|
56
|
+
ossCtx: GitRepoIdentity,
|
|
57
|
+
worktreeOssSha: string,
|
|
58
|
+
sourceSha: string,
|
|
59
|
+
rg: (ctx: GitRepoIdentity, argv: string[], opts?: { timeoutMs?: number }) => Promise<unknown>,
|
|
60
|
+
): Promise<OssCloneSyncAction> {
|
|
61
|
+
if (!worktreeOssSha || !sourceSha || worktreeOssSha === sourceSha) return 'noop';
|
|
62
|
+
|
|
63
|
+
const isAncestor = async (ancestor: string, descendant: string): Promise<boolean> => {
|
|
64
|
+
try {
|
|
65
|
+
await rg(ossCtx, ['merge-base', '--is-ancestor', ancestor, descendant], { timeoutMs: 10000 });
|
|
66
|
+
return true;
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
// `merge-base --is-ancestor` exits 1 for a clean "not an ancestor".
|
|
69
|
+
// Any other exit (128 = unresolvable commit, etc.) is a real failure.
|
|
70
|
+
if (err?.exitCode === 1 || err?.code === 1) return false;
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Source is an ancestor of the fresh worktree tip → checking it out rewinds.
|
|
76
|
+
if (await isAncestor(sourceSha, worktreeOssSha)) return 'skip_rewind';
|
|
77
|
+
// Worktree tip is an ancestor of source → source is strictly newer → safe FF.
|
|
78
|
+
if (await isAncestor(worktreeOssSha, sourceSha)) return 'advance';
|
|
79
|
+
// Neither is an ancestor → diverged → keep the fresh worktree HEAD.
|
|
80
|
+
return 'skip_diverged';
|
|
81
|
+
}
|
|
82
|
+
|
|
29
83
|
export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
30
84
|
list_meshes: async (_ctx: MedFamilyContext, _args: any) => {
|
|
31
85
|
try {
|
|
@@ -129,7 +183,8 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
129
183
|
// entry. This is an export scaffold for the operator to review and commit to
|
|
130
184
|
// the repo, NOT an automatic data migration: nothing is written to disk and
|
|
131
185
|
// meshes.json is untouched. The returned `scaffold` (object) + `scaffoldJson`
|
|
132
|
-
// (2-space text) capture the
|
|
186
|
+
// (2-space text) capture the coordinator prompt override/append (policy is
|
|
187
|
+
// machine-local and is intentionally NOT exported into mesh.json).
|
|
133
188
|
export_mesh_json_config: async (_ctx: MedFamilyContext, args: any) => {
|
|
134
189
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
135
190
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
@@ -637,7 +692,10 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
637
692
|
};
|
|
638
693
|
|
|
639
694
|
const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
|
|
640
|
-
|
|
695
|
+
// Read the worktree bootstrap config through the unified RepoSettings
|
|
696
|
+
// loader (file-separated `.adhdev/worktree_bootstrap.json`; machine-local
|
|
697
|
+
// inline seam honored). runMeshWorktreeBootstrap below re-loads it to run.
|
|
698
|
+
const loadedBootstrap = loadRepoSettings({ workspace: result.worktreePath, mesh }).worktreeBootstrap;
|
|
641
699
|
const runningBootstrapState: WorktreeBootstrapState = {
|
|
642
700
|
status: 'running',
|
|
643
701
|
required: loadedBootstrap.config?.required !== false,
|
|
@@ -679,13 +737,32 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
679
737
|
const worktreeOssHeadOut = await rg(ossCtx, ['rev-parse', 'HEAD'], { timeoutMs: 10000 });
|
|
680
738
|
const worktreeOssSha = (typeof worktreeOssHeadOut === 'string' ? worktreeOssHeadOut : (worktreeOssHeadOut as any)?.stdout ?? '').trim();
|
|
681
739
|
|
|
682
|
-
if (worktreeOssSha !== sourceSha) {
|
|
683
|
-
//
|
|
740
|
+
if (worktreeOssSha && worktreeOssSha !== sourceSha) {
|
|
741
|
+
// Bring the source node's oss HEAD into the worktree object DB so
|
|
742
|
+
// both SHAs are resolvable for the ancestry (rewind) guard below.
|
|
684
743
|
await rg(ossCtx, ['fetch', `${sourceWorkspace}/oss`, 'HEAD'], { timeoutMs: 60000 });
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
744
|
+
|
|
745
|
+
// Rewind guard: the worktree oss HEAD was just checked out from the
|
|
746
|
+
// FRESH (origin/main-derived) root base. Only advance to the source
|
|
747
|
+
// SHA when it is strictly newer — never rewind to a stale source.
|
|
748
|
+
let ossAction: OssCloneSyncAction;
|
|
749
|
+
try {
|
|
750
|
+
ossAction = await decideOssCloneSync(ossCtx, worktreeOssSha, sourceSha, rg);
|
|
751
|
+
} catch (decideErr: any) {
|
|
752
|
+
ossAction = 'skip_diverged';
|
|
753
|
+
console.warn(`[mesh] oss submodule sync guard could not resolve ancestry (kept fresh worktree HEAD): ${decideErr?.message ?? decideErr}`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (ossAction === 'advance') {
|
|
757
|
+
await rg(ossCtx, ['checkout', sourceSha], { timeoutMs: 10000 });
|
|
758
|
+
await rg(worktreeCtx, ['add', 'oss'], { timeoutMs: 10000 });
|
|
759
|
+
await rg(worktreeCtx, ['commit', '-m', 'chore: sync oss to source node HEAD on clone'], { timeoutMs: 10000 });
|
|
760
|
+
console.log(`[mesh] Advanced oss submodule to newer source HEAD ${sourceSha.slice(0, 8)} in worktree`);
|
|
761
|
+
} else if (ossAction === 'skip_rewind') {
|
|
762
|
+
console.warn(`[mesh] Skipped oss submodule rewind on clone: source node oss ${sourceSha.slice(0, 8)} is an ancestor of the fresh worktree oss ${worktreeOssSha.slice(0, 8)} — kept fresher worktree HEAD`);
|
|
763
|
+
} else if (ossAction === 'skip_diverged') {
|
|
764
|
+
console.warn(`[mesh] Skipped oss submodule sync on clone: source node oss ${sourceSha.slice(0, 8)} diverged from the fresh worktree oss ${worktreeOssSha.slice(0, 8)} — kept worktree HEAD (coordinator reconciles)`);
|
|
765
|
+
}
|
|
689
766
|
}
|
|
690
767
|
}
|
|
691
768
|
} catch (ossErr: any) {
|
|
@@ -74,8 +74,22 @@ export const meshQueueHandlers: Record<string, MedFamilyHandler> = {
|
|
|
74
74
|
targetSessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : undefined,
|
|
75
75
|
clearTargetNode: args?.clearTargetNode === true,
|
|
76
76
|
clearTargetSession: args?.clearTargetSession !== false,
|
|
77
|
+
// CANON-IDENTITY: an in-flight (actively-generating) task is refused by
|
|
78
|
+
// default to avoid a duplicate second dispatch; an explicit operator
|
|
79
|
+
// force overrides that guard (and the retry cap).
|
|
80
|
+
force: args?.force === true,
|
|
77
81
|
});
|
|
78
82
|
if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
|
|
83
|
+
// The single-flight guard returns the row UNCHANGED (still 'assigned') when it
|
|
84
|
+
// refuses an in-flight requeue — surface that as a clear, non-success signal so
|
|
85
|
+
// the coordinator does not believe a second dispatch was opened.
|
|
86
|
+
if (task.status === 'assigned' && args?.force !== true) {
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
error: `Task '${taskId}' is actively dispatched/generating; requeue refused to avoid a duplicate second dispatch. Pass force:true to override, or cancel and re-enqueue.`,
|
|
90
|
+
task,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
79
93
|
return { success: true, task };
|
|
80
94
|
} catch (e: any) {
|
|
81
95
|
return { success: false, error: e.message };
|