@adhdev/daemon-core 0.9.82-rc.314 → 0.9.82-rc.316
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/cli-adapter-types.d.ts +23 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +9 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +279 -26
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +278 -30
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-work-queue.d.ts +22 -0
- package/dist/repo-mesh-types.d.ts +68 -0
- package/package.json +2 -2
- package/src/cli-adapter-types.ts +25 -0
- package/src/cli-adapters/provider-cli-adapter.ts +36 -1
- package/src/cli-adapters/provider-cli-shared.ts +19 -2
- package/src/commands/router.ts +10 -1
- package/src/config/mesh-config.ts +72 -1
- package/src/index.ts +7 -1
- package/src/mesh/mesh-work-queue.ts +89 -11
- package/src/providers/spec/native-history-executor.ts +104 -3
- package/src/repo-mesh-types.ts +112 -0
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import * as fs from 'node:fs';
|
|
22
22
|
import * as os from 'node:os';
|
|
23
23
|
import * as path from 'node:path';
|
|
24
|
+
import { LOG } from '../../logging/logger.js';
|
|
24
25
|
import type {
|
|
25
26
|
NativeHistoryConfig,
|
|
26
27
|
NativeHistoryJsonlSource,
|
|
@@ -138,8 +139,48 @@ function executeJsonl(src: NativeHistoryJsonlSource, input: NativeHistoryInput):
|
|
|
138
139
|
|| (requestedSessionId ? null : pickSessionBoundFileAcrossDateWindow(src.path, input, filePat, windowMs, sessionFloor, workspaceHint))
|
|
139
140
|
|| (requestedSessionId ? null : newestRecentFileAcrossDateWindow(src.path, input, filePat, windowMs, sessionFloor));
|
|
140
141
|
}
|
|
142
|
+
// Raw-workspace slug candidate: `resolved` derives its {cwd*} slug from
|
|
143
|
+
// fs.realpathSync(workspace). On Windows realpath normalizes the path
|
|
144
|
+
// (drive-letter case D:↔d:, \\?\ long-path prefix, junction expansion)
|
|
145
|
+
// so the slug can diverge from the one the CLI actually wrote — and the
|
|
146
|
+
// concrete path above then misses with ENOENT. Retry with the slug built
|
|
147
|
+
// from the RAW workspace string before falling back to a scan; it's the
|
|
148
|
+
// cheap fix when realpath divergence is the only problem.
|
|
149
|
+
if (!sourcePath && !hasDateTemplateSegment(src.path)) {
|
|
150
|
+
const resolvedRaw = expandPath(src.path, input, { skipWorkspaceRealpath: true });
|
|
151
|
+
if (resolvedRaw && resolvedRaw !== resolved) {
|
|
152
|
+
try {
|
|
153
|
+
const rawStat = fs.statSync(resolvedRaw);
|
|
154
|
+
if (rawStat.isFile()) sourcePath = resolvedRaw;
|
|
155
|
+
else if (rawStat.isDirectory()) {
|
|
156
|
+
sourcePath = pickExactSessionFile(resolvedRaw, filePat, requestedSessionId)
|
|
157
|
+
|| (requestedSessionId ? null : newestRecentFile(resolvedRaw, filePat, windowMs, sessionFloor));
|
|
158
|
+
}
|
|
159
|
+
} catch { /* raw slug also missed — fall through to scan */ }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Last-resort scan: the slug-derived directory missed entirely (the
|
|
163
|
+
// dominant Windows failure: realpath/raw slug both diverge from the CLI's
|
|
164
|
+
// on-disk project dir → 0 messages, no PTY fallback for native-source
|
|
165
|
+
// providers). When we have an exact session id, walk the projects root
|
|
166
|
+
// for `<sessionId>.jsonl` regardless of which project subdir holds it.
|
|
167
|
+
// The session id is a UUID, so basename matching is unambiguous; mirrors
|
|
168
|
+
// the standalone reader's scan (claude-cli-transcript.ts resolveTranscriptPath).
|
|
169
|
+
if (!sourcePath && requestedSessionId) {
|
|
170
|
+
sourcePath = scanProjectsRootForSessionFile(src.path, input, requestedSessionId);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!sourcePath) {
|
|
174
|
+
// Was silent before — a slug miss produced 0 messages with no trace, so
|
|
175
|
+
// a live read_chat returning empty was indistinguishable from "no file"
|
|
176
|
+
// vs "wrong path". Log the attempted concrete path + both slug variants
|
|
177
|
+
// so the failure mode is greppable in daemon logs.
|
|
178
|
+
const wsRaw = typeof input.workspace === 'string' ? input.workspace : '';
|
|
179
|
+
let wsReal = wsRaw;
|
|
180
|
+
try { if (wsRaw) wsReal = fs.realpathSync(wsRaw); } catch { /* keep raw */ }
|
|
181
|
+
LOG.debug('NativeHistory', `jsonl unresolved: tried=${JSON.stringify(resolved)} sessionId=${requestedSessionId || '(none)'} wsRaw=${JSON.stringify(wsRaw)} wsReal=${JSON.stringify(wsReal)} rawSlug=${JSON.stringify(claudeProjectDirName(wsRaw))} realSlug=${JSON.stringify(claudeProjectDirName(wsReal))} (concrete miss + raw-slug retry + projects scan all failed)`);
|
|
182
|
+
return null;
|
|
141
183
|
}
|
|
142
|
-
if (!sourcePath) return null;
|
|
143
184
|
|
|
144
185
|
const mtime = safeMtimeMs(sourcePath);
|
|
145
186
|
const lines = readJsonlLines(sourcePath);
|
|
@@ -278,7 +319,7 @@ function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput
|
|
|
278
319
|
// Path expansion + globbing
|
|
279
320
|
// ────────────────────────────────────────────────────────────────────────────
|
|
280
321
|
|
|
281
|
-
function expandPath(template: string, input: NativeHistoryInput): string | null {
|
|
322
|
+
function expandPath(template: string, input: NativeHistoryInput, opts?: { skipWorkspaceRealpath?: boolean }): string | null {
|
|
282
323
|
if (!template) return null;
|
|
283
324
|
let out = template;
|
|
284
325
|
if (out.startsWith('~/') || out === '~') {
|
|
@@ -301,7 +342,11 @@ function expandPath(template: string, input: NativeHistoryInput): string | null
|
|
|
301
342
|
// `-`. Realpath also handles aliases such as /tmp -> /private/tmp.
|
|
302
343
|
const workspaceRaw = input.workspace ?? '';
|
|
303
344
|
let workspaceResolved = workspaceRaw;
|
|
304
|
-
|
|
345
|
+
// The caller may request the RAW slug (skip realpath) to recover from
|
|
346
|
+
// Windows realpath normalization diverging the {cwd*} slug from the dir
|
|
347
|
+
// the CLI actually created. Default keeps realpath (handles /tmp ->
|
|
348
|
+
// /private/tmp aliasing that the CLI itself resolves on macOS).
|
|
349
|
+
if (workspaceRaw && !opts?.skipWorkspaceRealpath) {
|
|
305
350
|
try { workspaceResolved = fs.realpathSync(workspaceRaw); }
|
|
306
351
|
catch { /* path may not exist yet — keep the raw value */ }
|
|
307
352
|
}
|
|
@@ -333,6 +378,62 @@ function claudeProjectDirName(workspace: string): string {
|
|
|
333
378
|
return workspace.replace(/[^A-Za-z0-9_-]/g, '-');
|
|
334
379
|
}
|
|
335
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Last-resort lookup for a transcript by its exact session id, ignoring the
|
|
383
|
+
* per-cwd slug entirely. The slug-derived directory above can miss completely
|
|
384
|
+
* when the CLI's on-disk project dir disagrees with the slug we reconstruct
|
|
385
|
+
* (notably on Windows, where fs.realpathSync normalizes drive-letter case and
|
|
386
|
+
* adds a \\?\ prefix). Since the session id is a UUID, scanning the projects
|
|
387
|
+
* root for `<sessionId>.jsonl` is unambiguous.
|
|
388
|
+
*
|
|
389
|
+
* Derives the scan base from the template's segments up to (but excluding) the
|
|
390
|
+
* first one that references a per-session variable ({cwd*} or {session_id}) —
|
|
391
|
+
* e.g. `~/.claude/projects/{cwd_claude_project}/{session_id}.jsonl` → scan
|
|
392
|
+
* `~/.claude/projects`. Returns the matching file path, or null.
|
|
393
|
+
*/
|
|
394
|
+
function scanProjectsRootForSessionFile(template: string, input: NativeHistoryInput, requestedSessionId: string): string | null {
|
|
395
|
+
if (!requestedSessionId) return null;
|
|
396
|
+
// Resolve the leading static portion of the template (everything before the
|
|
397
|
+
// first {var} segment) into a concrete base directory.
|
|
398
|
+
let head = template;
|
|
399
|
+
if (head.startsWith('~/') || head === '~') head = path.join(os.homedir(), head.slice(2));
|
|
400
|
+
const segs = head.split('/');
|
|
401
|
+
const baseParts: string[] = [];
|
|
402
|
+
for (const seg of segs) {
|
|
403
|
+
if (/[{}*?]/.test(seg)) break;
|
|
404
|
+
baseParts.push(seg);
|
|
405
|
+
}
|
|
406
|
+
const base = baseParts.join('/');
|
|
407
|
+
if (!base) return null;
|
|
408
|
+
let baseStat: fs.Stats | null = null;
|
|
409
|
+
try { baseStat = fs.statSync(base); } catch { return null; }
|
|
410
|
+
if (!baseStat.isDirectory()) return null;
|
|
411
|
+
|
|
412
|
+
const needle = `${requestedSessionId.toLowerCase()}.jsonl`;
|
|
413
|
+
// Bounded walk: project layouts are <root>/<projectDir>/<uuid>.jsonl, so a
|
|
414
|
+
// shallow scan (root + one level of subdirs) suffices and avoids walking an
|
|
415
|
+
// unbounded tree. Check the root itself first, then each immediate subdir.
|
|
416
|
+
const dirsToScan: string[] = [base];
|
|
417
|
+
try {
|
|
418
|
+
for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
|
|
419
|
+
if (entry.isDirectory()) dirsToScan.push(path.join(base, entry.name));
|
|
420
|
+
}
|
|
421
|
+
} catch { /* readdir failed — fall back to scanning base only */ }
|
|
422
|
+
|
|
423
|
+
for (const dir of dirsToScan) {
|
|
424
|
+
let entries: fs.Dirent[];
|
|
425
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
426
|
+
for (const entry of entries) {
|
|
427
|
+
if (!entry.isFile()) continue;
|
|
428
|
+
if (entry.name.toLowerCase() !== needle) continue;
|
|
429
|
+
const found = path.join(dir, entry.name);
|
|
430
|
+
LOG.debug('NativeHistory', `jsonl scan-fallback hit: sessionId=${requestedSessionId} resolved via projects-root scan → ${JSON.stringify(found)} (slug-derived path missed; likely realpath/slug divergence)`);
|
|
431
|
+
return found;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
|
|
336
437
|
function globToRegex(pattern: string): RegExp {
|
|
337
438
|
// Minimal glob: `*` → `[^/]*`, `?` → `[^/]`, `.` → `\.`. Anchored.
|
|
338
439
|
const re = pattern
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -165,6 +165,106 @@ export function resolveNodeSchedulingPriority(
|
|
|
165
165
|
export const MESH_CONVERGE_REFINE_TAG = 'converge=refine';
|
|
166
166
|
export const MESH_CONVERGE_FAST_FORWARD_TAG = 'converge=fast_forward';
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Standard mesh resource-pool roles. These are the dashboard-provided defaults for
|
|
170
|
+
* the role=<x> routing tag (advertised by node policy.providerRoles and matched
|
|
171
|
+
* through nodeSatisfiesRequiredTags). A mesh is free to declare additional custom
|
|
172
|
+
* role strings in policy.taskAffinity; the dashboard role dropdown exposes the union
|
|
173
|
+
* of these four standards plus any config-declared roles. Roles are lowercased
|
|
174
|
+
* everywhere (the role=<x> tag is lowercased in roleCapabilityTags).
|
|
175
|
+
*/
|
|
176
|
+
export const STANDARD_MESH_ROLES = ['investigator', 'coder', 'validator', 'converger'] as const;
|
|
177
|
+
export type StandardMeshRole = typeof STANDARD_MESH_ROLES[number];
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Default taskMode → role mapping (all five task modes). A task enqueued with a
|
|
181
|
+
* given taskMode auto-routes to a node advertising the mapped role=<x> tag, unless
|
|
182
|
+
* the mesh overrides the mapping in policy.taskAffinity.byTaskMode. Keyed by the
|
|
183
|
+
* MeshTaskMode string literals (the canonical union lives in mesh-work-queue.ts;
|
|
184
|
+
* this map is keyed by string to avoid a types ↔ queue import cycle). Kept in sync
|
|
185
|
+
* with MeshTaskMode by the exhaustive lookup in resolveTaskAffinityRole.
|
|
186
|
+
*/
|
|
187
|
+
export const DEFAULT_TASKMODE_ROLE_MAP: Readonly<Record<string, StandardMeshRole>> = {
|
|
188
|
+
live_debug_readonly: 'investigator',
|
|
189
|
+
code_change: 'coder',
|
|
190
|
+
launch_app: 'coder',
|
|
191
|
+
validation: 'validator',
|
|
192
|
+
convergence: 'converger',
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Declarative task_mode → role affinity policy. When set, enqueueTask auto-injects a
|
|
197
|
+
* `role=<role>` required tag (see resolveTaskAffinityRequiredTags) so the task
|
|
198
|
+
* hard-filters onto nodes advertising that role — but only when the caller did NOT
|
|
199
|
+
* already pin the task with an explicit target_node_id or its own required_tags.
|
|
200
|
+
*
|
|
201
|
+
* - `enabled`: master switch. Defaults to true so the built-in DEFAULT_TASKMODE_ROLE_MAP
|
|
202
|
+
* takes effect even when a mesh declares no taskAffinity block at all. Set false to
|
|
203
|
+
* fully disable affinity routing (today's pre-affinity behavior).
|
|
204
|
+
* - `byTaskMode`: per-taskMode override of the default mapping. A role string here may
|
|
205
|
+
* be any of STANDARD_MESH_ROLES or a custom role declared by the operator. An empty
|
|
206
|
+
* string disables affinity for that one task mode.
|
|
207
|
+
* - `customRoles`: extra (non-standard) role strings to surface in the dashboard role
|
|
208
|
+
* dropdown. Routing itself does not require a role to be pre-declared here — any role
|
|
209
|
+
* referenced in byTaskMode is injected regardless; customRoles only widens the UI list.
|
|
210
|
+
*/
|
|
211
|
+
export interface RepoMeshTaskAffinityPolicy {
|
|
212
|
+
enabled?: boolean;
|
|
213
|
+
byTaskMode?: Record<string, string>;
|
|
214
|
+
customRoles?: string[];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Resolve the affinity role for a task mode against a mesh's taskAffinity policy.
|
|
219
|
+
* Returns the lowercased role string to inject as `role=<role>`, or null when no
|
|
220
|
+
* affinity applies (affinity disabled, an explicit empty override, or an unknown mode).
|
|
221
|
+
*
|
|
222
|
+
* Precedence: policy.byTaskMode[mode] (operator override) → DEFAULT_TASKMODE_ROLE_MAP[mode].
|
|
223
|
+
* A blank override string explicitly opts that mode out of affinity routing.
|
|
224
|
+
*/
|
|
225
|
+
export function resolveTaskAffinityRole(
|
|
226
|
+
taskMode: string | undefined,
|
|
227
|
+
policy: RepoMeshTaskAffinityPolicy | null | undefined,
|
|
228
|
+
): string | null {
|
|
229
|
+
if (!taskMode) return null;
|
|
230
|
+
if (policy?.enabled === false) return null;
|
|
231
|
+
const override = policy?.byTaskMode && Object.prototype.hasOwnProperty.call(policy.byTaskMode, taskMode)
|
|
232
|
+
? policy.byTaskMode[taskMode]
|
|
233
|
+
: undefined;
|
|
234
|
+
if (typeof override === 'string') {
|
|
235
|
+
const trimmed = override.trim().toLowerCase();
|
|
236
|
+
// An explicit blank override opts this task mode out of affinity routing.
|
|
237
|
+
return trimmed ? trimmed : null;
|
|
238
|
+
}
|
|
239
|
+
const fallback = DEFAULT_TASKMODE_ROLE_MAP[taskMode];
|
|
240
|
+
return fallback ?? null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Union of the standard roles plus any operator-declared roles (byTaskMode values +
|
|
245
|
+
* customRoles), lowercased and deduped, preserving standard-first ordering. Used by
|
|
246
|
+
* the dashboard role dropdown so operators can pick a standard or a config-declared
|
|
247
|
+
* custom role. Pure/UI-facing — does not gate routing.
|
|
248
|
+
*/
|
|
249
|
+
export function resolveMeshRoleOptions(policy: RepoMeshTaskAffinityPolicy | null | undefined): string[] {
|
|
250
|
+
const out: string[] = [...STANDARD_MESH_ROLES];
|
|
251
|
+
const seen = new Set<string>(out);
|
|
252
|
+
const add = (raw: unknown) => {
|
|
253
|
+
if (typeof raw !== 'string') return;
|
|
254
|
+
const role = raw.trim().toLowerCase();
|
|
255
|
+
if (!role || seen.has(role)) return;
|
|
256
|
+
seen.add(role);
|
|
257
|
+
out.push(role);
|
|
258
|
+
};
|
|
259
|
+
if (policy?.byTaskMode) {
|
|
260
|
+
for (const value of Object.values(policy.byTaskMode)) add(value);
|
|
261
|
+
}
|
|
262
|
+
if (Array.isArray(policy?.customRoles)) {
|
|
263
|
+
for (const value of policy.customRoles) add(value);
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
|
|
168
268
|
/**
|
|
169
269
|
* Resolve whether the load-balancing scheduler should auto-inject a
|
|
170
270
|
* `converge=refine` required tag onto code_change tasks so they hard-filter onto
|
|
@@ -220,6 +320,18 @@ export interface RepoMeshPolicy {
|
|
|
220
320
|
* Defaults to false: code_change routing is unchanged unless opted in.
|
|
221
321
|
*/
|
|
222
322
|
autoConvergeCodeChange?: boolean;
|
|
323
|
+
/**
|
|
324
|
+
* Declarative task_mode → role affinity routing. When present (and not disabled),
|
|
325
|
+
* enqueueTask auto-injects a `role=<role>` required tag for the task's taskMode so
|
|
326
|
+
* the work hard-filters onto nodes advertising that role. Explicit target_node_id
|
|
327
|
+
* routing and any caller-supplied required_tags are preserved (auto-injection only
|
|
328
|
+
* applies when the caller pinned neither). When the mapped role has no advertising
|
|
329
|
+
* node in the mesh, the task is NOT blocked — the role tag is skipped so the work
|
|
330
|
+
* falls back to ordinary least_loaded eligibility (soft affinity). Defaults to the
|
|
331
|
+
* built-in DEFAULT_TASKMODE_ROLE_MAP even when this block is omitted; set
|
|
332
|
+
* `{ enabled: false }` to fully restore pre-affinity routing.
|
|
333
|
+
*/
|
|
334
|
+
taskAffinity?: RepoMeshTaskAffinityPolicy;
|
|
223
335
|
/**
|
|
224
336
|
* Whether sessions spawned by mesh/coordinator policy should auto-open as visible
|
|
225
337
|
* dashboard tabs or start hidden. Defaults to 'visible' to preserve existing
|