@adhdev/daemon-core 0.9.82-rc.332 → 0.9.82-rc.334
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 +10 -0
- package/dist/index.js +709 -589
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +698 -578
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-runtime-store.d.ts +18 -1
- package/dist/mesh/mesh-work-queue.d.ts +1 -0
- package/dist/mesh/refine-config.d.ts +1 -0
- package/dist/system/load-better-sqlite3.d.ts +21 -0
- package/package.json +2 -2
- package/src/commands/router.ts +65 -10
- package/src/mesh/mesh-events-coordinator.ts +4 -0
- package/src/mesh/mesh-runtime-store.ts +61 -13
- package/src/mesh/mesh-work-queue.ts +18 -4
- package/src/mesh/refine-config.ts +23 -3
- package/src/mesh/worktree-bootstrap-config.ts +6 -1
- package/src/providers/native-history/hermes-cli-transcript.ts +2 -2
- package/src/providers/spec/native-history-executor.ts +2 -2
- package/src/system/load-better-sqlite3.ts +68 -0
|
@@ -82,7 +82,24 @@ export declare class MeshRuntimeStore {
|
|
|
82
82
|
sessionId?: string;
|
|
83
83
|
message: string;
|
|
84
84
|
}>;
|
|
85
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Resolve the `assigned` queue row a completion event belongs to.
|
|
87
|
+
*
|
|
88
|
+
* Clock-skew safety (C2): a completion event's `occurredAtIso` carries the
|
|
89
|
+
* REMOTE WORKER's clock, while `updated_at` carries the COORDINATOR's clock
|
|
90
|
+
* (set at assignment and re-bumped on every mutation). For a remote node,
|
|
91
|
+
* coordinator-clock > worker-clock skew used to make an `updated_at <= occurredAt`
|
|
92
|
+
* filter return nothing, stranding the finished task as `assigned` forever.
|
|
93
|
+
*
|
|
94
|
+
* We therefore NEVER filter completion-matching on the mutable `updated_at`:
|
|
95
|
+
* 1. If `taskId` is given, match the exact `assigned` row by id (no time filter).
|
|
96
|
+
* 2. Otherwise a session holds at most one `assigned` task — match it without a
|
|
97
|
+
* time filter. If several exist (shouldn't normally), disambiguate by the
|
|
98
|
+
* IMMUTABLE `dispatchTimestamp`: latest `dispatchTimestamp <= occurredAt`,
|
|
99
|
+
* and if skew makes ALL of them later than `occurredAt`, fall back to the
|
|
100
|
+
* most-recent `dispatchTimestamp` rather than returning null.
|
|
101
|
+
*/
|
|
102
|
+
findAssignedBySession(meshId: string, sessionId: string, occurredAtIso?: string, taskId?: string): MeshWorkQueueEntry | null;
|
|
86
103
|
private toRow;
|
|
87
104
|
insertDirectDispatch(entry: {
|
|
88
105
|
taskId: string;
|
|
@@ -225,6 +225,7 @@ export declare function requeueTask(meshId: string, taskId: string, opts?: {
|
|
|
225
225
|
*/
|
|
226
226
|
export declare function updateSessionTaskStatus(meshId: string, sessionId: string, status: MeshTaskStatus, opts?: {
|
|
227
227
|
occurredAt?: string;
|
|
228
|
+
taskId?: string;
|
|
228
229
|
}): MeshWorkQueueEntry | null;
|
|
229
230
|
/**
|
|
230
231
|
* M1-3: true when at least one pending task is waiting on the given task.
|
|
@@ -194,6 +194,7 @@ export declare const MESH_REFINE_CONFIG_SCHEMA: {
|
|
|
194
194
|
};
|
|
195
195
|
};
|
|
196
196
|
export declare function isMeshConfigRecord(value: unknown): value is Record<string, unknown>;
|
|
197
|
+
export declare function tokenizeCommandString(command: string): string[] | null;
|
|
197
198
|
export declare function normalizeMeshCommandConfig(entry: unknown, source: string): {
|
|
198
199
|
command?: MeshRefineValidationCommandPlan;
|
|
199
200
|
rejected?: Record<string, unknown>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type BetterSqlite3 from 'better-sqlite3';
|
|
2
|
+
/**
|
|
3
|
+
* Load the `better-sqlite3` constructor in a way that survives every runtime the
|
|
4
|
+
* daemon ships in.
|
|
5
|
+
*
|
|
6
|
+
* The naive `typeof require === 'function' ? require : createRequire(import.meta.url)`
|
|
7
|
+
* is unsafe inside the daemon-cloud bundle: esbuild emits CJS output but, for any
|
|
8
|
+
* chunk that touches `import.meta.url`, it shims the local `require` with a stub
|
|
9
|
+
* that THROWS `Dynamic require of "..." is not supported`. That stub is still
|
|
10
|
+
* `typeof === 'function'`, so the ternary picks it and the call throws — it never
|
|
11
|
+
* reaches the `createRequire` fallback. The failure surfaced as `mesh_send_task`
|
|
12
|
+
* crashing the whole tool call instead of gracefully degrading.
|
|
13
|
+
*
|
|
14
|
+
* The robust approach is to ATTEMPT the real `require` and CATCH the esbuild stub's
|
|
15
|
+
* throw, then fall back to `createRequire`. We try multiple resolution bases so the
|
|
16
|
+
* load works whether the code runs as:
|
|
17
|
+
* - a genuine CJS module (bare `require` works),
|
|
18
|
+
* - an esbuild CJS bundle with the throwing `require` shim (createRequire(import.meta.url)),
|
|
19
|
+
* - a pure ESM module where `require` is undefined (createRequire(import.meta.url)).
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadBetterSqlite3(): typeof BetterSqlite3;
|
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.334",
|
|
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.334",
|
|
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
|
@@ -93,6 +93,7 @@ import * as fs from 'fs';
|
|
|
93
93
|
import { execFileSync } from 'node:child_process';
|
|
94
94
|
import { normalizeInteractivePromptResponse } from '../providers/types/interactive-prompt.js';
|
|
95
95
|
import { workingDirBasename } from '../providers/working-dir.js';
|
|
96
|
+
import { resolveWin32Executable } from '../cli-adapters/resolve-executable.js';
|
|
96
97
|
|
|
97
98
|
type ReleaseChannel = 'stable' | 'preview';
|
|
98
99
|
const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
|
|
@@ -1675,6 +1676,8 @@ type MeshRefineValidationSummary = {
|
|
|
1675
1676
|
skippedReason?: string;
|
|
1676
1677
|
failureKind?: string;
|
|
1677
1678
|
failureCode?: string;
|
|
1679
|
+
/** Human-readable cause when failureKind === 'spawn_resolution_failed' (win32 .cmd shim, etc). */
|
|
1680
|
+
spawnResolutionError?: string;
|
|
1678
1681
|
timeoutMs: number;
|
|
1679
1682
|
outputLimitBytes: number;
|
|
1680
1683
|
configSource?: string;
|
|
@@ -1895,6 +1898,32 @@ function truncateValidationOutput(value: unknown): string {
|
|
|
1895
1898
|
return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
|
|
1896
1899
|
}
|
|
1897
1900
|
|
|
1901
|
+
/**
|
|
1902
|
+
* A spawn-resolution failure is when the executable itself could not be found by
|
|
1903
|
+
* the OS spawn boundary — `spawn <cmd> ENOENT` — as opposed to the command
|
|
1904
|
+
* running and exiting non-zero. On win32 this is the .cmd-shim case: libuv's
|
|
1905
|
+
* spawn search appends only .com/.exe, so a bare `npm`/`npx`/`tsc` (which are
|
|
1906
|
+
* .cmd shims) ENOENTs even though it is installed. It carries no stderr, so it
|
|
1907
|
+
* must be detected by error.code/syscall, not by string-matching output.
|
|
1908
|
+
*/
|
|
1909
|
+
export function isSpawnResolutionError(error: any): boolean {
|
|
1910
|
+
if (!error) return false;
|
|
1911
|
+
if (error.code === 'ENOENT' && typeof error.syscall === 'string' && error.syscall.startsWith('spawn')) return true;
|
|
1912
|
+
// Fall back to code alone: execFile sets syscall on the spawn boundary error,
|
|
1913
|
+
// but guard for environments/mocks that only surface the code.
|
|
1914
|
+
return error.code === 'ENOENT' && (error.syscall === undefined || String(error.syscall).startsWith('spawn'));
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
export function describeSpawnError(error: any, command: string, spawnResolutionFailed: boolean): string {
|
|
1918
|
+
if (spawnResolutionFailed) {
|
|
1919
|
+
const hint = process.platform === 'win32'
|
|
1920
|
+
? ' On Windows, npm-family commands (npm/npx/tsc/vitest) are .cmd shims that the bare-command spawn search does not resolve; configure an absolute path or ensure the command is on PATH.'
|
|
1921
|
+
: '';
|
|
1922
|
+
return `Could not resolve executable "${command}" (spawn ENOENT).${hint}`;
|
|
1923
|
+
}
|
|
1924
|
+
return String(error?.message || error);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1898
1927
|
function recordMeshRefineStage(
|
|
1899
1928
|
stages: Array<Record<string, unknown>>,
|
|
1900
1929
|
stage: string,
|
|
@@ -3119,8 +3148,13 @@ async function runMeshRefineValidationGate(
|
|
|
3119
3148
|
const startedAt = Date.now();
|
|
3120
3149
|
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
3121
3150
|
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
3151
|
+
// On win32, libuv's spawn search only appends .com/.exe (not .cmd/.bat),
|
|
3152
|
+
// so a bare `npm`/`npx`/`tsc` (which are .cmd shims) throws spawn ENOENT.
|
|
3153
|
+
// Resolve to an absolute path via the same helper the PTY path uses
|
|
3154
|
+
// (no-op on non-win32 and when the command is already absolute).
|
|
3155
|
+
const resolvedCommand = resolveWin32Executable(candidate.command);
|
|
3122
3156
|
try {
|
|
3123
|
-
const result = await execFileAsync(
|
|
3157
|
+
const result = await execFileAsync(resolvedCommand, candidate.args, {
|
|
3124
3158
|
cwd,
|
|
3125
3159
|
encoding: 'utf8',
|
|
3126
3160
|
timeout,
|
|
@@ -3129,16 +3163,19 @@ async function runMeshRefineValidationGate(
|
|
|
3129
3163
|
});
|
|
3130
3164
|
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
3131
3165
|
} catch (error: any) {
|
|
3166
|
+
const spawnResolutionFailed = isSpawnResolutionError(error);
|
|
3132
3167
|
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
3133
3168
|
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
3134
3169
|
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
3135
3170
|
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
3136
|
-
|
|
3171
|
+
...(spawnResolutionFailed
|
|
3172
|
+
? { failureKind: 'spawn_resolution_failed', resolvedCommand }
|
|
3173
|
+
: { failureKind: 'dependency_bootstrap_failed' }),
|
|
3137
3174
|
}));
|
|
3138
|
-
summary.bootstrap = { stage: 'failed', error:
|
|
3175
|
+
summary.bootstrap = { stage: 'failed', error: describeSpawnError(error, candidate.command, spawnResolutionFailed) };
|
|
3139
3176
|
summary.status = 'failed';
|
|
3140
|
-
summary.failureKind = 'dependency_bootstrap_failed';
|
|
3141
|
-
summary.failureCode = 'dependency_bootstrap_failed';
|
|
3177
|
+
summary.failureKind = spawnResolutionFailed ? 'spawn_resolution_failed' : 'dependency_bootstrap_failed';
|
|
3178
|
+
summary.failureCode = spawnResolutionFailed ? 'spawn_resolution_failed' : 'dependency_bootstrap_failed';
|
|
3142
3179
|
return summary;
|
|
3143
3180
|
}
|
|
3144
3181
|
}
|
|
@@ -3162,8 +3199,11 @@ async function runMeshRefineValidationGate(
|
|
|
3162
3199
|
summary.failureCode = 'missing_dependencies';
|
|
3163
3200
|
return summary;
|
|
3164
3201
|
}
|
|
3202
|
+
// See the bootstrap loop above: resolve the win32 .cmd shim to an
|
|
3203
|
+
// absolute path before handing it to the spawn boundary.
|
|
3204
|
+
const resolvedCommand = resolveWin32Executable(candidate.command);
|
|
3165
3205
|
try {
|
|
3166
|
-
const result = await execFileAsync(
|
|
3206
|
+
const result = await execFileAsync(resolvedCommand, candidate.args, {
|
|
3167
3207
|
cwd,
|
|
3168
3208
|
encoding: 'utf8',
|
|
3169
3209
|
timeout,
|
|
@@ -3172,16 +3212,28 @@ async function runMeshRefineValidationGate(
|
|
|
3172
3212
|
});
|
|
3173
3213
|
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
3174
3214
|
} catch (error: any) {
|
|
3215
|
+
// ENOENT check first: a spawn-resolution failure ("spawn npm ENOENT")
|
|
3216
|
+
// carries no stderr and would otherwise fall through to an
|
|
3217
|
+
// unclassified generic failure. Classify it distinctly so the
|
|
3218
|
+
// coordinator surfaces the real cause (win32 .cmd resolution).
|
|
3219
|
+
const spawnResolutionFailed = isSpawnResolutionError(error);
|
|
3175
3220
|
const stderr = truncateValidationOutput(error?.stderr || error?.message);
|
|
3176
|
-
const missingDependencyFailure =
|
|
3221
|
+
const missingDependencyFailure = !spawnResolutionFailed
|
|
3222
|
+
&& /Cannot find module|MODULE_NOT_FOUND|node_modules|command not found|not found/i.test(stderr);
|
|
3177
3223
|
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
3178
3224
|
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
3179
3225
|
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
3180
3226
|
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
3181
|
-
...(
|
|
3227
|
+
...(spawnResolutionFailed
|
|
3228
|
+
? { failureKind: 'spawn_resolution_failed', resolvedCommand }
|
|
3229
|
+
: missingDependencyFailure ? { failureKind: 'missing_dependencies' } : {}),
|
|
3182
3230
|
}));
|
|
3183
3231
|
summary.status = 'failed';
|
|
3184
|
-
if (
|
|
3232
|
+
if (spawnResolutionFailed) {
|
|
3233
|
+
summary.failureKind = 'spawn_resolution_failed';
|
|
3234
|
+
summary.failureCode = 'spawn_resolution_failed';
|
|
3235
|
+
summary.spawnResolutionError = describeSpawnError(error, candidate.command, true);
|
|
3236
|
+
} else if (missingDependencyFailure) {
|
|
3185
3237
|
summary.failureKind = 'missing_dependencies';
|
|
3186
3238
|
summary.failureCode = 'missing_dependencies';
|
|
3187
3239
|
}
|
|
@@ -4776,7 +4828,10 @@ export class DaemonCommandRouter {
|
|
|
4776
4828
|
? 'Refinery validation dependencies are missing; merge/refine was not attempted. Configure validation.bootstrapCommands if Refinery should bootstrap dependencies before validation.'
|
|
4777
4829
|
: validationSummary.failureCode === 'dependency_bootstrap_failed'
|
|
4778
4830
|
? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
|
|
4779
|
-
:
|
|
4831
|
+
: validationSummary.failureCode === 'spawn_resolution_failed'
|
|
4832
|
+
? (validationSummary.spawnResolutionError
|
|
4833
|
+
|| 'Refinery validation command could not be spawned (executable not found); merge/refine was not attempted.')
|
|
4834
|
+
: 'Refinery validation gate failed; merge/refine was not attempted.';
|
|
4780
4835
|
if (!firstFailedCmd) return base;
|
|
4781
4836
|
const cmdName = typeof firstFailedCmd.displayCommand === 'string' ? firstFailedCmd.displayCommand
|
|
4782
4837
|
: typeof firstFailedCmd.command === 'string'
|
|
@@ -1534,8 +1534,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1534
1534
|
}
|
|
1535
1535
|
|
|
1536
1536
|
function markSessionTerminal(sessionId: string, outcome: 'completed' | 'failed', occurredAtMs?: number | null): { id?: string } | null {
|
|
1537
|
+
// C2: prefer an exact taskId match when the completion event carries one —
|
|
1538
|
+
// it's immune to coordinator↔worker clock skew that can hide the assigned row.
|
|
1539
|
+
const eventTaskId = readNonEmptyString(args.metadataEvent.taskId) || undefined;
|
|
1537
1540
|
const task = updateSessionTaskStatus(args.meshId, sessionId, outcome, {
|
|
1538
1541
|
occurredAt: occurredAtMs != null ? new Date(occurredAtMs).toISOString() : undefined,
|
|
1542
|
+
taskId: eventTaskId,
|
|
1539
1543
|
});
|
|
1540
1544
|
updateDirectDispatchStatus(args.meshId, sessionId, outcome);
|
|
1541
1545
|
markSessionDeliveriesTerminal(args.meshId, sessionId, outcome);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, statSync } from 'fs';
|
|
2
2
|
import { dirname, join } from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { loadBetterSqlite3 } from '../system/load-better-sqlite3.js';
|
|
4
4
|
import { getLedgerDir } from './mesh-ledger.js';
|
|
5
5
|
import { nodeSatisfiesRequiredTags } from './mesh-work-queue.js';
|
|
6
6
|
import type { MeshTaskStatus, MeshWorkQueueEntry } from './mesh-work-queue.js';
|
|
@@ -11,10 +11,7 @@ let DatabaseCtor: typeof BetterSqlite3 | undefined;
|
|
|
11
11
|
|
|
12
12
|
function loadDatabaseCtor(): typeof BetterSqlite3 {
|
|
13
13
|
if (DatabaseCtor) return DatabaseCtor;
|
|
14
|
-
|
|
15
|
-
? require
|
|
16
|
-
: createRequire(import.meta.url);
|
|
17
|
-
DatabaseCtor = runtimeRequire('better-sqlite3') as typeof BetterSqlite3;
|
|
14
|
+
DatabaseCtor = loadBetterSqlite3() as typeof BetterSqlite3;
|
|
18
15
|
return DatabaseCtor;
|
|
19
16
|
}
|
|
20
17
|
|
|
@@ -701,15 +698,66 @@ export class MeshRuntimeStore {
|
|
|
701
698
|
});
|
|
702
699
|
}
|
|
703
700
|
|
|
704
|
-
|
|
701
|
+
/**
|
|
702
|
+
* Resolve the `assigned` queue row a completion event belongs to.
|
|
703
|
+
*
|
|
704
|
+
* Clock-skew safety (C2): a completion event's `occurredAtIso` carries the
|
|
705
|
+
* REMOTE WORKER's clock, while `updated_at` carries the COORDINATOR's clock
|
|
706
|
+
* (set at assignment and re-bumped on every mutation). For a remote node,
|
|
707
|
+
* coordinator-clock > worker-clock skew used to make an `updated_at <= occurredAt`
|
|
708
|
+
* filter return nothing, stranding the finished task as `assigned` forever.
|
|
709
|
+
*
|
|
710
|
+
* We therefore NEVER filter completion-matching on the mutable `updated_at`:
|
|
711
|
+
* 1. If `taskId` is given, match the exact `assigned` row by id (no time filter).
|
|
712
|
+
* 2. Otherwise a session holds at most one `assigned` task — match it without a
|
|
713
|
+
* time filter. If several exist (shouldn't normally), disambiguate by the
|
|
714
|
+
* IMMUTABLE `dispatchTimestamp`: latest `dispatchTimestamp <= occurredAt`,
|
|
715
|
+
* and if skew makes ALL of them later than `occurredAt`, fall back to the
|
|
716
|
+
* most-recent `dispatchTimestamp` rather than returning null.
|
|
717
|
+
*/
|
|
718
|
+
findAssignedBySession(
|
|
719
|
+
meshId: string,
|
|
720
|
+
sessionId: string,
|
|
721
|
+
occurredAtIso?: string,
|
|
722
|
+
taskId?: string,
|
|
723
|
+
): MeshWorkQueueEntry | null {
|
|
705
724
|
this.ensureLegacyQueueMigrated(meshId);
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
725
|
+
|
|
726
|
+
// 1. Exact taskId match — robust against clock skew and stale rows.
|
|
727
|
+
if (taskId) {
|
|
728
|
+
const row = this.db.prepare(
|
|
729
|
+
`SELECT payload FROM mesh_queue WHERE mesh_id = ? AND assigned_session_id = ? AND status = 'assigned' AND id = ? LIMIT 1`
|
|
730
|
+
).get(meshId, sessionId, taskId) as { payload: string } | undefined;
|
|
731
|
+
if (row) return JSON.parse(row.payload) as MeshWorkQueueEntry;
|
|
732
|
+
// Fall through to session-based matching if the id didn't line up
|
|
733
|
+
// (e.g. event carried a stale/foreign taskId).
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// 2. Session-based match WITHOUT the mutable updated_at filter.
|
|
737
|
+
const rows = this.db.prepare(
|
|
738
|
+
`SELECT payload FROM mesh_queue WHERE mesh_id = ? AND assigned_session_id = ? AND status = 'assigned'`
|
|
739
|
+
).all(meshId, sessionId) as Array<{ payload: string }>;
|
|
740
|
+
if (rows.length === 0) return null;
|
|
741
|
+
|
|
742
|
+
const entries = rows
|
|
743
|
+
.map(r => { try { return JSON.parse(r.payload) as MeshWorkQueueEntry; } catch { return null; } })
|
|
744
|
+
.filter((e): e is MeshWorkQueueEntry => e !== null);
|
|
745
|
+
if (entries.length === 0) return null;
|
|
746
|
+
if (entries.length === 1) return entries[0];
|
|
747
|
+
|
|
748
|
+
// Multiple assigned rows for one session: disambiguate by the immutable
|
|
749
|
+
// dispatchTimestamp (falling back to updated_at only for legacy rows that
|
|
750
|
+
// predate dispatchTimestamp). We use these to ORDER, never to FILTER —
|
|
751
|
+
// so a skewed occurredAt can never drop the live row to null.
|
|
752
|
+
const orderKey = (e: MeshWorkQueueEntry) => e.dispatchTimestamp ?? e.updatedAt ?? '';
|
|
753
|
+
const byDispatchDesc = [...entries].sort((a, b) => orderKey(b).localeCompare(orderKey(a)));
|
|
754
|
+
if (occurredAtIso) {
|
|
755
|
+
const atOrBefore = byDispatchDesc.find(e => orderKey(e) <= occurredAtIso);
|
|
756
|
+
if (atOrBefore) return atOrBefore;
|
|
757
|
+
}
|
|
758
|
+
// Skew made every dispatch later than occurredAt — fall back to the
|
|
759
|
+
// most-recently dispatched row rather than stranding the completion.
|
|
760
|
+
return byDispatchDesc[0];
|
|
713
761
|
}
|
|
714
762
|
|
|
715
763
|
private toRow(entry: MeshWorkQueueEntry): Record<string, unknown> {
|
|
@@ -4,6 +4,7 @@ import type { RepoMeshDaemonRole } from '../repo-mesh-types.js';
|
|
|
4
4
|
import { MESH_CONVERGE_REFINE_TAG, resolveAutoConvergeCodeChange } from '../repo-mesh-types.js';
|
|
5
5
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
6
6
|
import { getMesh } from '../config/mesh-config.js';
|
|
7
|
+
import { LOG } from '../logging/logger.js';
|
|
7
8
|
|
|
8
9
|
export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
|
|
9
10
|
export type MeshActiveTaskStatus = Extract<MeshTaskStatus, 'pending' | 'assigned'>;
|
|
@@ -880,14 +881,27 @@ export function updateSessionTaskStatus(
|
|
|
880
881
|
meshId: string,
|
|
881
882
|
sessionId: string,
|
|
882
883
|
status: MeshTaskStatus,
|
|
883
|
-
opts?: { occurredAt?: string },
|
|
884
|
+
opts?: { occurredAt?: string; taskId?: string },
|
|
884
885
|
): MeshWorkQueueEntry | null {
|
|
885
886
|
return withQueueLock(meshId, () => {
|
|
887
|
+
const store = MeshRuntimeStore.getInstance();
|
|
886
888
|
const occurredAtIso = opts?.occurredAt ? new Date(opts.occurredAt).toISOString() : undefined;
|
|
887
|
-
const entry =
|
|
888
|
-
if (!entry)
|
|
889
|
+
const entry = store.findAssignedBySession(meshId, sessionId, occurredAtIso, opts?.taskId);
|
|
890
|
+
if (!entry) {
|
|
891
|
+
// C2: the silent null here is exactly what stranded a finished task as
|
|
892
|
+
// `assigned` for 19 minutes. If the session still has an assigned row we
|
|
893
|
+
// failed to resolve, surface it loudly instead of dropping the completion.
|
|
894
|
+
const assignedRows = store.getActiveAssignmentDetails(meshId)
|
|
895
|
+
.filter(r => r.sessionId === sessionId);
|
|
896
|
+
if (assignedRows.length > 0) {
|
|
897
|
+
LOG.warn('MeshQueue', `No assigned queue row matched completion for mesh ${meshId} session ${sessionId} `
|
|
898
|
+
+ `(taskId=${opts?.taskId ?? 'none'}, occurredAt=${occurredAtIso ?? 'none'}); `
|
|
899
|
+
+ `${assignedRows.length} assigned row(s) exist: ${assignedRows.map(r => r.id).join(',')}`);
|
|
900
|
+
}
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
889
903
|
entry.status = status;
|
|
890
|
-
|
|
904
|
+
store.updateQueueEntry(entry);
|
|
891
905
|
if (DEPENDENCY_FAILURE_TERMINALS.has(status)) propagateDependencyFailure(meshId, entry.id);
|
|
892
906
|
return entry;
|
|
893
907
|
});
|
|
@@ -164,15 +164,35 @@ export function isMeshConfigRecord(value: unknown): value is Record<string, unkn
|
|
|
164
164
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
// True shell metacharacters — never allowed anywhere in a config command, on
|
|
168
|
+
// any platform. Note: backslash is deliberately NOT in this set; it is handled
|
|
169
|
+
// separately (allowed only in the win32 executable token).
|
|
170
|
+
const SHELL_METACHAR_RE = /[;&|<>`$\n\r'"]/;
|
|
171
|
+
// Per-token allowlist for argument tokens and non-win32 executable tokens.
|
|
172
|
+
const SAFE_TOKEN_RE = /^[A-Za-z0-9_@./:=+-]+$/;
|
|
173
|
+
// Executable token on win32: same as SAFE_TOKEN_RE but also permits backslash so
|
|
174
|
+
// an absolute path like C:\Users\me\AppData\Roaming\npm\npm.cmd is accepted.
|
|
175
|
+
const SAFE_WIN32_EXEC_TOKEN_RE = /^[A-Za-z0-9_@./:=+\\-]+$/;
|
|
176
|
+
|
|
177
|
+
export function tokenizeCommandString(command: string): string[] | null {
|
|
168
178
|
const trimmed = command.trim();
|
|
169
179
|
if (!trimmed) return null;
|
|
170
180
|
// Explicit config may name any executable, but the Refinery never invokes a shell.
|
|
171
181
|
// Reject shell syntax, quotes and substitutions so config cannot smuggle a compound command.
|
|
172
|
-
|
|
182
|
+
// Backslash is the one exception (handled per-token below): rejecting it here
|
|
183
|
+
// would block legitimate win32 absolute .cmd paths, but it is never a shell
|
|
184
|
+
// metacharacter in the no-shell execFile boundary we run under.
|
|
185
|
+
if (SHELL_METACHAR_RE.test(trimmed)) return null;
|
|
173
186
|
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
174
187
|
if (!tokens.length) return null;
|
|
175
|
-
|
|
188
|
+
const isWin32 = process.platform === 'win32';
|
|
189
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
190
|
+
// Backslash is permitted ONLY in the executable (first) token, ONLY on
|
|
191
|
+
// win32. Every other token (and all tokens off win32) uses the strict
|
|
192
|
+
// allowlist with no backslash.
|
|
193
|
+
const re = (isWin32 && i === 0) ? SAFE_WIN32_EXEC_TOKEN_RE : SAFE_TOKEN_RE;
|
|
194
|
+
if (!re.test(tokens[i])) return null;
|
|
195
|
+
}
|
|
176
196
|
return tokens;
|
|
177
197
|
}
|
|
178
198
|
|
|
@@ -4,6 +4,7 @@ import { execFile } from 'node:child_process';
|
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import * as yaml from 'js-yaml';
|
|
7
|
+
import { resolveWin32Executable } from '../cli-adapters/resolve-executable.js';
|
|
7
8
|
import {
|
|
8
9
|
isMeshConfigRecord,
|
|
9
10
|
normalizeMeshCommandConfig,
|
|
@@ -268,8 +269,12 @@ export async function runMeshWorktreeBootstrap(mesh: any, workspace: string): Pr
|
|
|
268
269
|
const cwd = command.cwd ? pathResolve(workspace, command.cwd) : workspace;
|
|
269
270
|
const startedAt = Date.now();
|
|
270
271
|
state.lastCommand = command.displayCommand;
|
|
272
|
+
// On win32 a bare npm/npx/tsc is a .cmd shim that libuv's spawn search
|
|
273
|
+
// (which appends only .com/.exe) cannot resolve → spawn ENOENT. Resolve
|
|
274
|
+
// to an absolute path first (no-op on non-win32 / already-absolute).
|
|
275
|
+
const resolvedCommand = resolveWin32Executable(command.command);
|
|
271
276
|
try {
|
|
272
|
-
const result = await execFileAsync(
|
|
277
|
+
const result = await execFileAsync(resolvedCommand, command.args, {
|
|
273
278
|
cwd,
|
|
274
279
|
encoding: 'utf8',
|
|
275
280
|
timeout: command.timeoutMs || DEFAULT_TIMEOUT_MS,
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import * as fs from 'node:fs';
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import * as os from 'node:os';
|
|
22
|
+
import { loadBetterSqlite3 } from '../../system/load-better-sqlite3.js';
|
|
22
23
|
|
|
23
24
|
export interface NativeHistoryMessage {
|
|
24
25
|
id: string;
|
|
@@ -61,8 +62,7 @@ function statMtimeMs(p: string): number {
|
|
|
61
62
|
function openDb(): any | null {
|
|
62
63
|
if (!fs.existsSync(HERMES_STATE_DB)) return null;
|
|
63
64
|
try {
|
|
64
|
-
|
|
65
|
-
const Database = require('better-sqlite3');
|
|
65
|
+
const Database = loadBetterSqlite3();
|
|
66
66
|
return new Database(HERMES_STATE_DB, { readonly: true, fileMustExist: true });
|
|
67
67
|
} catch {
|
|
68
68
|
return null;
|
|
@@ -22,6 +22,7 @@ import * as fs from 'node:fs';
|
|
|
22
22
|
import * as os from 'node:os';
|
|
23
23
|
import * as path from 'node:path';
|
|
24
24
|
import { LOG } from '../../logging/logger.js';
|
|
25
|
+
import { loadBetterSqlite3 } from '../../system/load-better-sqlite3.js';
|
|
25
26
|
import type {
|
|
26
27
|
NativeHistoryConfig,
|
|
27
28
|
NativeHistoryJsonlSource,
|
|
@@ -253,8 +254,7 @@ function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput
|
|
|
253
254
|
|
|
254
255
|
let Database: any;
|
|
255
256
|
try {
|
|
256
|
-
|
|
257
|
-
Database = require('better-sqlite3');
|
|
257
|
+
Database = loadBetterSqlite3();
|
|
258
258
|
} catch { return null; }
|
|
259
259
|
|
|
260
260
|
let db: any;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import type BetterSqlite3 from 'better-sqlite3';
|
|
3
|
+
|
|
4
|
+
let cached: typeof BetterSqlite3 | undefined;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Load the `better-sqlite3` constructor in a way that survives every runtime the
|
|
8
|
+
* daemon ships in.
|
|
9
|
+
*
|
|
10
|
+
* The naive `typeof require === 'function' ? require : createRequire(import.meta.url)`
|
|
11
|
+
* is unsafe inside the daemon-cloud bundle: esbuild emits CJS output but, for any
|
|
12
|
+
* chunk that touches `import.meta.url`, it shims the local `require` with a stub
|
|
13
|
+
* that THROWS `Dynamic require of "..." is not supported`. That stub is still
|
|
14
|
+
* `typeof === 'function'`, so the ternary picks it and the call throws — it never
|
|
15
|
+
* reaches the `createRequire` fallback. The failure surfaced as `mesh_send_task`
|
|
16
|
+
* crashing the whole tool call instead of gracefully degrading.
|
|
17
|
+
*
|
|
18
|
+
* The robust approach is to ATTEMPT the real `require` and CATCH the esbuild stub's
|
|
19
|
+
* throw, then fall back to `createRequire`. We try multiple resolution bases so the
|
|
20
|
+
* load works whether the code runs as:
|
|
21
|
+
* - a genuine CJS module (bare `require` works),
|
|
22
|
+
* - an esbuild CJS bundle with the throwing `require` shim (createRequire(import.meta.url)),
|
|
23
|
+
* - a pure ESM module where `require` is undefined (createRequire(import.meta.url)).
|
|
24
|
+
*/
|
|
25
|
+
export function loadBetterSqlite3(): typeof BetterSqlite3 {
|
|
26
|
+
if (cached) return cached;
|
|
27
|
+
|
|
28
|
+
const errors: unknown[] = [];
|
|
29
|
+
|
|
30
|
+
// 1) Real CJS require, when present and not the esbuild throwing shim.
|
|
31
|
+
if (typeof require === 'function') {
|
|
32
|
+
try {
|
|
33
|
+
cached = require('better-sqlite3') as typeof BetterSqlite3;
|
|
34
|
+
return cached;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// Either the esbuild "Dynamic require is not supported" shim, or a
|
|
37
|
+
// genuine resolution failure. Fall through to createRequire.
|
|
38
|
+
errors.push(e);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2) createRequire anchored at this module's URL (works in ESM and in esbuild
|
|
43
|
+
// CJS bundles, where import.meta.url is rewritten to a usable value).
|
|
44
|
+
try {
|
|
45
|
+
const metaUrl = typeof import.meta?.url === 'string' ? import.meta.url : undefined;
|
|
46
|
+
if (metaUrl) {
|
|
47
|
+
cached = createRequire(metaUrl)('better-sqlite3') as typeof BetterSqlite3;
|
|
48
|
+
return cached;
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {
|
|
51
|
+
errors.push(e);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3) Last resort: createRequire anchored at the current working directory.
|
|
55
|
+
try {
|
|
56
|
+
cached = createRequire(`${process.cwd()}/__adhdev_better_sqlite3_loader__.js`)(
|
|
57
|
+
'better-sqlite3',
|
|
58
|
+
) as typeof BetterSqlite3;
|
|
59
|
+
return cached;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
errors.push(e);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const detail = errors
|
|
65
|
+
.map((e) => (e instanceof Error ? e.message : String(e)))
|
|
66
|
+
.join('; ');
|
|
67
|
+
throw new Error(`Failed to load better-sqlite3: ${detail}`);
|
|
68
|
+
}
|