dominds 1.25.12 → 1.25.13

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.
@@ -47,5 +47,6 @@ export declare function supplySideDialogResponseToAssignedAskerIfPendingV2(args:
47
47
  callId: string;
48
48
  replyCallName: 'replyTellask' | 'replyTellaskSessionless' | 'replyTellaskBack';
49
49
  };
50
+ allowExplicitReplyWithoutAssignmentAnchor?: boolean;
50
51
  scheduleDrive: ScheduleDriveFn;
51
52
  }): Promise<boolean>;
@@ -655,18 +655,36 @@ async function supplySideDialogResponseToAssignedAskerIfPendingV2(args) {
655
655
  status: sideDialog.status,
656
656
  });
657
657
  if (!assignmentAnchorRef) {
658
- log_1.log.debug('Skip assigned Type B response supply before updated assignment is rendered locally', undefined, {
659
- rootId: sideDialog.mainDialog.id.rootId,
660
- sideDialogId: sideDialog.id.selfId,
661
- askerDialogId: askerDialog.id.selfId,
662
- callId: activeCalleeDispatch.callId,
663
- responseGenseq,
664
- });
665
- return false;
658
+ const replyResolution = args.replyResolution;
659
+ if (args.allowExplicitReplyWithoutAssignmentAnchor === true &&
660
+ args.deliveryMode === 'reply_tool' &&
661
+ replyResolution !== undefined) {
662
+ log_1.log.warn('Delivering assigned Type B reply without local assignment anchor', undefined, {
663
+ rootId: sideDialog.mainDialog.id.rootId,
664
+ sideDialogId: sideDialog.id.selfId,
665
+ askerDialogId: askerDialog.id.selfId,
666
+ callId: activeCalleeDispatch.callId,
667
+ replyCallId: replyResolution.callId,
668
+ replyCallName: replyResolution.replyCallName,
669
+ responseCourse: sideDialog.currentCourse,
670
+ responseGenseq,
671
+ });
672
+ }
673
+ else {
674
+ log_1.log.debug('Skip assigned Type B response supply before updated assignment is rendered locally', undefined, {
675
+ rootId: sideDialog.mainDialog.id.rootId,
676
+ sideDialogId: sideDialog.id.selfId,
677
+ askerDialogId: askerDialog.id.selfId,
678
+ callId: activeCalleeDispatch.callId,
679
+ responseGenseq,
680
+ });
681
+ return false;
682
+ }
666
683
  }
