@adhdev/daemon-core 0.9.82-rc.262 → 0.9.82-rc.263
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/build-info.d.ts +37 -0
- package/dist/git/git-status.d.ts +7 -0
- package/dist/git/git-types.d.ts +19 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +490 -64
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +486 -64
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +18 -0
- package/dist/mesh/mesh-fast-forward.d.ts +41 -1
- package/dist/mesh/mesh-ledger.d.ts +1 -1
- package/dist/mesh/mesh-runtime-store.d.ts +6 -0
- package/dist/mesh/mesh-work-queue.d.ts +6 -0
- package/package.json +1 -1
- package/src/build-info.ts +73 -0
- package/src/commands/router.ts +49 -2
- package/src/git/git-status.ts +73 -1
- package/src/git/git-types.ts +20 -0
- package/src/index.ts +5 -2
- package/src/mesh/mesh-active-work.ts +31 -0
- package/src/mesh/mesh-fast-forward.ts +418 -17
- package/src/mesh/mesh-ledger.ts +1 -0
- package/src/mesh/mesh-runtime-store.ts +19 -0
- package/src/mesh/mesh-work-queue.ts +13 -0
|
@@ -83,6 +83,24 @@ export declare function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): {
|
|
|
83
83
|
terminalDirectWork: MeshActiveWorkRecord[];
|
|
84
84
|
summary: MeshActiveWorkSummary;
|
|
85
85
|
};
|
|
86
|
+
/**
|
|
87
|
+
* staleReason strings (produced by sessionStatusFromNodes above) that indicate the original
|
|
88
|
+
* node/session is GONE from the live mesh — i.e. the staleDirect record is an orphaned ledger
|
|
89
|
+
* artifact, not active or recoverable work. These are the only reasons safe to prune from the
|
|
90
|
+
* active staleDirect surface. The "no provider acknowledgement" reason is deliberately excluded:
|
|
91
|
+
* those entries have a still-live node/session (staleDispatchUnacknowledged) and represent
|
|
92
|
+
* recoverable dispatch failures, never orphans.
|
|
93
|
+
*/
|
|
94
|
+
export declare const PRUNABLE_ORPHAN_STALE_REASONS: ReadonlySet<string>;
|
|
95
|
+
export type StaleDirectPruneClassification = 'prunable_orphan' | 'prunable_terminal' | 'preserve_unacknowledged' | 'preserve_active';
|
|
96
|
+
/**
|
|
97
|
+
* Classify a direct-work record for the staleDirect prune path. Pure function — the prune tool
|
|
98
|
+
* uses this so the safety rules (never touch active work or recoverable unacknowledged dispatches)
|
|
99
|
+
* live next to the staleReason producers and are independently testable.
|
|
100
|
+
*/
|
|
101
|
+
export declare function classifyStaleDirectForPrune(record: Pick<MeshActiveWorkRecord, 'staleReason' | 'staleDispatchUnacknowledged' | 'terminal'>, opts?: {
|
|
102
|
+
includeTerminal?: boolean;
|
|
103
|
+
}): StaleDirectPruneClassification;
|
|
86
104
|
export declare function buildCompactStaleDirectWorkSummary(staleDirectWork: MeshActiveWorkRecord[], opts?: {
|
|
87
105
|
sampleLimit?: number;
|
|
88
106
|
detailHint?: string;
|
|
@@ -10,19 +10,50 @@ export interface MeshFastForwardNodeArgs {
|
|
|
10
10
|
submoduleIgnorePaths?: string[];
|
|
11
11
|
timeoutMs?: number;
|
|
12
12
|
trigger?: 'manual' | 'idle_auto' | string;
|
|
13
|
+
/**
|
|
14
|
+
* Operation mode. 'merge' (default) absorbs upstream commits into the local
|
|
15
|
+
* branch via git merge --ff-only (requires ahead=0, behind>0). 'push' publishes
|
|
16
|
+
* local commits to origin via a strict ff-only push (requires HEAD to be a
|
|
17
|
+
* descendant of origin/<branch>); it never force-pushes, resets, or rebases.
|
|
18
|
+
*/
|
|
19
|
+
mode?: 'merge' | 'push';
|
|
20
|
+
/**
|
|
21
|
+
* When mode='push', also fast-forward push submodule (e.g. oss) HEADs to their
|
|
22
|
+
* origin main branch. Gated by allowAutoPublishSubmoduleMainCommits — skipped
|
|
23
|
+
* unless that policy is true. Each submodule must still pass the descendant gate.
|
|
24
|
+
* Defaults false (root push only).
|
|
25
|
+
*/
|
|
26
|
+
pushSubmodules?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Mesh policy flag mirrored from RepoMeshPolicy.allowAutoPublishSubmoduleMainCommits.
|
|
29
|
+
* Submodule pushes are refused unless this is true.
|
|
30
|
+
*/
|
|
31
|
+
allowAutoPublishSubmoduleMainCommits?: boolean;
|
|
13
32
|
}
|
|
14
33
|
export interface MeshFastForwardPlannedStep {
|
|
15
|
-
operation: 'refresh_upstream' | 'verify_clean_worktree' | 'verify_fast_forward' | 'merge_ff_only' | 'submodule_update' | 'verify_post_status';
|
|
34
|
+
operation: 'refresh_upstream' | 'verify_clean_worktree' | 'verify_fast_forward' | 'merge_ff_only' | 'submodule_update' | 'verify_post_status' | 'verify_push_descendant' | 'push_ff_only' | 'push_submodules_ff_only';
|
|
16
35
|
description: string;
|
|
17
36
|
safe: true;
|
|
18
37
|
willMutateWorktree: boolean;
|
|
19
38
|
}
|
|
39
|
+
export interface MeshFastForwardSubmodulePushResult {
|
|
40
|
+
path: string;
|
|
41
|
+
commit?: string;
|
|
42
|
+
remote: string;
|
|
43
|
+
remoteBranch: string;
|
|
44
|
+
pushed: boolean;
|
|
45
|
+
skipped: boolean;
|
|
46
|
+
code: string;
|
|
47
|
+
refspec?: string;
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
20
50
|
export interface MeshFastForwardResult {
|
|
21
51
|
success: boolean;
|
|
22
52
|
code: string;
|
|
23
53
|
nodeId?: string;
|
|
24
54
|
meshId?: string;
|
|
25
55
|
workspace: string;
|
|
56
|
+
mode: 'merge' | 'push';
|
|
26
57
|
allowed: boolean;
|
|
27
58
|
dryRun: boolean;
|
|
28
59
|
willRun: boolean;
|
|
@@ -33,9 +64,18 @@ export interface MeshFastForwardResult {
|
|
|
33
64
|
current?: GitRepoStatus;
|
|
34
65
|
preStatus?: GitRepoStatus;
|
|
35
66
|
postStatus?: GitRepoStatus;
|
|
67
|
+
/** Push target derived from the tracked upstream (mode='push'). */
|
|
68
|
+
pushTarget?: {
|
|
69
|
+
remote: string;
|
|
70
|
+
remoteBranch: string;
|
|
71
|
+
refspec: string;
|
|
72
|
+
};
|
|
73
|
+
/** Submodule ff-only push outcomes (mode='push' with pushSubmodules). */
|
|
74
|
+
submodulePushes?: MeshFastForwardSubmodulePushResult[];
|
|
36
75
|
finalBranchConvergenceState?: Record<string, unknown>;
|
|
37
76
|
operationError?: string;
|
|
38
77
|
ledgerError?: string;
|
|
78
|
+
nextStep?: string;
|
|
39
79
|
trigger?: string;
|
|
40
80
|
}
|
|
41
81
|
export declare function fastForwardMeshNode(args: MeshFastForwardNodeArgs): Promise<MeshFastForwardResult>;
|
|
@@ -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';
|
|
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';
|
|
18
18
|
export interface MeshLedgerEntry {
|
|
19
19
|
id: string;
|
|
20
20
|
meshId: string;
|
|
@@ -72,6 +72,12 @@ export declare class MeshRuntimeStore {
|
|
|
72
72
|
updateDirectDispatchStatus(meshId: string, sessionId: string, status: 'acked' | 'completed' | 'failed' | 'stale'): void;
|
|
73
73
|
cleanupTerminalDirectDispatches(olderThanMs: number): void;
|
|
74
74
|
deleteDirectDispatches(meshId: string): void;
|
|
75
|
+
/**
|
|
76
|
+
* Delete specific direct dispatch rows by taskId for a mesh. Used by the staleDirect prune
|
|
77
|
+
* path to remove orphaned/terminal dispatch records whose node/session is no longer in the
|
|
78
|
+
* live mesh. Returns the number of rows actually deleted. No-op for an empty taskId list.
|
|
79
|
+
*/
|
|
80
|
+
deleteDirectDispatchesByTaskId(meshId: string, taskIds: string[]): number;
|
|
75
81
|
markStaleDirectDispatches(meshId: string, olderThanMs: number): void;
|
|
76
82
|
setRemoteIdleSession(nodeId: string, sessionId: string, providerType: string, expiresAt: number, metadata?: any): void;
|
|
77
83
|
getRemoteIdleSessions(): Array<{
|
|
@@ -243,6 +243,12 @@ export declare function getActiveDirectDispatches(meshId: string): DirectDispatc
|
|
|
243
243
|
export declare function updateDirectDispatchStatus(meshId: string, sessionId: string, status: 'acked' | 'completed' | 'failed' | 'stale'): void;
|
|
244
244
|
export declare function cleanupTerminalDirectDispatches(olderThanMs?: number): void;
|
|
245
245
|
export declare function markStaleDirectDispatches(meshId: string, olderThanMs?: number): void;
|
|
246
|
+
/**
|
|
247
|
+
* Delete specific direct dispatch rows by taskId. Returns the number of rows deleted.
|
|
248
|
+
* Used by the staleDirect prune path to evict orphaned/terminal dispatch records from the
|
|
249
|
+
* active staleDirect surface while leaving the append-only mesh ledger (audit history) intact.
|
|
250
|
+
*/
|
|
251
|
+
export declare function deleteDirectDispatchesByTaskId(meshId: string, taskIds: string[]): number;
|
|
246
252
|
export type MeshToolCallRateResult = {
|
|
247
253
|
rateLimitExceeded: boolean;
|
|
248
254
|
callsInWindow: number;
|
package/package.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon build stamp — single runtime source for "which commit is this running
|
|
3
|
+
* daemon built from?".
|
|
4
|
+
*
|
|
5
|
+
* The values are injected at BUILD time via tsup `define` (esbuild global
|
|
6
|
+
* replacement). Every build config that bundles daemon-core into a shippable
|
|
7
|
+
* daemon (daemon-core's own dist, daemon-standalone, daemon-cloud) replaces
|
|
8
|
+
* these `__DAEMON_BUILD_*` identifiers with the literal git commit/version that
|
|
9
|
+
* was current when the bundle was produced. See:
|
|
10
|
+
* - oss/packages/daemon-core/tsup.config.ts (standalone consumes this dist)
|
|
11
|
+
* - packages/daemon-cloud/tsup.config.ts (re-bundles daemon-core from source)
|
|
12
|
+
*
|
|
13
|
+
* If a bundle is produced WITHOUT the define (e.g. running src directly through
|
|
14
|
+
* tsx in dev, or a build env without git), the identifiers stay undefined and
|
|
15
|
+
* we fall back to `"unknown"` — never a ReferenceError, never a build failure.
|
|
16
|
+
*
|
|
17
|
+
* IMPORTANT (live-reflection caveat): a fresh local `daemon-core dist` rebuild +
|
|
18
|
+
* daemon restart is NOT enough to make a *cloud* daemon report a new commit —
|
|
19
|
+
* daemon-cloud ships its own re-bundle of daemon-core, so the build stamp only
|
|
20
|
+
* advances after the cloud daemon is rebuilt/redeployed and restarted. This is
|
|
21
|
+
* precisely the gap `mesh_status`'s `staleDaemonBuild` warning exists to surface.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// These are replaced at build time by tsup `define`. `typeof` guards keep this
|
|
25
|
+
// safe when the define is absent (the identifiers are simply not declared).
|
|
26
|
+
declare const __DAEMON_BUILD_COMMIT__: string | undefined;
|
|
27
|
+
declare const __DAEMON_BUILD_COMMIT_SHORT__: string | undefined;
|
|
28
|
+
declare const __DAEMON_BUILD_VERSION__: string | undefined;
|
|
29
|
+
declare const __DAEMON_BUILD_AT__: string | undefined;
|
|
30
|
+
|
|
31
|
+
export interface DaemonBuildInfo {
|
|
32
|
+
/** Full 40-char git commit the daemon bundle was built from, or 'unknown'. */
|
|
33
|
+
commit: string;
|
|
34
|
+
/** Short (7-char) form of the same commit, or 'unknown'. */
|
|
35
|
+
commitShort: string;
|
|
36
|
+
/** package.json version baked in at build time, or 'unknown'. */
|
|
37
|
+
version: string;
|
|
38
|
+
/** ISO build timestamp if the build config injected one. */
|
|
39
|
+
builtAt?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readInjected(value: string | undefined): string | undefined {
|
|
43
|
+
if (typeof value !== 'string') return undefined;
|
|
44
|
+
const trimmed = value.trim();
|
|
45
|
+
if (!trimmed || trimmed === 'unknown') return undefined;
|
|
46
|
+
return trimmed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let cached: DaemonBuildInfo | undefined;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the build stamp baked into the running daemon bundle. Pure read of
|
|
53
|
+
* build-time constants — no I/O, safe to call from any runtime path. Cached.
|
|
54
|
+
*/
|
|
55
|
+
export function getDaemonBuildInfo(): DaemonBuildInfo {
|
|
56
|
+
if (cached) return cached;
|
|
57
|
+
|
|
58
|
+
const commit =
|
|
59
|
+
readInjected(typeof __DAEMON_BUILD_COMMIT__ !== 'undefined' ? __DAEMON_BUILD_COMMIT__ : undefined)
|
|
60
|
+
?? 'unknown';
|
|
61
|
+
const commitShort =
|
|
62
|
+
readInjected(typeof __DAEMON_BUILD_COMMIT_SHORT__ !== 'undefined' ? __DAEMON_BUILD_COMMIT_SHORT__ : undefined)
|
|
63
|
+
?? (commit !== 'unknown' ? commit.slice(0, 7) : 'unknown');
|
|
64
|
+
const version =
|
|
65
|
+
readInjected(typeof __DAEMON_BUILD_VERSION__ !== 'undefined' ? __DAEMON_BUILD_VERSION__ : undefined)
|
|
66
|
+
// Fall back to the runtime-injected package version env used elsewhere.
|
|
67
|
+
?? readInjected(typeof process !== 'undefined' ? process.env?.ADHDEV_PKG_VERSION : undefined)
|
|
68
|
+
?? 'unknown';
|
|
69
|
+
const builtAt = readInjected(typeof __DAEMON_BUILD_AT__ !== 'undefined' ? __DAEMON_BUILD_AT__ : undefined);
|
|
70
|
+
|
|
71
|
+
cached = builtAt ? { commit, commitShort, version, builtAt } : { commit, commitShort, version };
|
|
72
|
+
return cached;
|
|
73
|
+
}
|
package/src/commands/router.ts
CHANGED
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
type WorktreeBootstrapState,
|
|
65
65
|
} from '../mesh/worktree-bootstrap-config.js';
|
|
66
66
|
import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
|
|
67
|
+
import { getDaemonBuildInfo } from '../build-info.js';
|
|
67
68
|
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
68
69
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
69
70
|
import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
|
|
@@ -3521,6 +3522,8 @@ export class DaemonCommandRouter {
|
|
|
3521
3522
|
const skippedSessionIds: string[] = [];
|
|
3522
3523
|
const skippedLiveSessionIds: string[] = [];
|
|
3523
3524
|
const skippedCoordinatorSessionIds: string[] = [];
|
|
3525
|
+
const skippedLiveSessionReasons: Array<{ sessionId: string; reason: string }> = [];
|
|
3526
|
+
const actedLiveDelegateSessionIds: string[] = [];
|
|
3524
3527
|
const deleteUnsupportedSessionIds: string[] = [];
|
|
3525
3528
|
const recordsRemainSessionIds: string[] = [];
|
|
3526
3529
|
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
@@ -3556,16 +3559,49 @@ export class DaemonCommandRouter {
|
|
|
3556
3559
|
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
3557
3560
|
const liveRuntime = surfaceKind === 'live_runtime';
|
|
3558
3561
|
const coordinatorSession = readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId;
|
|
3562
|
+
// A delegate session was launched by the coordinator specifically FOR this node
|
|
3563
|
+
// (meta.meshNodeId === this node). It is 1:1 bound to the node, even when the node
|
|
3564
|
+
// shares its daemon runtime with the main/other nodes. Removing the node should be
|
|
3565
|
+
// able to stop its own delegate session — the shared-daemon concern only applies to
|
|
3566
|
+
// sessions we matched by workspace alone (which could belong to the coordinator or to
|
|
3567
|
+
// a sibling node that is still active).
|
|
3568
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
3569
|
+
const recordMeshNodeFor = readStringValue(record?.meta?.meshNodeFor);
|
|
3570
|
+
const delegateBoundToThisNode = !!recordNodeId
|
|
3571
|
+
&& recordNodeId === args.nodeId
|
|
3572
|
+
&& (!recordMeshNodeFor || recordMeshNodeFor === args.meshId);
|
|
3559
3573
|
if (!hasExplicitSessionIds && coordinatorSession) {
|
|
3560
3574
|
skippedSessionIds.push(sessionId);
|
|
3561
3575
|
skippedCoordinatorSessionIds.push(sessionId);
|
|
3562
3576
|
continue;
|
|
3563
3577
|
}
|
|
3564
|
-
|
|
3578
|
+
// Only the conservative shared-daemon guard for live sessions that are NOT a delegate
|
|
3579
|
+
// explicitly bound to this node. Delegate-bound live sessions fall through and are
|
|
3580
|
+
// stopped/deleted by the mode handlers below (which already record an intentional stop).
|
|
3581
|
+
if (!hasExplicitSessionIds && liveRuntime && !delegateBoundToThisNode) {
|
|
3565
3582
|
skippedSessionIds.push(sessionId);
|
|
3566
3583
|
skippedLiveSessionIds.push(sessionId);
|
|
3584
|
+
const matchedByWorkspaceOnly = !recordNodeId;
|
|
3585
|
+
const reason = recordNodeId && recordNodeId !== args.nodeId
|
|
3586
|
+
? `live_delegate_bound_to_other_node:${recordNodeId}`
|
|
3587
|
+
: matchedByWorkspaceOnly
|
|
3588
|
+
? 'live_session_matched_by_workspace_only_no_node_binding'
|
|
3589
|
+
: 'live_session_not_bound_to_this_node';
|
|
3590
|
+
skippedLiveSessionReasons.push({ sessionId, reason });
|
|
3567
3591
|
continue;
|
|
3568
3592
|
}
|
|
3593
|
+
if (!hasExplicitSessionIds && liveRuntime && delegateBoundToThisNode && args.mode === 'delete_stopped') {
|
|
3594
|
+
// delete_stopped never stops live runtimes by contract — even bound delegates.
|
|
3595
|
+
// Surface a clear reason instead of an unexplained skip so callers know to use
|
|
3596
|
+
// stop / stop_and_delete to release a still-running bound delegate.
|
|
3597
|
+
skippedSessionIds.push(sessionId);
|
|
3598
|
+
skippedLiveSessionIds.push(sessionId);
|
|
3599
|
+
skippedLiveSessionReasons.push({ sessionId, reason: 'live_delegate_preserved_by_delete_stopped_mode_use_stop_or_stop_and_delete' });
|
|
3600
|
+
continue;
|
|
3601
|
+
}
|
|
3602
|
+
if (!hasExplicitSessionIds && liveRuntime && delegateBoundToThisNode) {
|
|
3603
|
+
actedLiveDelegateSessionIds.push(sessionId);
|
|
3604
|
+
}
|
|
3569
3605
|
try {
|
|
3570
3606
|
if (args.mode === 'stop') {
|
|
3571
3607
|
if (!completed) {
|
|
@@ -3631,6 +3667,8 @@ export class DaemonCommandRouter {
|
|
|
3631
3667
|
skippedSessionIds,
|
|
3632
3668
|
skippedLiveSessionIds,
|
|
3633
3669
|
skippedCoordinatorSessionIds,
|
|
3670
|
+
...(actedLiveDelegateSessionIds.length ? { actedLiveDelegateSessionIds } : {}),
|
|
3671
|
+
...(skippedLiveSessionReasons.length ? { skippedLiveSessionReasons } : {}),
|
|
3634
3672
|
...(deleteUnsupported ? {
|
|
3635
3673
|
deleteUnsupported: true,
|
|
3636
3674
|
effectiveCleanup: args.mode === 'stop_and_delete'
|
|
@@ -5783,7 +5821,11 @@ export class DaemonCommandRouter {
|
|
|
5783
5821
|
version: this.deps.statusVersion || 'unknown',
|
|
5784
5822
|
profile: 'metadata',
|
|
5785
5823
|
});
|
|
5786
|
-
|
|
5824
|
+
// Surface the daemon's build stamp so coordinators (mesh_status)
|
|
5825
|
+
// can detect a running daemon that predates a just-merged fix and
|
|
5826
|
+
// is awaiting deploy/restart. Sibling of `status` to avoid
|
|
5827
|
+
// perturbing the dashboard status snapshot shape.
|
|
5828
|
+
return { success: true, status: snapshot, daemonBuild: getDaemonBuildInfo() };
|
|
5787
5829
|
}
|
|
5788
5830
|
|
|
5789
5831
|
case 'get_machine_runtime_stats': {
|
|
@@ -6886,6 +6928,7 @@ export class DaemonCommandRouter {
|
|
|
6886
6928
|
? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
6887
6929
|
: undefined;
|
|
6888
6930
|
let nodeDaemonId: string | undefined;
|
|
6931
|
+
let allowAutoPublishSubmoduleMainCommits = false;
|
|
6889
6932
|
if (meshId && nodeId) {
|
|
6890
6933
|
// preferInline so fast-forward can resolve inline-cache-only clone worktree nodes.
|
|
6891
6934
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
@@ -6897,6 +6940,7 @@ export class DaemonCommandRouter {
|
|
|
6897
6940
|
if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
|
|
6898
6941
|
submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
|
|
6899
6942
|
}
|
|
6943
|
+
allowAutoPublishSubmoduleMainCommits = mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true;
|
|
6900
6944
|
nodeDaemonId = typeof node?.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
6901
6945
|
}
|
|
6902
6946
|
// If the target node belongs to a remote daemon, forward the command there.
|
|
@@ -6921,6 +6965,9 @@ export class DaemonCommandRouter {
|
|
|
6921
6965
|
dryRun: args?.dryRun === true,
|
|
6922
6966
|
updateSubmodules: args?.updateSubmodules === true,
|
|
6923
6967
|
submoduleIgnorePaths,
|
|
6968
|
+
mode: args?.mode === 'push' ? 'push' : 'merge',
|
|
6969
|
+
pushSubmodules: args?.pushSubmodules === true,
|
|
6970
|
+
allowAutoPublishSubmoduleMainCommits,
|
|
6924
6971
|
}) as Promise<unknown>);
|
|
6925
6972
|
return result as CommandRouterResult;
|
|
6926
6973
|
}
|
package/src/git/git-status.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { GitRepoStatus, GitSubmoduleStatus, GitUpstreamFreshness } from './git-types.js';
|
|
1
|
+
import type { DaemonBuildBehind, GitRepoStatus, GitSubmoduleStatus, GitUpstreamFreshness } from './git-types.js';
|
|
2
2
|
import { GitCommandError, resolveGitRepository, runGit } from './git-executor.js';
|
|
3
|
+
import { getDaemonBuildInfo, type DaemonBuildInfo } from '../build-info.js';
|
|
3
4
|
|
|
4
5
|
type ResolvedGitRepo = { workspace: string; repoRoot: string | null; isGitRepo: boolean };
|
|
5
6
|
|
|
@@ -14,6 +15,12 @@ export interface GitStatusOptions {
|
|
|
14
15
|
* Callers should opt into this only for convergence-critical surfaces.
|
|
15
16
|
*/
|
|
16
17
|
refreshUpstream?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Test/override seam for the daemon build stamp used by the stale-build
|
|
20
|
+
* detector. Production callers omit this so the real baked-in build commit
|
|
21
|
+
* (getDaemonBuildInfo) is used.
|
|
22
|
+
*/
|
|
23
|
+
daemonBuildInfo?: DaemonBuildInfo;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
interface GitUpstreamProbe {
|
|
@@ -54,6 +61,8 @@ export async function getGitRepoStatus(
|
|
|
54
61
|
|| stashCount > 0
|
|
55
62
|
|| submoduleDirty;
|
|
56
63
|
|
|
64
|
+
const daemonBuildBehind = await detectDaemonBuildBehind(repo, submodules, options);
|
|
65
|
+
|
|
57
66
|
return {
|
|
58
67
|
workspace: repo.workspace,
|
|
59
68
|
repoRoot: repo.repoRoot,
|
|
@@ -78,6 +87,7 @@ export async function getGitRepoStatus(
|
|
|
78
87
|
stashCount,
|
|
79
88
|
lastCheckedAt,
|
|
80
89
|
submodules,
|
|
90
|
+
...(daemonBuildBehind ? { daemonBuildBehind } : {}),
|
|
81
91
|
};
|
|
82
92
|
} catch (error) {
|
|
83
93
|
if (error instanceof GitCommandError) {
|
|
@@ -91,6 +101,68 @@ export async function getGitRepoStatus(
|
|
|
91
101
|
}
|
|
92
102
|
}
|
|
93
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Detect whether the running daemon's build commit is a STRICT ancestor of this
|
|
106
|
+
* workspace's HEAD (root) or any of its submodules' HEAD. This surfaces the
|
|
107
|
+
* "merged a fix to main but the live daemon still ships the old bundle" gap:
|
|
108
|
+
* once the fix is committed, the workspace HEAD advances past the daemon's
|
|
109
|
+
* baked-in build commit, but the daemon keeps the old behavior until it is
|
|
110
|
+
* rebuilt/redeployed and restarted.
|
|
111
|
+
*
|
|
112
|
+
* Conservative by construction — returns undefined unless ancestry is provable:
|
|
113
|
+
* - build commit unknown → undefined
|
|
114
|
+
* - build commit not an object in this repo/submodule (different repo) → skip
|
|
115
|
+
* - build commit === HEAD (daemon is current) → undefined
|
|
116
|
+
* - build commit NOT an ancestor of HEAD (daemon ahead / diverged) → undefined
|
|
117
|
+
* Any git error is swallowed (no warning) so a flaky probe never over-warns.
|
|
118
|
+
*/
|
|
119
|
+
async function detectDaemonBuildBehind(
|
|
120
|
+
repo: ResolvedGitRepo,
|
|
121
|
+
submodules: GitSubmoduleStatus[] | undefined,
|
|
122
|
+
options: GitStatusOptions,
|
|
123
|
+
): Promise<DaemonBuildBehind | undefined> {
|
|
124
|
+
const build = options.daemonBuildInfo ?? getDaemonBuildInfo();
|
|
125
|
+
if (!build.commit || build.commit === 'unknown') return undefined;
|
|
126
|
+
|
|
127
|
+
// Check the root repo first, then each submodule. The daemon build commit is
|
|
128
|
+
// baked from the daemon-core (oss submodule) HEAD, so on an adhdev
|
|
129
|
+
// superproject worktree the match is expected on the `oss` submodule, not the
|
|
130
|
+
// root — checking both keeps the helper repo-agnostic.
|
|
131
|
+
const scopes: Array<{ scope: string; repoPath: string }> = [
|
|
132
|
+
{ scope: 'root', repoPath: repo.repoRoot || repo.workspace },
|
|
133
|
+
];
|
|
134
|
+
for (const sub of submodules || []) {
|
|
135
|
+
if (sub.repoPath && !sub.error) scopes.push({ scope: sub.path, repoPath: sub.repoPath });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const { scope, repoPath } of scopes) {
|
|
139
|
+
try {
|
|
140
|
+
// Build commit must be a real object in THIS repo, else it's a different repo.
|
|
141
|
+
await runGit(repoPath, ['cat-file', '-e', `${build.commit}^{commit}`], options);
|
|
142
|
+
const headResult = await runGit(repoPath, ['rev-parse', 'HEAD'], options);
|
|
143
|
+
const head = headResult.stdout.trim();
|
|
144
|
+
if (!head || head === build.commit) continue;
|
|
145
|
+
// Strict ancestor: build commit is reachable from HEAD but is not HEAD.
|
|
146
|
+
await runGit(repoPath, ['merge-base', '--is-ancestor', build.commit, 'HEAD'], options);
|
|
147
|
+
// No throw → build commit IS an ancestor of HEAD → daemon is behind.
|
|
148
|
+
return {
|
|
149
|
+
buildCommit: build.commit,
|
|
150
|
+
buildCommitShort: build.commitShort,
|
|
151
|
+
head,
|
|
152
|
+
scope,
|
|
153
|
+
warning:
|
|
154
|
+
`Live daemon was built from ${build.commitShort} which is behind ${scope === 'root' ? 'workspace' : scope} HEAD ${head.slice(0, 7)}. ` +
|
|
155
|
+
`Merged code is NOT live until the daemon is rebuilt/redeployed and restarted — a local dist rebuild alone does not update a cloud daemon.`,
|
|
156
|
+
};
|
|
157
|
+
} catch {
|
|
158
|
+
// cat-file / merge-base non-zero exit (commit absent or not an ancestor)
|
|
159
|
+
// or any git error → not a provable staleness for this scope; try next.
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
94
166
|
interface ParsedPorcelainStatus {
|
|
95
167
|
branch: string | null;
|
|
96
168
|
upstream: string | null;
|
package/src/git/git-types.ts
CHANGED
|
@@ -68,10 +68,30 @@ export interface GitRepoStatus extends GitRepoIdentity {
|
|
|
68
68
|
lastCheckedAt: number;
|
|
69
69
|
/** Submodule statuses when auto-discover is enabled */
|
|
70
70
|
submodules?: GitSubmoduleStatus[];
|
|
71
|
+
/**
|
|
72
|
+
* Set when the running daemon's build commit is a STRICT ancestor of this
|
|
73
|
+
* repo's HEAD (or a submodule HEAD) — i.e. the live daemon predates committed
|
|
74
|
+
* code in this workspace and is awaiting a deploy/restart to catch up.
|
|
75
|
+
* Omitted entirely when no staleness is provable (unknown build, commit not
|
|
76
|
+
* present in repo, or build commit == HEAD) to avoid over-warning.
|
|
77
|
+
*/
|
|
78
|
+
daemonBuildBehind?: DaemonBuildBehind;
|
|
71
79
|
error?: string;
|
|
72
80
|
reason?: GitFailureReason;
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
export interface DaemonBuildBehind {
|
|
84
|
+
/** Full build commit baked into the running daemon. */
|
|
85
|
+
buildCommit: string;
|
|
86
|
+
/** Short build commit. */
|
|
87
|
+
buildCommitShort: string;
|
|
88
|
+
/** HEAD commit the build commit is behind (repo or submodule). */
|
|
89
|
+
head: string;
|
|
90
|
+
/** Where the comparison matched: 'root' or the submodule path. */
|
|
91
|
+
scope: string;
|
|
92
|
+
warning: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
75
95
|
export type GitFileChangeStatus =
|
|
76
96
|
| 'added'
|
|
77
97
|
| 'modified'
|
package/src/index.ts
CHANGED
|
@@ -217,9 +217,10 @@ export { buildMeshLedgerReconciliationEvidence, buildMeshLedgerReplicaEvidence }
|
|
|
217
217
|
export type { AnyLedgerSlice, MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshLedgerReplicaStatus } from './mesh/mesh-ledger-reconciliation.js';
|
|
218
218
|
|
|
219
219
|
// ── Mesh Work Queue (GUPP) ──
|
|
220
|
-
export { enqueueTask, recordDirectDispatchTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, normalizeMeshCapabilityTags, insertDirectDispatch, getActiveDirectDispatches, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, markStaleDirectDispatches, recordMeshToolCall, assertNoDependencyCycle, hasPendingDependents, describeTaskDependencyState } from './mesh/mesh-work-queue.js';
|
|
220
|
+
export { enqueueTask, recordDirectDispatchTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, normalizeMeshCapabilityTags, insertDirectDispatch, getActiveDirectDispatches, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, markStaleDirectDispatches, deleteDirectDispatchesByTaskId, recordMeshToolCall, assertNoDependencyCycle, hasPendingDependents, describeTaskDependencyState } from './mesh/mesh-work-queue.js';
|
|
221
221
|
export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult, DirectDispatchRecord, MeshToolCallRateResult } from './mesh/mesh-work-queue.js';
|
|
222
|
-
export { buildCompactStaleDirectWorkSummary, buildMeshActiveWork, buildMeshActiveWorkSummary } from './mesh/mesh-active-work.js';
|
|
222
|
+
export { buildCompactStaleDirectWorkSummary, buildMeshActiveWork, buildMeshActiveWorkSummary, classifyStaleDirectForPrune, PRUNABLE_ORPHAN_STALE_REASONS } from './mesh/mesh-active-work.js';
|
|
223
|
+
export type { StaleDirectPruneClassification } from './mesh/mesh-active-work.js';
|
|
223
224
|
export type { MeshActiveWorkRecord, MeshActiveWorkStatus, MeshActiveWorkSummary, MeshActiveWorkSource, MeshStaleDirectWorkSummary } from './mesh/mesh-active-work.js';
|
|
224
225
|
export { buildMeshAsyncRefineJobs, summarizeMeshAsyncRefineJobs, STALE_TERMINAL_REFINE_WINDOW_MS, RECENT_TERMINAL_REFINE_CAP } from './mesh/mesh-refine-status.js';
|
|
225
226
|
export type { MeshAsyncRefineJobStatus, MeshAsyncRefineJobSummary, MeshAsyncRefineJobsSummary } from './mesh/mesh-refine-status.js';
|
|
@@ -306,6 +307,8 @@ export type {
|
|
|
306
307
|
export { DaemonStatusReporter } from './status/reporter.js';
|
|
307
308
|
export { buildSessionEntries, findCdpManager, hasCdpManager, isCdpConnected } from './status/builders.js';
|
|
308
309
|
export { buildStatusSnapshot, buildMachineInfo } from './status/snapshot.js';
|
|
310
|
+
export { getDaemonBuildInfo } from './build-info.js';
|
|
311
|
+
export type { DaemonBuildInfo } from './build-info.js';
|
|
309
312
|
export { normalizeManagedStatus, isManagedStatusWorking, isManagedStatusWaiting, normalizeActiveChatData } from './status/normalize.js';
|
|
310
313
|
export type { ManagedStatus } from './status/normalize.js';
|
|
311
314
|
export type { StatusSnapshotOptions, StatusSnapshot } from './status/snapshot.js';
|
|
@@ -408,6 +408,37 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
408
408
|
return { activeWork: records, staleDirectWork, staleDirectWorkNote, terminalDirectWork, summary };
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
+
/**
|
|
412
|
+
* staleReason strings (produced by sessionStatusFromNodes above) that indicate the original
|
|
413
|
+
* node/session is GONE from the live mesh — i.e. the staleDirect record is an orphaned ledger
|
|
414
|
+
* artifact, not active or recoverable work. These are the only reasons safe to prune from the
|
|
415
|
+
* active staleDirect surface. The "no provider acknowledgement" reason is deliberately excluded:
|
|
416
|
+
* those entries have a still-live node/session (staleDispatchUnacknowledged) and represent
|
|
417
|
+
* recoverable dispatch failures, never orphans.
|
|
418
|
+
*/
|
|
419
|
+
export const PRUNABLE_ORPHAN_STALE_REASONS: ReadonlySet<string> = new Set([
|
|
420
|
+
'direct task node is no longer in the live mesh',
|
|
421
|
+
'direct task session is not present in live session records',
|
|
422
|
+
'direct task has no node id',
|
|
423
|
+
]);
|
|
424
|
+
|
|
425
|
+
export type StaleDirectPruneClassification = 'prunable_orphan' | 'prunable_terminal' | 'preserve_unacknowledged' | 'preserve_active';
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Classify a direct-work record for the staleDirect prune path. Pure function — the prune tool
|
|
429
|
+
* uses this so the safety rules (never touch active work or recoverable unacknowledged dispatches)
|
|
430
|
+
* live next to the staleReason producers and are independently testable.
|
|
431
|
+
*/
|
|
432
|
+
export function classifyStaleDirectForPrune(
|
|
433
|
+
record: Pick<MeshActiveWorkRecord, 'staleReason' | 'staleDispatchUnacknowledged' | 'terminal'>,
|
|
434
|
+
opts: { includeTerminal?: boolean } = {},
|
|
435
|
+
): StaleDirectPruneClassification {
|
|
436
|
+
if (record.staleDispatchUnacknowledged === true) return 'preserve_unacknowledged';
|
|
437
|
+
if (record.terminal === true) return opts.includeTerminal ? 'prunable_terminal' : 'preserve_active';
|
|
438
|
+
if (record.staleReason && PRUNABLE_ORPHAN_STALE_REASONS.has(record.staleReason)) return 'prunable_orphan';
|
|
439
|
+
return 'preserve_active';
|
|
440
|
+
}
|
|
441
|
+
|
|
411
442
|
export function buildCompactStaleDirectWorkSummary(
|
|
412
443
|
staleDirectWork: MeshActiveWorkRecord[],
|
|
413
444
|
opts: { sampleLimit?: number; detailHint?: string; note?: string } = {},
|