@adhdev/daemon-core 0.9.82-rc.363 → 0.9.82-rc.365
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/cli-agent.d.ts +2 -0
- package/dist/commands/med-family/fast-forward.d.ts +2 -0
- package/dist/commands/med-family/ide.d.ts +10 -0
- package/dist/commands/med-family/index.d.ts +3 -0
- package/dist/commands/med-family/mesh-crud.d.ts +2 -0
- package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
- package/dist/commands/med-family/mesh-queue.d.ts +2 -0
- package/dist/commands/med-family/types.d.ts +116 -0
- package/dist/commands/router.d.ts +83 -0
- package/dist/index.js +1652 -1515
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1655 -1519
- package/dist/index.mjs.map +1 -1
- package/dist/providers/spec/fsm-driver.d.ts +27 -0
- package/dist/system/hash.d.ts +8 -0
- package/package.json +2 -2
- package/src/commands/cli-manager.ts +2 -1
- package/src/commands/med-family/cli-agent.ts +218 -0
- package/src/commands/med-family/fast-forward.ts +198 -0
- package/src/commands/med-family/ide.ts +163 -0
- package/src/commands/med-family/index.ts +35 -0
- package/src/commands/med-family/mesh-crud.ts +788 -0
- package/src/commands/med-family/mesh-host-pairing.ts +234 -0
- package/src/commands/med-family/mesh-queue.ts +131 -0
- package/src/commands/med-family/types.ts +120 -0
- package/src/commands/mesh-coordinator.ts +2 -2
- package/src/commands/router.ts +57 -1602
- package/src/config/mesh-config.ts +3 -2
- package/src/mesh/mesh-active-work.ts +59 -81
- package/src/providers/spec/fsm-driver.ts +56 -2
- package/src/system/hash.ts +23 -0
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
10
10
|
import { join } from 'path';
|
|
11
|
-
import {
|
|
11
|
+
import { randomBytes, randomUUID } from 'crypto';
|
|
12
|
+
import { shortHash } from '../system/hash.js';
|
|
12
13
|
import { getConfigDir } from './config.js';
|
|
13
14
|
import type {
|
|
14
15
|
LocalMeshConfig,
|
|
@@ -309,7 +310,7 @@ function normalizeManualHostAddress(hostAddress: string): string {
|
|
|
309
310
|
}
|
|
310
311
|
|
|
311
312
|
export function tokenIdForManualPairing(token: string): string {
|
|
312
|
-
return `tok_${
|
|
313
|
+
return `tok_${shortHash(token)}`;
|
|
313
314
|
}
|
|
314
315
|
|
|
315
316
|
function normalizeTokenExpiry(value: unknown): string | undefined {
|
|
@@ -208,6 +208,60 @@ function classifyDirectDispatch(params: {
|
|
|
208
208
|
return { ledgerOnlyStaleReason, isFreshUnacknowledged };
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Build a direct-dispatch MeshActiveWorkRecord from a `task_dispatched` ledger entry,
|
|
213
|
+
* matching it against the terminal ledger entries and live mesh nodes. Shared by the
|
|
214
|
+
* remote-ledger scan (inside the MeshRuntimeStore branch) and the full-ledger scan
|
|
215
|
+
* (standalone branch) — those two loops were previously byte-identical except for a
|
|
216
|
+
* single `dbTaskIds.has(taskId)` skip guard that stays in the caller. Returns the record
|
|
217
|
+
* plus `terminalRow` so the caller can route it into terminal/stale/active buckets.
|
|
218
|
+
*/
|
|
219
|
+
function buildLedgerDirectDispatchRecord(
|
|
220
|
+
dispatch: MeshLedgerEntry,
|
|
221
|
+
ctx: { terminals: MeshLedgerEntry[]; nodes: any[] | undefined; now: number },
|
|
222
|
+
): { record: MeshActiveWorkRecord; terminalRow: boolean } {
|
|
223
|
+
const taskId = directDispatchTaskId(dispatch);
|
|
224
|
+
const terminal = ctx.terminals
|
|
225
|
+
.filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
|
|
226
|
+
.find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
|
|
227
|
+
const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
|
|
228
|
+
const live = sessionStatusFromNodes(ctx.nodes, dispatch.nodeId, dispatch.sessionId);
|
|
229
|
+
const status = terminalStatus || live.status || 'assigned';
|
|
230
|
+
const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
|
|
231
|
+
const { ledgerOnlyStaleReason, isFreshUnacknowledged } = classifyDirectDispatch({
|
|
232
|
+
status,
|
|
233
|
+
isTerminalRow: terminalRow,
|
|
234
|
+
hasTerminalStatus: Boolean(terminalStatus),
|
|
235
|
+
liveStatus: live.status,
|
|
236
|
+
liveStaleReason: live.staleReason,
|
|
237
|
+
dispatchedToIdleSession: dispatch.payload?.dispatchedToIdleSession === true,
|
|
238
|
+
});
|
|
239
|
+
const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
|
|
240
|
+
const { title, summary } = summarizeMessage(message);
|
|
241
|
+
const record: MeshActiveWorkRecord = {
|
|
242
|
+
taskId,
|
|
243
|
+
source: 'direct',
|
|
244
|
+
status,
|
|
245
|
+
nodeId: dispatch.nodeId,
|
|
246
|
+
sessionId: dispatch.sessionId,
|
|
247
|
+
providerType: dispatch.providerType || readString(dispatch.payload?.providerType),
|
|
248
|
+
taskTitle: readString(dispatch.payload?.taskTitle) || title,
|
|
249
|
+
taskSummary: readString(dispatch.payload?.taskSummary) || summary,
|
|
250
|
+
message,
|
|
251
|
+
taskMode: readString(dispatch.payload?.taskMode),
|
|
252
|
+
createdAt: dispatch.timestamp,
|
|
253
|
+
updatedAt: terminal?.timestamp || dispatch.timestamp,
|
|
254
|
+
dispatchedAt: dispatch.timestamp,
|
|
255
|
+
elapsedMs: elapsedSince(dispatch.timestamp, ctx.now),
|
|
256
|
+
terminal: terminalRow,
|
|
257
|
+
terminalKind: terminal?.kind,
|
|
258
|
+
terminalAt: terminal?.timestamp,
|
|
259
|
+
staleReason: live.staleReason || ledgerOnlyStaleReason,
|
|
260
|
+
...(isFreshUnacknowledged ? { staleDispatchUnacknowledged: true } : {}),
|
|
261
|
+
};
|
|
262
|
+
return { record, terminalRow };
|
|
263
|
+
}
|
|
264
|
+
|
|
211
265
|
export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]): MeshActiveWorkSummary {
|
|
212
266
|
const statusCounts: Record<MeshActiveWorkStatus, number> = {
|
|
213
267
|
pending: 0,
|
|
@@ -324,51 +378,13 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
324
378
|
const ledgerEntries = (opts.ledgerEntries || []).slice().sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
325
379
|
const terminals = ledgerEntries.filter(entry => TERMINAL_LEDGER_KINDS.has(entry.kind) || entry.kind === 'task_approval_needed');
|
|
326
380
|
for (const dispatch of ledgerEntries.filter(isDirectDispatch)) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const terminal = terminals
|
|
330
|
-
.filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
|
|
331
|
-
.find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
|
|
332
|
-
const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
|
|
333
|
-
const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
|
|
334
|
-
const status = terminalStatus || live.status || 'assigned';
|
|
335
|
-
const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
|
|
336
|
-
const { ledgerOnlyStaleReason, isFreshUnacknowledged } = classifyDirectDispatch({
|
|
337
|
-
status,
|
|
338
|
-
isTerminalRow: terminalRow,
|
|
339
|
-
hasTerminalStatus: Boolean(terminalStatus),
|
|
340
|
-
liveStatus: live.status,
|
|
341
|
-
liveStaleReason: live.staleReason,
|
|
342
|
-
dispatchedToIdleSession: dispatch.payload?.dispatchedToIdleSession === true,
|
|
343
|
-
});
|
|
344
|
-
const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
|
|
345
|
-
const { title, summary } = summarizeMessage(message);
|
|
346
|
-
const record: MeshActiveWorkRecord = {
|
|
347
|
-
taskId,
|
|
348
|
-
source: 'direct',
|
|
349
|
-
status,
|
|
350
|
-
nodeId: dispatch.nodeId,
|
|
351
|
-
sessionId: dispatch.sessionId,
|
|
352
|
-
providerType: dispatch.providerType || readString(dispatch.payload?.providerType),
|
|
353
|
-
taskTitle: readString(dispatch.payload?.taskTitle) || title,
|
|
354
|
-
taskSummary: readString(dispatch.payload?.taskSummary) || summary,
|
|
355
|
-
message,
|
|
356
|
-
taskMode: readString(dispatch.payload?.taskMode),
|
|
357
|
-
createdAt: dispatch.timestamp,
|
|
358
|
-
updatedAt: terminal?.timestamp || dispatch.timestamp,
|
|
359
|
-
dispatchedAt: dispatch.timestamp,
|
|
360
|
-
elapsedMs: elapsedSince(dispatch.timestamp, now),
|
|
361
|
-
terminal: terminalRow,
|
|
362
|
-
terminalKind: terminal?.kind,
|
|
363
|
-
terminalAt: terminal?.timestamp,
|
|
364
|
-
staleReason: live.staleReason || ledgerOnlyStaleReason,
|
|
365
|
-
...(isFreshUnacknowledged ? { staleDispatchUnacknowledged: true } : {}),
|
|
366
|
-
};
|
|
381
|
+
if (dbTaskIds.has(directDispatchTaskId(dispatch))) continue; // already covered by MeshRuntimeStore path above
|
|
382
|
+
const { record, terminalRow } = buildLedgerDirectDispatchRecord(dispatch, { terminals, nodes: opts.nodes, now });
|
|
367
383
|
if (terminalRow) {
|
|
368
384
|
terminalDirectWork.push(record);
|
|
369
385
|
if (opts.includeTerminalDirect !== true) continue;
|
|
370
386
|
}
|
|
371
|
-
if (
|
|
387
|
+
if (record.staleReason && !terminalRow) {
|
|
372
388
|
staleDirectWork.push(record);
|
|
373
389
|
continue;
|
|
374
390
|
}
|
|
@@ -379,50 +395,12 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
379
395
|
const ledgerEntries = (opts.ledgerEntries || []).slice().sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
380
396
|
const terminals = ledgerEntries.filter(entry => TERMINAL_LEDGER_KINDS.has(entry.kind) || entry.kind === 'task_approval_needed');
|
|
381
397
|
for (const dispatch of ledgerEntries.filter(isDirectDispatch)) {
|
|
382
|
-
const
|
|
383
|
-
const terminal = terminals
|
|
384
|
-
.filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
|
|
385
|
-
.find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
|
|
386
|
-
const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
|
|
387
|
-
const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
|
|
388
|
-
const status = terminalStatus || live.status || 'assigned';
|
|
389
|
-
const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
|
|
390
|
-
const { ledgerOnlyStaleReason, isFreshUnacknowledged } = classifyDirectDispatch({
|
|
391
|
-
status,
|
|
392
|
-
isTerminalRow: terminalRow,
|
|
393
|
-
hasTerminalStatus: Boolean(terminalStatus),
|
|
394
|
-
liveStatus: live.status,
|
|
395
|
-
liveStaleReason: live.staleReason,
|
|
396
|
-
dispatchedToIdleSession: dispatch.payload?.dispatchedToIdleSession === true,
|
|
397
|
-
});
|
|
398
|
-
const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
|
|
399
|
-
const { title, summary } = summarizeMessage(message);
|
|
400
|
-
const record: MeshActiveWorkRecord = {
|
|
401
|
-
taskId,
|
|
402
|
-
source: 'direct',
|
|
403
|
-
status,
|
|
404
|
-
nodeId: dispatch.nodeId,
|
|
405
|
-
sessionId: dispatch.sessionId,
|
|
406
|
-
providerType: dispatch.providerType || readString(dispatch.payload?.providerType),
|
|
407
|
-
taskTitle: readString(dispatch.payload?.taskTitle) || title,
|
|
408
|
-
taskSummary: readString(dispatch.payload?.taskSummary) || summary,
|
|
409
|
-
message,
|
|
410
|
-
taskMode: readString(dispatch.payload?.taskMode),
|
|
411
|
-
createdAt: dispatch.timestamp,
|
|
412
|
-
updatedAt: terminal?.timestamp || dispatch.timestamp,
|
|
413
|
-
dispatchedAt: dispatch.timestamp,
|
|
414
|
-
elapsedMs: elapsedSince(dispatch.timestamp, now),
|
|
415
|
-
terminal: terminalRow,
|
|
416
|
-
terminalKind: terminal?.kind,
|
|
417
|
-
terminalAt: terminal?.timestamp,
|
|
418
|
-
staleReason: live.staleReason || ledgerOnlyStaleReason,
|
|
419
|
-
...(isFreshUnacknowledged ? { staleDispatchUnacknowledged: true } : {}),
|
|
420
|
-
};
|
|
398
|
+
const { record, terminalRow } = buildLedgerDirectDispatchRecord(dispatch, { terminals, nodes: opts.nodes, now });
|
|
421
399
|
if (terminalRow) {
|
|
422
400
|
terminalDirectWork.push(record);
|
|
423
401
|
if (opts.includeTerminalDirect !== true) continue;
|
|
424
402
|
}
|
|
425
|
-
if (
|
|
403
|
+
if (record.staleReason && !terminalRow) {
|
|
426
404
|
staleDirectWork.push(record);
|
|
427
405
|
continue;
|
|
428
406
|
}
|
|
@@ -294,6 +294,11 @@ export class FsmDriver implements ISpecDriver {
|
|
|
294
294
|
private lastWin32WriteAt = 0;
|
|
295
295
|
/** Pending paced chunk-write timer for a large win32 body (see writeWin32Body). */
|
|
296
296
|
private win32WriteTimer: ReturnType<typeof setTimeout> | null = null;
|
|
297
|
+
/** Timer driving the win32 verification-based modal-confirm CR resend loop (see
|
|
298
|
+
* scheduleWin32ModalConfirm). A lone CR that confirms an approval/picker choice
|
|
299
|
+
* is absorbed by ConPTY the same way a send_message submit CR is, so the confirm
|
|
300
|
+
* must be resent until the modal actually resolves (status leaves 'approval'). */
|
|
301
|
+
private win32ModalConfirmTimer: ReturnType<typeof setTimeout> | null = null;
|
|
297
302
|
|
|
298
303
|
private currentEval: CurrentEval | null = null;
|
|
299
304
|
private stateHistory: HistoryEntry[] = [];
|
|
@@ -413,6 +418,7 @@ export class FsmDriver implements ISpecDriver {
|
|
|
413
418
|
if (this.stallTimer) { clearTimeout(this.stallTimer); this.stallTimer = null; }
|
|
414
419
|
if (this.win32SubmitTimer) { clearTimeout(this.win32SubmitTimer); this.win32SubmitTimer = null; }
|
|
415
420
|
if (this.win32WriteTimer) { clearTimeout(this.win32WriteTimer); this.win32WriteTimer = null; }
|
|
421
|
+
if (this.win32ModalConfirmTimer) { clearTimeout(this.win32ModalConfirmTimer); this.win32ModalConfirmTimer = null; }
|
|
416
422
|
this.specWatcher?.close();
|
|
417
423
|
this.adapter.kill();
|
|
418
424
|
}
|
|
@@ -1140,10 +1146,58 @@ export class FsmDriver implements ISpecDriver {
|
|
|
1140
1146
|
// `{index}\r` → `\r`.
|
|
1141
1147
|
const confirm = (rule.key_for_index || '\r').replace(/\{index\}/g, '') || '\r';
|
|
1142
1148
|
if (nav) this.adapter.send_keys(nav);
|
|
1143
|
-
this.
|
|
1149
|
+
this.submitModalConfirm(confirm);
|
|
1144
1150
|
return;
|
|
1145
1151
|
}
|
|
1146
|
-
this.
|
|
1152
|
+
this.submitModalConfirm(btn.key);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Submit a modal-confirm key sequence (the choice key + its trailing CR).
|
|
1157
|
+
*
|
|
1158
|
+
* On win32 the trailing CR is the SAME lone-CR-swallow case as a send_message
|
|
1159
|
+
* submit: ConPTY can absorb a single CR as a literal newline instead of a
|
|
1160
|
+
* confirm, so the approval/picker modal never resolves and the FSM flaps
|
|
1161
|
+
* approval↔busy while auto-approve keeps firing into the void (APPROVESTUCK).
|
|
1162
|
+
* So we split any non-CR prefix (e.g. the "1" of "1\r") off, write it once, and
|
|
1163
|
+
* resend the CR on a fixed cadence until the modal actually resolves (status
|
|
1164
|
+
* leaves 'approval'). Non-win32 keeps the single direct write — its CR submits
|
|
1165
|
+
* on the first try.
|
|
1166
|
+
*/
|
|
1167
|
+
private submitModalConfirm(keys: string): void {
|
|
1168
|
+
if (process.platform !== 'win32') {
|
|
1169
|
+
this.adapter.send_keys(keys);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const m = /^([\s\S]*?)([\r\n]+)$/.exec(keys);
|
|
1173
|
+
const prefix = m ? m[1] : keys;
|
|
1174
|
+
const cr = m ? m[2] : '';
|
|
1175
|
+
if (prefix) this.adapter.send_keys(prefix);
|
|
1176
|
+
if (!cr) return;
|
|
1177
|
+
this.scheduleWin32ModalConfirm(cr);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* win32 modal-confirm CR resend loop. Mirrors scheduleWin32Submit's phase-2
|
|
1182
|
+
* verified resend, but gated on still being IN a modal (status 'approval')
|
|
1183
|
+
* rather than still idle: the first CR fires immediately, then resends every
|
|
1184
|
+
* WIN32_SUBMIT_RESEND_GAP_MS while the FSM is still showing the modal, up to
|
|
1185
|
+
* WIN32_SUBMIT_MAX_RESENDS. The instant the modal resolves (status flips to
|
|
1186
|
+
* generating/idle) we stop, so no stray CR leaks into the next turn's composer.
|
|
1187
|
+
*/
|
|
1188
|
+
private scheduleWin32ModalConfirm(submitKey: string): void {
|
|
1189
|
+
if (this.win32ModalConfirmTimer) { clearTimeout(this.win32ModalConfirmTimer); this.win32ModalConfirmTimer = null; }
|
|
1190
|
+
const fire = (attempt: number): void => {
|
|
1191
|
+
this.win32ModalConfirmTimer = null;
|
|
1192
|
+
this.adapter.send_keys(submitKey);
|
|
1193
|
+
if (attempt + 1 >= WIN32_SUBMIT_MAX_RESENDS) return;
|
|
1194
|
+
this.win32ModalConfirmTimer = setTimeout(() => {
|
|
1195
|
+
// Left the modal → it resolved; stop resending.
|
|
1196
|
+
if (this.currentStatus() !== 'approval') { this.win32ModalConfirmTimer = null; return; }
|
|
1197
|
+
fire(attempt + 1);
|
|
1198
|
+
}, WIN32_SUBMIT_RESEND_GAP_MS);
|
|
1199
|
+
};
|
|
1200
|
+
fire(0);
|
|
1147
1201
|
}
|
|
1148
1202
|
|
|
1149
1203
|
private handleAttachImage(blob: string, mime: string): void {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared SHA-256 hashing helpers for daemon-core (Node runtime).
|
|
3
|
+
*
|
|
4
|
+
* daemon-core runs under Node, so it uses `node:crypto` directly and must NOT
|
|
5
|
+
* import the server's WebCrypto-based `utils/crypto.ts` (different runtime +
|
|
6
|
+
* cross-package dependency direction). Output is byte-identical to that helper
|
|
7
|
+
* for the same input — lowercase hex SHA-256.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
|
|
11
|
+
/** Full lowercase hex SHA-256 digest of `input`. */
|
|
12
|
+
export function sha256Hex(input: string): string {
|
|
13
|
+
return createHash('sha256').update(input).digest('hex');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Truncated SHA-256 hex digest — the first `length` hex chars (default 16).
|
|
18
|
+
* Used for stable short identifiers (workspace hashes, token ids, coordinator
|
|
19
|
+
* home dirs) where collision risk at 16 hex chars (64 bits) is negligible.
|
|
20
|
+
*/
|
|
21
|
+
export function shortHash(input: string, length = 16): string {
|
|
22
|
+
return sha256Hex(input).slice(0, length);
|
|
23
|
+
}
|