667
- if (sideDialog.currentCourse < assignmentAnchorRef.course ||
668
- (sideDialog.currentCourse === assignmentAnchorRef.course &&
669
- responseGenseq < assignmentAnchorRef.genseq)) {
684
+ if (assignmentAnchorRef !== undefined &&
685
+ (sideDialog.currentCourse < assignmentAnchorRef.course ||
686
+ (sideDialog.currentCourse === assignmentAnchorRef.course &&
687
+ responseGenseq < assignmentAnchorRef.genseq))) {
670
688
  log_1.log.debug('Skip assigned stale Type B response supply from before latest local assignment', undefined, {
671
689
  rootId: sideDialog.mainDialog.id.rootId,
672
690
  sideDialogId: sideDialog.id.selfId,
@@ -1957,13 +1957,16 @@ async function executeReplyTellaskCall(args) {
1957
1957
  sideDialog: args.dlg,
1958
1958
  responseText: args.call.replyContent,
1959
1959
  responseGenseq: genseq,
1960
+ deliveryMode: 'reply_tool',
1960
1961
  replyResolution: {
1961
1962
  callId: args.call.callId,
1962
1963
  replyCallName: args.call.callName,
1963
1964
  },
1965
+ allowExplicitReplyWithoutAssignmentAnchor: true,
1964
1966
  scheduleDrive: args.callbacks.scheduleDrive,
1965
1967
  });
1966
1968
  if (!supplied) {
1969
+ await persistence_1.DialogPersistence.clearPendingReplyDeliveryForCall(args.dlg.id, args.call.callId, args.dlg.status);
1967
1970
  return {
1968
1971
  delivered: false,
1969
1972
  messages: [
@@ -660,6 +660,7 @@ export declare class DialogPersistence {
660
660
  static loadActiveTellaskReplyObligation(dialogId: DialogID, status?: DialogStatusKind): Promise<TellaskReplyDirective | undefined>;
661
661
  static markReplyDeliveryDelivered(dialogId: DialogID, replyCallId: string, deliveredAt: string, status?: DialogStatusKind): Promise<void>;
662
662
  static markReplyDeliveryToolResultRecorded(dialogId: DialogID, replyCallId: string, status?: DialogStatusKind): Promise<void>;
663
+ static clearPendingReplyDeliveryForCall(dialogId: DialogID, replyCallId: string, status?: DialogStatusKind): Promise<void>;
663
664
  static lookupRecordedTellaskCall(dialogId: DialogID, callId: string, status?: DialogStatusKind): Promise<DialogTellaskCallState['calls'][number] | undefined>;
664
665
  static recordTellaskCall(dialogId: DialogID, record: TellaskCallRecord, course: number, status?: DialogStatusKind): Promise<void>;
665
666
  static hasRecordedTellaskResult(dialogId: DialogID, callId: string, status?: DialogStatusKind): Promise<boolean>;
@@ -8431,6 +8431,24 @@ class DialogPersistence {
8431
8431
  };
8432
8432
  }, status);
8433
8433
  }
8434
+ static async clearPendingReplyDeliveryForCall(dialogId, replyCallId, status = 'running') {
8435
+ await this.mutateDialogLatest(dialogId, (previous) => {
8436
+ const replyDelivery = previous.replyDelivery;
8437
+ if (!replyDelivery ||
8438
+ replyDelivery.replyCallId !== replyCallId ||
8439
+ replyDelivery.status !== 'pending') {
8440
+ return { kind: 'noop' };
8441
+ }
8442
+ return {
8443
+ kind: 'patch',
8444
+ patch: {
8445
+ replyDelivery: undefined,
8446
+ nextStep: removeNextStepTrigger(previous.nextStep, (trigger) => trigger.kind === 'reply_delivery_recovery' &&
8447
+ trigger.replyDeliveryId === replyDelivery.replyDeliveryId),
8448
+ },
8449
+ };
8450
+ }, status);
8451
+ }
8434
8452
  static async lookupRecordedTellaskCall(dialogId, callId, status = 'running') {
8435
8453
  const normalizedCallId = callId.trim();
8436
8454
  if (normalizedCallId === '') {
@@ -66,6 +66,7 @@ const id_1 = require("../utils/id");
66
66
  const taskdoc_search_1 = require("../utils/taskdoc-search");
67
67
  const taskdoc_search_worker_client_1 = require("../utils/taskdoc-search-worker-client");
68
68
  const create_dialog_contract_1 = require("./create-dialog-contract");
69
+ const dialog_forensics_routes_1 = require("./dialog-forensics-routes");
69
70
  const dominds_runtime_status_1 = require("./dominds-runtime-status");
70
71
  const dominds_self_update_1 = require("./dominds-self-update");
71
72
  const mime_types_1 = require("./mime-types");
@@ -1063,6 +1064,12 @@ async function handleApiRoute(req, res, pathname, context) {
1063
1064
  if (pathname === '/api/info' && req.method === 'GET') {
1064
1065
  return await handleGetRuntimeInfo(res, context);
1065
1066
  }
1067
+ if (pathname === '/api/dialog-forensics.zip' && req.method === 'GET') {
1068
+ return await (0, dialog_forensics_routes_1.handleDialogForensicsZipRoute)(req.url, res);
1069
+ }
1070
+ if (pathname === '/api/rtws/raw' && req.method === 'GET') {
1071
+ return await handleGetRtwsRaw(req, res);
1072
+ }
1066
1073
  if (pathname === '/api/dominds/self-update' && req.method === 'POST') {
1067
1074
  const body = await readRequestBody(req);
1068
1075
  let parsed;
@@ -1407,6 +1414,64 @@ async function handleApiRoute(req, res, pathname, context) {
1407
1414
  return true;
1408
1415
  }
1409
1416
  }
1417
+ function encodeContentDispositionFilenameStar(name) {
1418
+ return encodeURIComponent(name).replace(/['()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
1419
+ }
1420
+ function contentDispositionInlineFilename(name) {
1421
+ const fallback = name.replace(/[^\x20-\x7e]|[\r\n\\"]/g, '_');
1422
+ return `inline; filename="${fallback}"; filename*=UTF-8''${encodeContentDispositionFilenameStar(name)}`;
1423
+ }
1424
+ async function handleGetRtwsRaw(req, res) {
1425
+ const urlObj = new URL(req.url ?? '', 'http://127.0.0.1');
1426
+ const rawPath = urlObj.searchParams.get('path');
1427
+ if (rawPath === null || rawPath.trim() === '') {
1428
+ respondJson(res, 400, { success: false, error: '`path` is required' });
1429
+ return true;
1430
+ }
1431
+ const pathRel = normalizeRtwsRelativePath(rawPath, { allowRoot: false });
1432
+ if (pathRel === null) {
1433
+ respondJson(res, 400, { success: false, error: 'Invalid rtws path' });
1434
+ return true;
1435
+ }
1436
+ try {
1437
+ const resolved = await resolveWorkspacePreviewPath(pathRel);
1438
+ const stat = await promises_1.default.stat(resolved.candidateAbsPath);
1439
+ if (!stat.isFile()) {
1440
+ respondJson(res, 400, {
1441
+ success: false,
1442
+ error: 'Path must resolve to a file',
1443
+ path: pathRel,
1444
+ });
1445
+ return true;
1446
+ }
1447
+ const headBytes = await readFileHead(resolved.candidateAbsPath, 512);
1448
+ const mimeType = (0, mime_types_1.sniffMimeType)(pathRel, headBytes);
1449
+ const data = await promises_1.default.readFile(resolved.candidateAbsPath);
1450
+ res.writeHead(200, {
1451
+ 'Content-Type': mimeType,
1452
+ 'Content-Length': data.byteLength,
1453
+ 'Content-Disposition': contentDispositionInlineFilename(path.basename(pathRel)),
1454
+ 'Cache-Control': 'no-store',
1455
+ 'X-Content-Type-Options': 'nosniff',
1456
+ });
1457
+ res.end(data);
1458
+ return true;
1459
+ }
1460
+ catch (error) {
1461
+ const code = getErrorCode(error);
1462
+ if (code === 'ENOENT') {
1463
+ respondJson(res, 404, { success: false, error: 'Path not found', path: pathRel });
1464
+ return true;
1465
+ }
1466
+ if (code === 'OUTSIDE_RTWS') {
1467
+ respondJson(res, 403, { success: false, error: 'Path resolves outside rtws', path: pathRel });
1468
+ return true;
1469
+ }
1470
+ log.error('Failed to read rtws raw file', error, { path: pathRel });
1471
+ respondJson(res, 500, { success: false, error: 'Failed to read rtws raw file', path: pathRel });
1472
+ return true;
1473
+ }
1474
+ }
1410
1475
  function resolveRtwsDiligencePath(lang) {
1411
1476
  const parsed = typeof lang === 'string' ? (0, language_1.normalizeLanguageCode)(lang) : null;
1412
1477
  if (parsed === 'zh')
@@ -0,0 +1,2 @@
1
+ import type { ServerResponse } from 'http';
2
+ export declare function handleDialogForensicsZipRoute(reqUrl: string | undefined, res: ServerResponse): Promise<boolean>;
@@ -0,0 +1,549 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.handleDialogForensicsZipRoute = handleDialogForensicsZipRoute;
40
+ const time_1 = require("@longrun-ai/kernel/utils/time");
41
+ const promises_1 = __importDefault(require("fs/promises"));
42
+ const path = __importStar(require("path"));
43
+ const FORENSICS_STATUS_DIRS = ['run', 'done', 'archive', 'malformed'];
44
+ class BadForensicsRequestError extends Error {
45
+ }
46
+ class ForensicsNotFoundError extends Error {
47
+ }
48
+ function badRequest(message) {
49
+ throw new BadForensicsRequestError(message);
50
+ }
51
+ function notFound(message) {
52
+ throw new ForensicsNotFoundError(message);
53
+ }
54
+ const ROOT_DIAGNOSTIC_FILE_NAMES = [
55
+ 'latest.yaml',
56
+ 'dialog.yaml',
57
+ 'drive-watch.json',
58
+ 'active-callees.json',
59
+ 'sideDialog-responses.json',
60
+ 'q4h.yaml',
61
+ 'backend-drive-stalls.jsonl',
62
+ 'wake-queue.jsonl',
63
+ 'asker-stack.jsonl',
64
+ ];
65
+ const CRC32_TABLE = (() => {
66
+ const table = [];
67
+ for (let i = 0; i < 256; i += 1) {
68
+ let value = i;
69
+ for (let bit = 0; bit < 8; bit += 1) {
70
+ value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
71
+ }
72
+ table.push(value >>> 0);
73
+ }
74
+ return table;
75
+ })();
76
+ function respondJson(res, statusCode, data) {
77
+ res.writeHead(statusCode, {
78
+ 'Content-Type': 'application/json',
79
+ 'Cache-Control': 'no-store',
80
+ });
81
+ res.end(JSON.stringify(data));
82
+ }
83
+ function isPositiveIntegerText(raw) {
84
+ if (raw === '')
85
+ return false;
86
+ for (const char of raw) {
87
+ if (char < '0' || char > '9')
88
+ return false;
89
+ }
90
+ return true;
91
+ }
92
+ function parsePositiveInteger(raw, fieldName) {
93
+ if (raw === null || raw.trim() === '')
94
+ return undefined;
95
+ if (!isPositiveIntegerText(raw)) {
96
+ badRequest(`${fieldName} must be a positive integer`);
97
+ }
98
+ const value = Number(raw);
99
+ if (!Number.isSafeInteger(value) || value <= 0) {
100
+ badRequest(`${fieldName} must be a positive integer`);
101
+ }
102
+ return value;
103
+ }
104
+ function normalizeStatus(raw) {
105
+ if (raw === null || raw.trim() === '')
106
+ return undefined;
107
+ switch (raw) {
108
+ case 'run':
109
+ case 'running':
110
+ return 'run';
111
+ case 'done':
112
+ case 'completed':
113
+ return 'done';
114
+ case 'archive':
115
+ case 'archived':
116
+ return 'archive';
117
+ case 'malformed':
118
+ return 'malformed';
119
+ default:
120
+ badRequest('status must be one of run, running, done, completed, archive, archived, malformed');
121
+ }
122
+ }
123
+ function normalizeMode(raw) {
124
+ if (raw === null || raw.trim() === '')
125
+ return 'full';
126
+ if (raw === 'full' || raw === 'pick')
127
+ return raw;
128
+ badRequest('mode must be full or pick');
129
+ }
130
+ function hasTraversalSegment(value) {
131
+ const parts = value.split('/');
132
+ return parts.some((part) => part === '' || part === '.' || part === '..');
133
+ }
134
+ function parseDialogId(raw, fieldName) {
135
+ if (raw === null || raw.trim() === '') {
136
+ badRequest(`${fieldName} is required`);
137
+ }
138
+ if (raw.includes(String.fromCharCode(92)) || raw.includes(String.fromCharCode(0))) {
139
+ badRequest(`${fieldName} must be a slash-separated relative dialog id`);
140
+ }
141
+ if (path.isAbsolute(raw) || hasTraversalSegment(raw)) {
142
+ badRequest(`${fieldName} must not contain empty or traversal path segments`);
143
+ }
144
+ return raw;
145
+ }
146
+ function validateBundleEntryName(name, fieldName) {
147
+ if (name === '' ||
148
+ name.startsWith('/') ||
149
+ name.includes(String.fromCharCode(92)) ||
150
+ name.includes(String.fromCharCode(0)) ||
151
+ hasTraversalSegment(name)) {
152
+ badRequest(`${fieldName} must be a safe bundle entry path`);
153
+ }
154
+ }
155
+ function parseRepeatedSafeBundlePaths(requestUrl, queryName, fieldName) {
156
+ const paths = requestUrl.searchParams
157
+ .getAll(queryName)
158
+ .map((raw) => raw.trim().replace(/\/+$/, ''))
159
+ .filter((raw) => raw !== '');
160
+ const unique = [...new Set(paths)];
161
+ for (const item of unique) {
162
+ validateBundleEntryName(item, fieldName);
163
+ }
164
+ return unique;
165
+ }
166
+ function parseFileSelection(requestUrl) {
167
+ return parseRepeatedSafeBundlePaths(requestUrl, 'files', 'files');
168
+ }
169
+ function parseForensicsLocatorRequest(requestUrl) {
170
+ const rootId = parseDialogId(requestUrl.searchParams.get('rootId'), 'rootId');
171
+ const selfId = parseDialogId(requestUrl.searchParams.get('selfId') ?? rootId, 'selfId');
172
+ return {
173
+ rootId,
174
+ selfId,
175
+ requestedStatus: normalizeStatus(requestUrl.searchParams.get('status')),
176
+ };
177
+ }
178
+ function parseForensicsRequest(requestUrl) {
179
+ const locator = parseForensicsLocatorRequest(requestUrl);
180
+ const mode = normalizeMode(requestUrl.searchParams.get('mode'));
181
+ const files = parseFileSelection(requestUrl);
182
+ if (mode === 'pick' && files.length === 0) {
183
+ badRequest('mode=pick requires at least one files= parameter');
184
+ }
185
+ return {
186
+ ...locator,
187
+ course: parsePositiveInteger(requestUrl.searchParams.get('course'), 'course'),
188
+ mode,
189
+ files,
190
+ };
191
+ }
192
+ function isNodeFileNotFound(error) {
193
+ return (typeof error === 'object' && error !== null && error.code === 'ENOENT');
194
+ }
195
+ async function pathExists(pathAbs) {
196
+ try {
197
+ await promises_1.default.access(pathAbs);
198
+ return true;
199
+ }
200
+ catch (error) {
201
+ if (isNodeFileNotFound(error))
202
+ return false;
203
+ throw error;
204
+ }
205
+ }
206
+ async function resolveForensicsPaths(request) {
207
+ const dialogsRoot = path.join(process.cwd(), '.dialogs');
208
+ const statuses = request.requestedStatus ? [request.requestedStatus] : FORENSICS_STATUS_DIRS;
209
+ const targetKind = request.selfId === request.rootId ? 'root' : 'side';
210
+ for (const status of statuses) {
211
+ const rootPath = path.join(dialogsRoot, status, request.rootId);
212
+ const targetPath = targetKind === 'root' ? rootPath : path.join(rootPath, 'sideDialogs', request.selfId);
213
+ if (await pathExists(targetPath)) {
214
+ return { dialogsRoot, status, rootPath, targetPath, targetKind };
215
+ }
216
+ }
217
+ notFound(`dialog records not found for rootId=${request.rootId}, selfId=${request.selfId}, status=${request.requestedStatus ?? 'auto'}`);
218
+ }
219
+ function toZipEntryName(...parts) {
220
+ let joined = parts.join('/').split(String.fromCharCode(92)).join('/');
221
+ while (joined.startsWith('/'))
222
+ joined = joined.slice(1);
223
+ while (joined.includes('//'))
224
+ joined = joined.replace('//', '/');
225
+ validateBundleEntryName(joined, 'zip entry');
226
+ return joined;
227
+ }
228
+ function sourcePathForBundleEntry(resolved, entryName) {
229
+ validateBundleEntryName(entryName, 'file');
230
+ const [prefix, ...restParts] = entryName.split('/');
231
+ if (restParts.length === 0) {
232
+ badRequest(`files must include a path after ${prefix}`);
233
+ }
234
+ const relativePath = path.join(...restParts);
235
+ switch (prefix) {
236
+ case 'side':
237
+ if (resolved.targetKind !== 'side') {
238
+ badRequest('side/* files can only be selected for a side dialog');
239
+ }
240
+ return path.join(resolved.targetPath, relativePath);
241
+ case 'root':
242
+ if (resolved.targetKind !== 'side') {
243
+ badRequest('root/* files can only be selected when selfId is a side dialog');
244
+ }
245
+ return path.join(resolved.rootPath, relativePath);
246
+ case 'dialog':
247
+ if (resolved.targetKind !== 'root') {
248
+ badRequest('dialog/* files can only be selected when selfId equals rootId');
249
+ }
250
+ return path.join(resolved.rootPath, relativePath);
251
+ case 'debug':
252
+ if (restParts.length !== 1) {
253
+ badRequest('debug selections must be direct debug filenames');
254
+ }
255
+ return path.join(resolved.dialogsRoot, 'debug', restParts[0]);
256
+ default:
257
+ badRequest('files must start with side/, root/, dialog/, or debug/');
258
+ }
259
+ }
260
+ async function collectFileIfPresent(entries, missingFiles, sourcePath, entryName) {
261
+ try {
262
+ const stat = await promises_1.default.stat(sourcePath);
263
+ if (!stat.isFile()) {
264
+ missingFiles.push({ entryName, sourcePath });
265
+ return;
266
+ }
267
+ entries.push({
268
+ name: entryName,
269
+ data: await promises_1.default.readFile(sourcePath),
270
+ sourcePath,
271
+ });
272
+ }
273
+ catch (error) {
274
+ if (isNodeFileNotFound(error)) {
275
+ missingFiles.push({ entryName, sourcePath });
276
+ return;
277
+ }
278
+ throw error;
279
+ }
280
+ }
281
+ async function collectTree(entries, rootPath, entryPrefix, relativeDir = '') {
282
+ const dirPath = path.join(rootPath, relativeDir);
283
+ const dirents = await promises_1.default.readdir(dirPath, { withFileTypes: true });
284
+ dirents.sort((left, right) => left.name.localeCompare(right.name));
285
+ for (const dirent of dirents) {
286
+ if (dirent.name === '.' || dirent.name === '..')
287
+ continue;
288
+ const nextRelative = relativeDir === '' ? dirent.name : path.join(relativeDir, dirent.name);
289
+ const sourcePath = path.join(rootPath, nextRelative);
290
+ if (dirent.isDirectory()) {
291
+ await collectTree(entries, rootPath, entryPrefix, nextRelative);
292
+ continue;
293
+ }
294
+ if (dirent.isFile()) {
295
+ entries.push({
296
+ name: toZipEntryName(entryPrefix, nextRelative),
297
+ data: await promises_1.default.readFile(sourcePath),
298
+ sourcePath,
299
+ });
300
+ }
301
+ }
302
+ }
303
+ async function collectOptionalRootFiles(entries, missingFiles, rootPath, entryPrefix, course) {
304
+ const names = [...ROOT_DIAGNOSTIC_FILE_NAMES];
305
+ if (course !== undefined) {
306
+ names.push(`course-${String(course).padStart(3, '0')}.jsonl`);
307
+ }
308
+ for (const name of names) {
309
+ const entryName = toZipEntryName(entryPrefix, name);
310
+ await collectFileIfPresent(entries, missingFiles, path.join(rootPath, name), entryName);
311
+ }
312
+ }
313
+ function filenameContainsDialogParts(filename, rootId, selfId) {
314
+ const parts = [...rootId.split('/'), ...selfId.split('/')];
315
+ return parts.every((part) => filename.includes(part));
316
+ }
317
+ async function collectDebugFiles(entries, missingFiles, dialogsRoot, rootId, selfId) {
318
+ const debugPath = path.join(dialogsRoot, 'debug');
319
+ let dirents;
320
+ try {
321
+ dirents = await promises_1.default.readdir(debugPath, { withFileTypes: true });
322
+ }
323
+ catch (error) {
324
+ if (isNodeFileNotFound(error)) {
325
+ missingFiles.push({ entryName: 'debug/', sourcePath: debugPath });
326
+ return;
327
+ }
328
+ throw error;
329
+ }
330
+ dirents.sort((left, right) => left.name.localeCompare(right.name));
331
+ for (const dirent of dirents) {
332
+ if (!dirent.isFile() || !filenameContainsDialogParts(dirent.name, rootId, selfId))
333
+ continue;
334
+ const sourcePath = path.join(debugPath, dirent.name);
335
+ entries.push({
336
+ name: toZipEntryName('debug', dirent.name),
337
+ data: await promises_1.default.readFile(sourcePath),
338
+ sourcePath,
339
+ });
340
+ }
341
+ }
342
+ async function collectPickedFiles(entries, missingFiles, resolved, request) {
343
+ for (const entryName of request.files) {
344
+ await collectFileIfPresent(entries, missingFiles, sourcePathForBundleEntry(resolved, entryName), entryName);
345
+ }
346
+ }
347
+ function assertUniqueEntries(entries) {
348
+ const seen = new Set();
349
+ for (const entry of entries) {
350
+ if (seen.has(entry.name)) {
351
+ throw new Error(`duplicate forensics zip entry: ${entry.name}`);
352
+ }
353
+ seen.add(entry.name);
354
+ }
355
+ }
356
+ function buildManifest(params) {
357
+ return {
358
+ generatedAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
359
+ rtwsRoot: process.cwd(),
360
+ request: {
361
+ rootId: params.request.rootId,
362
+ selfId: params.request.selfId,
363
+ course: params.request.course ?? null,
364
+ status: params.request.requestedStatus ?? null,
365
+ mode: params.request.mode,
366
+ files: params.request.files,
367
+ },
368
+ resolved: {
369
+ status: params.resolved.status,
370
+ dialogsRoot: params.resolved.dialogsRoot,
371
+ rootPath: params.resolved.rootPath,
372
+ targetPath: params.resolved.targetPath,
373
+ targetKind: params.resolved.targetKind,
374
+ },
375
+ files: params.entries.map((entry) => ({
376
+ entryName: entry.name,
377
+ sourcePath: entry.sourcePath,
378
+ size: entry.data.byteLength,
379
+ })),
380
+ missingFiles: params.missingFiles,
381
+ };
382
+ }
383
+ async function collectDialogForensicsZip(request) {
384
+ const resolved = await resolveForensicsPaths(request);
385
+ const entries = [];
386
+ const missingFiles = [];
387
+ if (request.mode === 'pick') {
388
+ await collectPickedFiles(entries, missingFiles, resolved, request);
389
+ }
390
+ else if (resolved.targetKind === 'side') {
391
+ await collectTree(entries, resolved.targetPath, 'side');
392
+ await collectOptionalRootFiles(entries, missingFiles, resolved.rootPath, 'root', request.course);
393
+ await collectDebugFiles(entries, missingFiles, resolved.dialogsRoot, request.rootId, request.selfId);
394
+ }
395
+ else {
396
+ await collectOptionalRootFiles(entries, missingFiles, resolved.rootPath, 'dialog', request.course);
397
+ await collectDebugFiles(entries, missingFiles, resolved.dialogsRoot, request.rootId, request.selfId);
398
+ }
399
+ assertUniqueEntries(entries);
400
+ const manifest = buildManifest({ request, resolved, entries, missingFiles });
401
+ const zipEntries = [
402
+ {
403
+ name: 'manifest.json',
404
+ data: Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, 'utf8'),
405
+ },
406
+ ...entries.map((entry) => ({ name: entry.name, data: entry.data })),
407
+ ];
408
+ return buildStoreZip(zipEntries);
409
+ }
410
+ function crc32(buffer) {
411
+ let crc = 0xffffffff;
412
+ for (const byte of buffer) {
413
+ crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
414
+ }
415
+ return (crc ^ 0xffffffff) >>> 0;
416
+ }
417
+ function toDosDateTime(date) {
418
+ const year = Math.max(1980, date.getFullYear());
419
+ return {
420
+ time: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
421
+ date: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
422
+ };
423
+ }
424
+ function writeUInt32(value) {
425
+ const buffer = Buffer.alloc(4);
426
+ buffer.writeUInt32LE(value >>> 0, 0);
427
+ return buffer;
428
+ }
429
+ function writeUInt16(value) {
430
+ const buffer = Buffer.alloc(2);
431
+ buffer.writeUInt16LE(value, 0);
432
+ return buffer;
433
+ }
434
+ function buildStoreZip(entries) {
435
+ if (entries.length > 0xffff) {
436
+ throw new Error('forensics zip has too many entries for non-ZIP64 output');
437
+ }
438
+ const now = toDosDateTime(new Date());
439
+ const generalPurposeUtf8Flag = 0x0800;
440
+ const localParts = [];
441
+ const centralParts = [];
442
+ let offset = 0;
443
+ for (const entry of entries) {
444
+ const nameBuffer = Buffer.from(entry.name, 'utf8');
445
+ const checksum = crc32(entry.data);
446
+ if (nameBuffer.byteLength > 0xffff) {
447
+ throw new Error(`zip entry name is too long: ${entry.name}`);
448
+ }
449
+ if (entry.data.byteLength > 0xffffffff || offset > 0xffffffff) {
450
+ throw new Error('forensics zip is too large for non-ZIP64 output');
451
+ }
452
+ const localHeader = Buffer.concat([
453
+ writeUInt32(0x04034b50),
454
+ writeUInt16(20),
455
+ writeUInt16(generalPurposeUtf8Flag),
456
+ writeUInt16(0),
457
+ writeUInt16(now.time),
458
+ writeUInt16(now.date),
459
+ writeUInt32(checksum),
460
+ writeUInt32(entry.data.byteLength),
461
+ writeUInt32(entry.data.byteLength),
462
+ writeUInt16(nameBuffer.byteLength),
463
+ writeUInt16(0),
464
+ nameBuffer,
465
+ ]);
466
+ localParts.push(localHeader, entry.data);
467
+ const centralHeader = Buffer.concat([
468
+ writeUInt32(0x02014b50),
469
+ writeUInt16(20),
470
+ writeUInt16(20),
471
+ writeUInt16(generalPurposeUtf8Flag),
472
+ writeUInt16(0),
473
+ writeUInt16(now.time),
474
+ writeUInt16(now.date),
475
+ writeUInt32(checksum),
476
+ writeUInt32(entry.data.byteLength),
477
+ writeUInt32(entry.data.byteLength),
478
+ writeUInt16(nameBuffer.byteLength),
479
+ writeUInt16(0),
480
+ writeUInt16(0),
481
+ writeUInt16(0),
482
+ writeUInt16(0),
483
+ writeUInt32(0),
484
+ writeUInt32(offset),
485
+ nameBuffer,
486
+ ]);
487
+ centralParts.push(centralHeader);
488
+ offset += localHeader.byteLength + entry.data.byteLength;
489
+ if (offset > 0xffffffff) {
490
+ throw new Error('forensics zip is too large for non-ZIP64 output');
491
+ }
492
+ }
493
+ const centralDirectory = Buffer.concat(centralParts);
494
+ if (centralDirectory.byteLength > 0xffffffff) {
495
+ throw new Error('forensics zip central directory is too large for non-ZIP64 output');
496
+ }
497
+ const endOfCentralDirectory = Buffer.concat([
498
+ writeUInt32(0x06054b50),
499
+ writeUInt16(0),
500
+ writeUInt16(0),
501
+ writeUInt16(entries.length),
502
+ writeUInt16(entries.length),
503
+ writeUInt32(centralDirectory.byteLength),
504
+ writeUInt32(offset),
505
+ writeUInt16(0),
506
+ ]);
507
+ return Buffer.concat([...localParts, centralDirectory, endOfCentralDirectory]);
508
+ }
509
+ function forensicsZipFilename(request) {
510
+ const safe = `${request.rootId}-${request.selfId}`.split('/').join('-');
511
+ return `dominds-dialog-forensics-${safe}.zip`;
512
+ }
513
+ async function handleDialogForensicsZipRoute(reqUrl, res) {
514
+ const requestUrl = new URL(reqUrl ?? '/', 'http://127.0.0.1');
515
+ let request;
516
+ try {
517
+ request = parseForensicsRequest(requestUrl);
518
+ }
519
+ catch (error) {
520
+ respondJson(res, 400, {
521
+ success: false,
522
+ error: error instanceof Error ? error.message : String(error),
523
+ });
524
+ return true;
525
+ }
526
+ try {
527
+ const zipBuffer = await collectDialogForensicsZip(request);
528
+ res.writeHead(200, {
529
+ 'Content-Type': 'application/zip',
530
+ 'Content-Disposition': `attachment; filename="${forensicsZipFilename(request)}"`,
531
+ 'Cache-Control': 'no-store',
532
+ 'X-Content-Type-Options': 'nosniff',
533
+ });
534
+ res.end(zipBuffer);
535
+ return true;
536
+ }
537
+ catch (error) {
538
+ const message = error instanceof Error ? error.message : String(error);
539
+ respondJson(res, error instanceof BadForensicsRequestError
540
+ ? 400
541
+ : error instanceof ForensicsNotFoundError
542
+ ? 404
543
+ : 500, {
544
+ success: false,
545
+ error: message,
546
+ });
547
+ return true;
548
+ }
549
+ }
@@ -892,50 +892,73 @@ function spawnDetachedRestartHelper(params) {
892
892
  ' socket.setTimeout(1000, () => finish(true));',
893
893
  ' });',
894
894
  '}',
895
- 'async function waitForPortRelease(timeoutMs) {',
895
+ 'function getErrorCode(error) {',
896
+ ' return error && typeof error === "object" && typeof error.code === "string" ? error.code : "";',
897
+ '}',
898
+ 'function assertValidRetiringPid() {',
899
+ ' if (!Number.isInteger(payload.retiringPid) || payload.retiringPid <= 0) {',
900
+ ' throw new Error(`Invalid retiring Dominds pid for restart: ${String(payload.retiringPid)}`);',
901
+ ' }',
902
+ ' if (payload.retiringPid === process.pid) {',
903
+ ' throw new Error(`Refusing to kill restart helper pid ${String(process.pid)}`);',
904
+ ' }',
905
+ '}',
906
+ 'function isRetiringProcessAlive() {',
907
+ ' assertValidRetiringPid();',
908
+ ' try {',
909
+ ' process.kill(payload.retiringPid, 0);',
910
+ ' return true;',
911
+ ' } catch (error) {',
912
+ ' const code = getErrorCode(error);',
913
+ ' if (code === "ESRCH") return false;',
914
+ ' if (code === "EPERM") return true;',
915
+ ' throw error;',
916
+ ' }',
917
+ '}',
918
+ 'async function waitForRestartReadiness(timeoutMs) {',
896
919
  ' const deadline = Date.now() + timeoutMs;',
897
- ' let consecutiveIdle = 0;',
920
+ ' let consecutiveReady = 0;',
898
921
  ' while (Date.now() < deadline) {',
899
- ' if (await isPortBusy()) {',
900
- ' consecutiveIdle = 0;',
901
- ' await new Promise((resolve) => setTimeout(resolve, payload.probeIntervalMs));',
902
- ' continue;',
922
+ ' const processAlive = isRetiringProcessAlive();',
923
+ ' const portBusy = await isPortBusy();',
924
+ ' if (!processAlive && !portBusy) {',
925
+ ' consecutiveReady += 1;',
926
+ ' if (consecutiveReady >= 2) return true;',
927
+ ' } else {',
928
+ ' consecutiveReady = 0;',
903
929
  ' }',
904
- ' consecutiveIdle += 1;',
905
- ' if (consecutiveIdle >= 2) return true;',
906
930
  ' await new Promise((resolve) => setTimeout(resolve, payload.probeIntervalMs));',
907
931
  ' }',
908
932
  ' return false;',
909
933
  '}',
910
- 'function forceKillRetiringProcess() {',
911
- ' if (!Number.isInteger(payload.retiringPid) || payload.retiringPid <= 0) {',
912
- ' throw new Error(`Invalid retiring Dominds pid for restart: ${String(payload.retiringPid)}`);',
934
+ 'function runBestEffortKiller(command, args) {',
935
+ ' return new Promise((resolve) => {',
936
+ ' const killer = spawn(command, args, { stdio: payload.stdioMode });',
937
+ " killer.once('error', () => resolve());",
938
+ " killer.once('exit', () => resolve());",
939
+ ' });',
940
+ '}',
941
+ 'async function forceKillRetiringProcess() {',
942
+ ' assertValidRetiringPid();',
943
+ ' try {',
944
+ " process.kill(payload.retiringPid, 'SIGKILL');",
945
+ ' } catch (error) {',
946
+ ' const code = getErrorCode(error);',
947
+ ' if (code === "ESRCH") return;',
948
+ ' if (process.platform !== "win32") throw error;',
913
949
  ' }',
914
- ' if (payload.retiringPid === process.pid) {',
915
- ' throw new Error(`Refusing to kill restart helper pid ${String(process.pid)}`);',
950
+ ' if (process.platform === "win32") {',
951
+ " await runBestEffortKiller('taskkill.exe', ['/PID', String(payload.retiringPid), '/F']);",
916
952
  ' }',
917
- " const killer = process.platform === 'win32'",
918
- " ? spawn('taskkill.exe', ['/PID', String(payload.retiringPid), '/F'], { stdio: payload.stdioMode })",
919
- " : spawn('kill', ['-KILL', String(payload.retiringPid)], { stdio: payload.stdioMode });",
920
- ' return new Promise((resolve, reject) => {',
921
- " killer.once('error', reject);",
922
- " killer.once('exit', (code) => {",
923
- ' if (code === 0) {',
924
- ' resolve();',
925
- ' return;',
926
- ' }',
927
- ' resolve();',
928
- ' });',
929
- ' });',
930
953
  '}',
931
954
  '(async () => {',
932
955
  ' try {',
933
- ' const releasedGracefully = await waitForPortRelease(payload.forceKillAfterMs);',
934
- ' if (!releasedGracefully) {',
956
+ ' const readyGracefully = await waitForRestartReadiness(payload.forceKillAfterMs);',
957
+ ' if (!readyGracefully) {',
935
958
  ' await forceKillRetiringProcess();',
936
- ' const releasedAfterKill = await waitForPortRelease(payload.portReleaseTimeoutMs);',
937
- ' if (!releasedAfterKill) {',
938
- ' throw new Error(`Dominds restart port ${String(payload.host)}:${String(payload.port)} is still busy after force-killing pid ${String(payload.retiringPid)}`);',
959
+ ' const readyAfterKill = await waitForRestartReadiness(payload.portReleaseTimeoutMs);',
960
+ ' if (!readyAfterKill) {',
961
+ ' throw new Error(`Dominds restart target is still not ready after force-killing pid ${String(payload.retiringPid)}; port=${String(payload.host)}:${String(payload.port)}`);',
939
962
  ' }',
940
963
  ' }',
941
964
  " const child = spawn(payload.command, payload.args, { cwd: payload.cwd, env: process.env, detached, stdio: payload.stdioMode, shell: process.platform === 'win32' });",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dominds",
3
- "version": "1.25.12",
3
+ "version": "1.25.13",
4
4
  "description": "Dominds CLI and aggregation shell for the LongRun AI kernel/runtime packages.",
5
5
  "type": "commonjs",
6
6
  "publishConfig": {
@@ -52,9 +52,9 @@
52
52
  "ws": "^8.19.0",
53
53
  "yaml": "^2.8.2",
54
54
  "zod": "^4.3.6",
55
- "@longrun-ai/codex-auth": "0.13.0",
56
55
  "@longrun-ai/kernel": "1.15.9",
57
- "@longrun-ai/shell": "1.15.9"
56
+ "@longrun-ai/shell": "1.15.9",
57
+ "@longrun-ai/codex-auth": "0.13.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^25.3.5",