@linxin666/dsh-client-ui-task-board 0.3.18 → 0.3.20
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/lib/client.js +1 -1
- package/lib/index.js +40 -5
- package/lib/types/host-ledger.d.ts +2 -0
- package/lib/types/host-service.d.ts +11 -0
- package/package.json +19 -19
- package/src/host-ledger.ts +30 -2
- package/src/host-service.ts +34 -3
package/lib/client.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import z from "schemastery";
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
4
|
-
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, isAbsolute, join, win32 } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { isAbsolute as isAbsolute$1, join as join$1 } from "node:path/posix";
|
|
@@ -1487,6 +1487,7 @@ var HostTaskLedger = class {
|
|
|
1487
1487
|
this.file = join(dir, "ledger-v2.json");
|
|
1488
1488
|
this.lockFile = join(dir, "ledger-v2.lock");
|
|
1489
1489
|
this.schedulerFile = join(dir, "scheduler-v2.json");
|
|
1490
|
+
this.cleanStaleTemporaryFiles(dir);
|
|
1490
1491
|
this.lockFd = this.acquireLock();
|
|
1491
1492
|
try {
|
|
1492
1493
|
this.document = this.load(dir);
|
|
@@ -1499,6 +1500,15 @@ var HostTaskLedger = class {
|
|
|
1499
1500
|
throw error;
|
|
1500
1501
|
}
|
|
1501
1502
|
}
|
|
1503
|
+
/** Remove leftover *.tmp-* files from previous crashes or interrupted writes. */
|
|
1504
|
+
cleanStaleTemporaryFiles(dir) {
|
|
1505
|
+
try {
|
|
1506
|
+
const entries = readdirSync(dir);
|
|
1507
|
+
for (const entry of entries) if (entry.includes(".tmp-")) try {
|
|
1508
|
+
unlinkSync(join(dir, entry));
|
|
1509
|
+
} catch {}
|
|
1510
|
+
} catch {}
|
|
1511
|
+
}
|
|
1502
1512
|
/** Revision + scheduler without any task cloning; feeds the SSE event frame. */
|
|
1503
1513
|
summary() {
|
|
1504
1514
|
const { importedSources: _imports, ...scheduler } = this.document.scheduler;
|
|
@@ -1636,7 +1646,12 @@ var HostTaskLedger = class {
|
|
|
1636
1646
|
...patch
|
|
1637
1647
|
};
|
|
1638
1648
|
if (patch.lastTickAt !== void 0 && Object.keys(patch).every((key) => key === "lastTickAt")) {
|
|
1639
|
-
|
|
1649
|
+
try {
|
|
1650
|
+
this.writeSchedulerSidecar();
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
if (error?.code === "ENOSPC") return;
|
|
1653
|
+
throw error;
|
|
1654
|
+
}
|
|
1640
1655
|
return;
|
|
1641
1656
|
}
|
|
1642
1657
|
this.commit(false);
|
|
@@ -2691,6 +2706,7 @@ var TaskBoardHostService = class {
|
|
|
2691
2706
|
this.runner = new HostExecutionRunner(gateway, options.commandDispatcher, options.workspaceRegistry);
|
|
2692
2707
|
this.power = options.power ?? new PowerInhibitor();
|
|
2693
2708
|
this.now = options.now ?? Date.now;
|
|
2709
|
+
installStreamErrorGuards();
|
|
2694
2710
|
this.ledger.subscribe(() => {
|
|
2695
2711
|
this.syncPowerReasons();
|
|
2696
2712
|
this.emit();
|
|
@@ -2843,14 +2859,14 @@ var TaskBoardHostService = class {
|
|
|
2843
2859
|
}
|
|
2844
2860
|
scheduleLaunch(opened) {
|
|
2845
2861
|
this.launch(opened).catch((error) => {
|
|
2846
|
-
|
|
2862
|
+
safeConsoleError("[dsh-task-board] execution launch settlement failed", error);
|
|
2847
2863
|
});
|
|
2848
2864
|
}
|
|
2849
2865
|
schedulePoll() {
|
|
2850
2866
|
if (this.pollInFlight || this.disposed) return;
|
|
2851
2867
|
this.pollInFlight = true;
|
|
2852
2868
|
this.pollSessions().catch((error) => {
|
|
2853
|
-
|
|
2869
|
+
safeConsoleError("[dsh-task-board] session polling failed", error);
|
|
2854
2870
|
}).finally(() => {
|
|
2855
2871
|
this.pollInFlight = false;
|
|
2856
2872
|
});
|
|
@@ -2859,7 +2875,7 @@ var TaskBoardHostService = class {
|
|
|
2859
2875
|
if (this.tickInFlight || this.disposed) return;
|
|
2860
2876
|
this.tickInFlight = true;
|
|
2861
2877
|
this.tickSchedule(first).catch((error) => {
|
|
2862
|
-
|
|
2878
|
+
safeConsoleError("[dsh-task-board] scheduler tick failed", error);
|
|
2863
2879
|
}).finally(() => {
|
|
2864
2880
|
this.tickInFlight = false;
|
|
2865
2881
|
});
|
|
@@ -2877,6 +2893,25 @@ var TaskBoardHostService = class {
|
|
|
2877
2893
|
for (const listener of [...this.listeners]) listener();
|
|
2878
2894
|
}
|
|
2879
2895
|
};
|
|
2896
|
+
/**
|
|
2897
|
+
* Install stream error listeners on process.stderr and process.stdout so that
|
|
2898
|
+
* transient write failures (e.g. ENOSPC when the disk is full, or EPIPE on a
|
|
2899
|
+
* closed pipe) never emit unhandled 'error' events that kill the Node.js host process.
|
|
2900
|
+
*/
|
|
2901
|
+
function installStreamErrorGuards() {
|
|
2902
|
+
for (const stream of [process.stderr, process.stdout]) if (stream && typeof stream.on === "function") {
|
|
2903
|
+
if (!(typeof stream.listenerCount === "function" && stream.listenerCount("error") > 0)) stream.on("error", () => {});
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
/**
|
|
2907
|
+
* Defensively log to console.error without letting stderr write failures
|
|
2908
|
+
* (e.g. ENOSPC from SyncWriteStream on redirected logs) crash the host process.
|
|
2909
|
+
*/
|
|
2910
|
+
function safeConsoleError(message, ...args) {
|
|
2911
|
+
try {
|
|
2912
|
+
console.error(message, ...args);
|
|
2913
|
+
} catch {}
|
|
2914
|
+
}
|
|
2880
2915
|
//#endregion
|
|
2881
2916
|
//#region src/http.ts
|
|
2882
2917
|
/** Family-default JSON response headers; callers may append or override. */
|
|
@@ -53,6 +53,8 @@ export declare class HostTaskLedger {
|
|
|
53
53
|
constructor(dir?: string, now?: () => number, options?: {
|
|
54
54
|
sessionDefaultPermission?: TaskPermission;
|
|
55
55
|
});
|
|
56
|
+
/** Remove leftover *.tmp-* files from previous crashes or interrupted writes. */
|
|
57
|
+
private cleanStaleTemporaryFiles;
|
|
56
58
|
/** Revision + scheduler without any task cloning; feeds the SSE event frame. */
|
|
57
59
|
summary(): {
|
|
58
60
|
revision: number;
|
|
@@ -53,3 +53,14 @@ export declare class TaskBoardHostService {
|
|
|
53
53
|
private syncPowerReasons;
|
|
54
54
|
private emit;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Install stream error listeners on process.stderr and process.stdout so that
|
|
58
|
+
* transient write failures (e.g. ENOSPC when the disk is full, or EPIPE on a
|
|
59
|
+
* closed pipe) never emit unhandled 'error' events that kill the Node.js host process.
|
|
60
|
+
*/
|
|
61
|
+
export declare function installStreamErrorGuards(): void;
|
|
62
|
+
/**
|
|
63
|
+
* Defensively log to console.error without letting stderr write failures
|
|
64
|
+
* (e.g. ENOSPC from SyncWriteStream on redirected logs) crash the host process.
|
|
65
|
+
*/
|
|
66
|
+
export declare function safeConsoleError(message: string, ...args: unknown[]): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-client-ui-task-board",
|
|
3
3
|
"description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.20",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dsh": {
|
|
25
25
|
"engines": {
|
|
26
|
-
"dsh": ">=0.1.
|
|
26
|
+
"dsh": ">=0.1.5-rc.1"
|
|
27
27
|
},
|
|
28
28
|
"bundle": {
|
|
29
29
|
"patch": "./cordis.patch.yml"
|
|
@@ -48,23 +48,23 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
51
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
52
|
-
"@deepseek-ai/dsh-agent-presets": "^0.1.
|
|
53
|
-
"@deepseek-ai/dsh-api-gateway": "^0.1.
|
|
54
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.
|
|
55
|
-
"@deepseek-ai/dsh-workspace": "^0.1.
|
|
56
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.1.
|
|
57
|
-
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.
|
|
58
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.
|
|
59
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
60
|
-
"@deepseek-ai/dsh-client-store": "^0.1.
|
|
61
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.
|
|
62
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.
|
|
63
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.
|
|
64
|
-
"@deepseek-ai/dsh-commands": "^0.1.
|
|
65
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.
|
|
66
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
67
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.
|
|
51
|
+
"@deepseek-ai/dsh-agent": "^0.1.5-rc.1",
|
|
52
|
+
"@deepseek-ai/dsh-agent-presets": "^0.1.5-rc.1",
|
|
53
|
+
"@deepseek-ai/dsh-api-gateway": "^0.1.5-rc.1",
|
|
54
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.5-rc.1",
|
|
55
|
+
"@deepseek-ai/dsh-workspace": "^0.1.5-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.5-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.5-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
|
|
60
|
+
"@deepseek-ai/dsh-client-store": "^0.1.5-rc.1",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-commands": "^0.1.5-rc.1",
|
|
65
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.1",
|
|
68
68
|
"@types/node": "^22.20.0",
|
|
69
69
|
"@types/react": "~18.3.1",
|
|
70
70
|
"@types/react-dom": "^18.3.5",
|
package/src/host-ledger.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
|
-
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
4
4
|
import { dirname, join } from 'node:path'
|
|
5
5
|
import { dshHome } from './dsh-home.ts'
|
|
6
6
|
import { isValidCron, nextRunAtMs } from './core/schedule.ts'
|
|
@@ -286,6 +286,7 @@ export class HostTaskLedger {
|
|
|
286
286
|
this.file = join(dir, 'ledger-v2.json')
|
|
287
287
|
this.lockFile = join(dir, 'ledger-v2.lock')
|
|
288
288
|
this.schedulerFile = join(dir, 'scheduler-v2.json')
|
|
289
|
+
this.cleanStaleTemporaryFiles(dir)
|
|
289
290
|
this.lockFd = this.acquireLock()
|
|
290
291
|
try {
|
|
291
292
|
this.document = this.load(dir)
|
|
@@ -303,6 +304,24 @@ export class HostTaskLedger {
|
|
|
303
304
|
}
|
|
304
305
|
}
|
|
305
306
|
|
|
307
|
+
/** Remove leftover *.tmp-* files from previous crashes or interrupted writes. */
|
|
308
|
+
private cleanStaleTemporaryFiles(dir: string): void {
|
|
309
|
+
try {
|
|
310
|
+
const entries = readdirSync(dir)
|
|
311
|
+
for (const entry of entries) {
|
|
312
|
+
if (entry.includes('.tmp-')) {
|
|
313
|
+
try {
|
|
314
|
+
unlinkSync(join(dir, entry))
|
|
315
|
+
} catch {
|
|
316
|
+
// Best-effort cleanup
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
// Directory may not exist yet or cannot be read
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
306
325
|
/** Revision + scheduler without any task cloning; feeds the SSE event frame. */
|
|
307
326
|
summary(): { revision: number; scheduler: TaskBoardSchedulerSnapshot } {
|
|
308
327
|
const { importedSources: _imports, ...scheduler } = this.document.scheduler
|
|
@@ -443,7 +462,16 @@ export class HostTaskLedger {
|
|
|
443
462
|
// tiny sidecar instead; any other patch still goes through the full
|
|
444
463
|
// atomic commit.
|
|
445
464
|
if (patch.lastTickAt !== undefined && Object.keys(patch).every(key => key === 'lastTickAt')) {
|
|
446
|
-
|
|
465
|
+
try {
|
|
466
|
+
this.writeSchedulerSidecar()
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOSPC') {
|
|
469
|
+
// Disk is full; sidecar persistence fails, but in-memory heartbeat
|
|
470
|
+
// remains updated. Swallow to prevent unhandled log cascade crashes.
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
throw error
|
|
474
|
+
}
|
|
447
475
|
return
|
|
448
476
|
}
|
|
449
477
|
this.commit(false)
|
package/src/host-service.ts
CHANGED
|
@@ -45,6 +45,7 @@ export class TaskBoardHostService {
|
|
|
45
45
|
this.runner = new HostExecutionRunner(gateway, options.commandDispatcher, options.workspaceRegistry)
|
|
46
46
|
this.power = options.power ?? new PowerInhibitor()
|
|
47
47
|
this.now = options.now ?? Date.now
|
|
48
|
+
installStreamErrorGuards()
|
|
48
49
|
this.ledger.subscribe(() => {
|
|
49
50
|
this.syncPowerReasons()
|
|
50
51
|
this.emit()
|
|
@@ -214,7 +215,7 @@ export class TaskBoardHostService {
|
|
|
214
215
|
|
|
215
216
|
private scheduleLaunch(opened: OpenedRun): void {
|
|
216
217
|
void this.launch(opened).catch(error => {
|
|
217
|
-
|
|
218
|
+
safeConsoleError('[dsh-task-board] execution launch settlement failed', error)
|
|
218
219
|
})
|
|
219
220
|
}
|
|
220
221
|
|
|
@@ -222,7 +223,7 @@ export class TaskBoardHostService {
|
|
|
222
223
|
if (this.pollInFlight || this.disposed) return
|
|
223
224
|
this.pollInFlight = true
|
|
224
225
|
void this.pollSessions().catch(error => {
|
|
225
|
-
|
|
226
|
+
safeConsoleError('[dsh-task-board] session polling failed', error)
|
|
226
227
|
}).finally(() => { this.pollInFlight = false })
|
|
227
228
|
}
|
|
228
229
|
|
|
@@ -230,7 +231,7 @@ export class TaskBoardHostService {
|
|
|
230
231
|
if (this.tickInFlight || this.disposed) return
|
|
231
232
|
this.tickInFlight = true
|
|
232
233
|
void this.tickSchedule(first).catch(error => {
|
|
233
|
-
|
|
234
|
+
safeConsoleError('[dsh-task-board] scheduler tick failed', error)
|
|
234
235
|
}).finally(() => { this.tickInFlight = false })
|
|
235
236
|
}
|
|
236
237
|
|
|
@@ -248,3 +249,33 @@ export class TaskBoardHostService {
|
|
|
248
249
|
for (const listener of [...this.listeners]) listener()
|
|
249
250
|
}
|
|
250
251
|
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Install stream error listeners on process.stderr and process.stdout so that
|
|
255
|
+
* transient write failures (e.g. ENOSPC when the disk is full, or EPIPE on a
|
|
256
|
+
* closed pipe) never emit unhandled 'error' events that kill the Node.js host process.
|
|
257
|
+
*/
|
|
258
|
+
export function installStreamErrorGuards(): void {
|
|
259
|
+
for (const stream of [process.stderr, process.stdout]) {
|
|
260
|
+
if (stream && typeof stream.on === 'function') {
|
|
261
|
+
const hasErrorListener = typeof stream.listenerCount === 'function' && stream.listenerCount('error') > 0
|
|
262
|
+
if (!hasErrorListener) {
|
|
263
|
+
stream.on('error', () => {
|
|
264
|
+
// Swallow write stream errors to keep the host process alive
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Defensively log to console.error without letting stderr write failures
|
|
273
|
+
* (e.g. ENOSPC from SyncWriteStream on redirected logs) crash the host process.
|
|
274
|
+
*/
|
|
275
|
+
export function safeConsoleError(message: string, ...args: unknown[]): void {
|
|
276
|
+
try {
|
|
277
|
+
console.error(message, ...args)
|
|
278
|
+
} catch {
|
|
279
|
+
// Best-effort stderr write; ignore write errors when stderr stream fails
|
|
280
|
+
}
|
|
281
|
+
}
|