termdock 1.4.108 → 1.4.110
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/.desktop-dist/main.js +483 -24
- package/dist/client/assets/{ModelPreview-By3cPzJ9.js → ModelPreview-B40SLVdu.js} +1 -1
- package/dist/client/assets/{abnfDiagram-VRR7QNED-Dm9g0Pi3.js → abnfDiagram-VRR7QNED-D7p11Ssd.js} +1 -1
- package/dist/client/assets/{arc-XntYYR9t.js → arc-CsmLIp7i.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-CW0kZlVv.js → architectureDiagram-ZJ3FMSHR-BjYttOye.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-DgMdh19W.js → blockDiagram-677ZJIJ3-BnWi8IVO.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-DJC4m2Y6.js → c4Diagram-LMCZKHZV-BFm2RcKX.js} +1 -1
- package/dist/client/assets/channel-ifcSKqN3.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-CgE5oTjY.js → chunk-2Q5K7J3B-D_OKw60X.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-B50lSI4-.js → chunk-32BRIVSS-D6k1lZV9.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-C984gGSO.js → chunk-5VM5RSS4-CEqvSDkW.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-C1jPbFiq.js → chunk-EX3LRPZG-Ca2Gmbvd.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-D4U9D5Kv.js → chunk-JWPE2WC7-CRRvRUQB.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-C4rLPnf_.js → chunk-MOJQB5TN-CeZ5qCFm.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-CnSX68rt.js → chunk-RYQCIY6F-Cx7sW5Rr.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-LTTPDSzs.js → chunk-V7JOEXUC-B3_grzN8.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-dCZ9yGOH.js → chunk-VR4S4FIN-g73M8USU.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-DiUZr3na.js → chunk-XXDRQBXY-9i-NV13r.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-CVu2gyed.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-CVu2gyed.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-Dw73llr7.js → cose-bilkent-JH36ORCC-DGKauMxg.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-CsB9ZT7a.js → cynefin-VYW2F7L2-DKvEKvGd.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-NinMHYPB.js → cynefinDiagram-TSTJHNR4-CLXYMIpB.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-CmFL5D4c.js → dagre-VKFMJZFB-DFAR5nAp.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-D_C52SB6.js → diagram-FQU43EPY-DeRBoyeq.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-BLlpvNO5.js → diagram-G47NLZAW-CDEAgMuP.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-BNT2NQnR.js → diagram-NH7WQ7WH-4VFR2ZsJ.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-DlJ-T7tl.js → diagram-OA4YK3LP-DMKIgZlI.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-D7s_sZnL.js → diagram-WEI45ONY-zEjxP4io.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-PGksmUKG.js → ebnfDiagram-CCIWWBDH-BuErqR1l.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-CbfrT-bt.js → erDiagram-Q63AITRT-D4nbc2T3.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-CqUGdXdQ.js → flowDiagram-23GEKE2U-B2MiQAI4.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-DpH0ybM6.js → ganttDiagram-NO4QXBWP-hGTX0vsS.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-Cmc3LsBg.js → gitGraphDiagram-IHSO6WYX-BHHSVtvH.js} +1 -1
- package/dist/client/assets/index-CFXqRKcJ.css +1 -0
- package/dist/client/assets/index-D3kro0zp.js +456 -0
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-CHzl_SDV.js → infoDiagram-FWYZ7A6U-BZuNpsCG.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-BUVwtCaN.js → ishikawaDiagram-FXEZZL3T-C0KwjZIv.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-DrmO4THP.js → journeyDiagram-5HDEW3XC-mxy7-Irj.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-DTZoUd1n.js → kanban-definition-HUTT4EX6-DslZKv6b.js} +1 -1
- package/dist/client/assets/{linear-DgCNjduP.js → linear-BJXz3Iqq.js} +1 -1
- package/dist/client/assets/{mermaid.core-DLidsBlx.js → mermaid.core-Btv5hFOv.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-srFG-Dae.js → mindmap-definition-LN4V7U3C-BSbqyDyq.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-Qary-lxo.js → pegDiagram-2B236MQR-L2cCu4W1.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-D924FFIQ.js → pieDiagram-ENE6RG2P-CXdawbl4.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-YFBMJKUv.js → quadrantDiagram-ABIIQ3AL-CwKwBctE.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-C__7fBPw.js → railroadDiagram-RFXS5EU6-B6MBOnSp.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-BtAq5f69.js → requirementDiagram-TGXJPOKE-CR9lns0Z.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-BGULYk91.js → sankeyDiagram-HTMAVEWB-DLuhNvTd.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-BBjCjIFm.js → sequenceDiagram-DBY2YBRQ-BorPhsA5.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-DjP0UZy6.js → sizeCapture-X5ZJPWSS-DPXho3_K.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-BTqu5zVt.js → stateDiagram-2N3HPSRC-DSbrRqVk.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB--bVRWeRL.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-ClySuTz2.js → swimlanes-5IMT3BWC-D4A4axIk.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-CqWtgk0W.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6-DHURyh7H.js → timeline-definition-FHXFAJF6-D4ObRKT8.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-BFdn5KML.js → vennDiagram-L72KCM5P-Bj_4-RtP.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-DKEHwCwO.js → wardleyDiagram-EHGQE667-BNvcAUoQ.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-CI5HzOMq.js → xychartDiagram-FW5EYKEG-BfGV9sTq.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/agent/automationStore.js +12 -7
- package/dist/server/agent/sessionSearchStore.js +135 -5
- package/dist/server/entry.js +24 -1
- package/dist/server/routes/terminal.js +6 -2
- package/dist/server/utils/runtimeMonitor.js +287 -0
- package/dist/server/utils/settings.js +9 -0
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
- package/dist/client/assets/channel-D1KecDCp.js +0 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-BYJ3p4nE.js +0 -1
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-BYJ3p4nE.js +0 -1
- package/dist/client/assets/index-BwMQSNQq.js +0 -456
- package/dist/client/assets/index-RGd2Qu1X.css +0 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-D5FsphfH.js +0 -1
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-C1_CRJSE.js +0 -8
|
@@ -2,6 +2,10 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
const MAX_LOG_BYTES = 2 * 1024 * 1024;
|
|
4
4
|
const LOG_SEGMENT_BYTES = MAX_LOG_BYTES / 2;
|
|
5
|
+
const DEFAULT_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
6
|
+
const DEFAULT_MAX_SESSIONS = 50;
|
|
7
|
+
const DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60_000;
|
|
8
|
+
const BUDGET_MAINTENANCE_INTERVAL_MS = 10 * 60_000;
|
|
5
9
|
const ANSI_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*)?\u0007)|(?:(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
|
|
6
10
|
function clean(value) {
|
|
7
11
|
return value
|
|
@@ -12,23 +16,36 @@ function clean(value) {
|
|
|
12
16
|
}
|
|
13
17
|
export class SessionSearchStore {
|
|
14
18
|
directory;
|
|
19
|
+
limits;
|
|
15
20
|
metadata = new Map();
|
|
16
21
|
pending = new Map();
|
|
17
22
|
flushTimer = null;
|
|
18
|
-
|
|
23
|
+
metadataDirty = false;
|
|
24
|
+
metadataMustPersist = false;
|
|
25
|
+
lastMetadataWriteAt = 0;
|
|
26
|
+
lastBudgetMaintenanceAt = 0;
|
|
27
|
+
constructor(directory, limits = {}) {
|
|
19
28
|
this.directory = directory;
|
|
29
|
+
this.limits = limits;
|
|
20
30
|
this.loadMetadata();
|
|
31
|
+
this.enforceBudget();
|
|
21
32
|
}
|
|
22
33
|
append(metadata, output) {
|
|
23
34
|
const text = clean(output);
|
|
35
|
+
const previous = this.metadata.get(metadata.sessionId);
|
|
24
36
|
this.metadata.set(metadata.sessionId, { ...metadata, updatedAt: Date.now() });
|
|
37
|
+
this.metadataDirty = true;
|
|
38
|
+
this.metadataMustPersist ||= this.metadataChanged(previous, metadata);
|
|
25
39
|
if (text.trim())
|
|
26
40
|
this.pending.set(metadata.sessionId, [...(this.pending.get(metadata.sessionId) ?? []), text]);
|
|
27
41
|
if (!this.flushTimer)
|
|
28
42
|
this.flushTimer = setTimeout(() => this.flush(), 750);
|
|
29
43
|
}
|
|
30
44
|
update(metadata) {
|
|
45
|
+
const previous = this.metadata.get(metadata.sessionId);
|
|
31
46
|
this.metadata.set(metadata.sessionId, { ...metadata, updatedAt: Date.now() });
|
|
47
|
+
this.metadataDirty = true;
|
|
48
|
+
this.metadataMustPersist ||= this.metadataChanged(previous, metadata);
|
|
32
49
|
if (!this.flushTimer)
|
|
33
50
|
this.flushTimer = setTimeout(() => this.flush(), 750);
|
|
34
51
|
}
|
|
@@ -61,7 +78,7 @@ export class SessionSearchStore {
|
|
|
61
78
|
if (this.flushTimer)
|
|
62
79
|
clearTimeout(this.flushTimer);
|
|
63
80
|
this.flushTimer = null;
|
|
64
|
-
if (this.metadata.size === 0 && this.pending.size === 0)
|
|
81
|
+
if (this.metadata.size === 0 && this.pending.size === 0 && !this.metadataDirty)
|
|
65
82
|
return;
|
|
66
83
|
fs.mkdirSync(this.directory, { recursive: true });
|
|
67
84
|
for (const [sessionId, chunks] of this.pending) {
|
|
@@ -84,9 +101,63 @@ export class SessionSearchStore {
|
|
|
84
101
|
fs.appendFileSync(logPath, boundedChunk, { mode: 0o600 });
|
|
85
102
|
}
|
|
86
103
|
this.pending.clear();
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
if (this.metadataDirty && (this.metadataMustPersist || now - this.lastMetadataWriteAt >= 30_000))
|
|
106
|
+
this.persistMetadata();
|
|
107
|
+
if (now - this.lastBudgetMaintenanceAt >= BUDGET_MAINTENANCE_INTERVAL_MS)
|
|
108
|
+
this.enforceBudget(now);
|
|
109
|
+
}
|
|
110
|
+
enforceBudget(now = Date.now()) {
|
|
111
|
+
this.lastBudgetMaintenanceAt = now;
|
|
112
|
+
const maxSessions = this.limits.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
113
|
+
const maxAgeMs = this.limits.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
114
|
+
const maxTotalBytes = this.limits.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
|
|
115
|
+
const newest = [...this.metadata.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
116
|
+
const retained = new Set(newest.filter((item, index) => index < maxSessions && now - item.updatedAt <= maxAgeMs).map((item) => item.sessionId));
|
|
117
|
+
let changed = false;
|
|
118
|
+
for (const sessionId of this.metadata.keys()) {
|
|
119
|
+
if (retained.has(sessionId))
|
|
120
|
+
continue;
|
|
121
|
+
this.metadata.delete(sessionId);
|
|
122
|
+
this.pending.delete(sessionId);
|
|
123
|
+
this.removeSessionLogs(sessionId);
|
|
124
|
+
changed = true;
|
|
125
|
+
}
|
|
126
|
+
let files = this.listLogFiles();
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
if (retained.has(file.sessionId))
|
|
129
|
+
continue;
|
|
130
|
+
try {
|
|
131
|
+
fs.rmSync(file.path, { force: true });
|
|
132
|
+
}
|
|
133
|
+
catch { /* best effort */ }
|
|
134
|
+
}
|
|
135
|
+
files = this.listLogFiles();
|
|
136
|
+
let totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
137
|
+
const removalOrder = [
|
|
138
|
+
...files.filter((file) => file.previous).sort((a, b) => a.mtimeMs - b.mtimeMs),
|
|
139
|
+
...files.filter((file) => !file.previous).sort((a, b) => a.mtimeMs - b.mtimeMs),
|
|
140
|
+
];
|
|
141
|
+
for (const file of removalOrder) {
|
|
142
|
+
if (totalBytes <= maxTotalBytes)
|
|
143
|
+
break;
|
|
144
|
+
try {
|
|
145
|
+
fs.rmSync(file.path, { force: true });
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
totalBytes -= file.size;
|
|
151
|
+
if (!file.previous) {
|
|
152
|
+
this.metadata.delete(file.sessionId);
|
|
153
|
+
changed = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (changed) {
|
|
157
|
+
this.metadataDirty = true;
|
|
158
|
+
this.metadataMustPersist = true;
|
|
159
|
+
this.persistMetadata();
|
|
160
|
+
}
|
|
90
161
|
}
|
|
91
162
|
loadMetadata() {
|
|
92
163
|
try {
|
|
@@ -95,6 +166,7 @@ export class SessionSearchStore {
|
|
|
95
166
|
if (item && typeof item.sessionId === 'string')
|
|
96
167
|
this.metadata.set(item.sessionId, item);
|
|
97
168
|
}
|
|
169
|
+
this.lastMetadataWriteAt = Date.now();
|
|
98
170
|
}
|
|
99
171
|
catch { /* first run */ }
|
|
100
172
|
}
|
|
@@ -118,6 +190,64 @@ export class SessionSearchStore {
|
|
|
118
190
|
return 0;
|
|
119
191
|
}
|
|
120
192
|
}
|
|
193
|
+
metadataChanged(previous, next) {
|
|
194
|
+
return !previous
|
|
195
|
+
|| previous.backendSessionId !== next.backendSessionId
|
|
196
|
+
|| previous.title !== next.title
|
|
197
|
+
|| previous.cwd !== next.cwd
|
|
198
|
+
|| previous.agentSlug !== next.agentSlug
|
|
199
|
+
|| previous.agentNativeSessionId !== next.agentNativeSessionId;
|
|
200
|
+
}
|
|
201
|
+
persistMetadata() {
|
|
202
|
+
fs.mkdirSync(this.directory, { recursive: true });
|
|
203
|
+
const temporaryPath = `${this.metadataPath()}.${process.pid}.tmp`;
|
|
204
|
+
fs.writeFileSync(temporaryPath, JSON.stringify({ version: 1, sessions: [...this.metadata.values()] }, null, 2), { mode: 0o600 });
|
|
205
|
+
fs.renameSync(temporaryPath, this.metadataPath());
|
|
206
|
+
this.lastMetadataWriteAt = Date.now();
|
|
207
|
+
this.metadataDirty = false;
|
|
208
|
+
this.metadataMustPersist = false;
|
|
209
|
+
}
|
|
210
|
+
removeSessionLogs(sessionId) {
|
|
211
|
+
try {
|
|
212
|
+
fs.rmSync(this.logPath(sessionId), { force: true });
|
|
213
|
+
}
|
|
214
|
+
catch { /* best effort */ }
|
|
215
|
+
try {
|
|
216
|
+
fs.rmSync(this.previousLogPath(sessionId), { force: true });
|
|
217
|
+
}
|
|
218
|
+
catch { /* best effort */ }
|
|
219
|
+
}
|
|
220
|
+
listLogFiles() {
|
|
221
|
+
let names;
|
|
222
|
+
try {
|
|
223
|
+
names = fs.readdirSync(this.directory);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
return names.flatMap((name) => {
|
|
229
|
+
const match = name.match(/^(.*)\.log(\.1)?$/);
|
|
230
|
+
if (!match)
|
|
231
|
+
return [];
|
|
232
|
+
let sessionId;
|
|
233
|
+
try {
|
|
234
|
+
sessionId = decodeURIComponent(match[1]);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
const filePath = path.join(this.directory, name);
|
|
240
|
+
try {
|
|
241
|
+
const stat = fs.statSync(filePath);
|
|
242
|
+
return stat.isFile()
|
|
243
|
+
? [{ path: filePath, sessionId, previous: Boolean(match[2]), size: stat.size, mtimeMs: stat.mtimeMs }]
|
|
244
|
+
: [];
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
121
251
|
metadataPath() { return path.join(this.directory, 'sessions.json'); }
|
|
122
252
|
logPath(sessionId) { return path.join(this.directory, `${encodeURIComponent(sessionId)}.log`); }
|
|
123
253
|
previousLogPath(sessionId) { return `${this.logPath(sessionId)}.1`; }
|
package/dist/server/entry.js
CHANGED
|
@@ -23,6 +23,7 @@ import { localAccessManager } from './utils/localAccess.js';
|
|
|
23
23
|
import { isAllowedHost, isUpgradeOriginAllowed, validateHostMiddleware, } from './utils/requestSecurity.js';
|
|
24
24
|
import { getCookieSecurityOptions, setSecureCookieMode } from './utils/cookieSecurity.js';
|
|
25
25
|
import { requestDeadlineMiddleware } from './utils/requestDeadline.js';
|
|
26
|
+
import { RuntimeMonitor } from './utils/runtimeMonitor.js';
|
|
26
27
|
import { startOnboardingServer, stopOnboardingServer, getOnboardingServerUrl } from './onboardingServer.js';
|
|
27
28
|
import { CertificateWatcher } from './certificateWatcher.js';
|
|
28
29
|
import { startTermdockLogMaintenance, writeDiffTraceLog, writeErrorLog, writeJsonLog, writeTextLog, } from './utils/serverLogger.js';
|
|
@@ -67,6 +68,8 @@ function getRouteFamily(pathname) {
|
|
|
67
68
|
return 'auth';
|
|
68
69
|
if (pathname.startsWith('/api/client-log'))
|
|
69
70
|
return 'client-log';
|
|
71
|
+
if (pathname.startsWith('/api/diagnostics'))
|
|
72
|
+
return 'diagnostics';
|
|
70
73
|
if (pathname === '/health')
|
|
71
74
|
return 'health';
|
|
72
75
|
if (pathname.startsWith('/assets/'))
|
|
@@ -288,6 +291,7 @@ export function createApp(options = {}) {
|
|
|
288
291
|
return;
|
|
289
292
|
logged = true;
|
|
290
293
|
const durationMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;
|
|
294
|
+
options.runtimeMonitor?.recordRequest(res.statusCode, durationMs);
|
|
291
295
|
writeJsonLog('access.log', {
|
|
292
296
|
event,
|
|
293
297
|
method: req.method,
|
|
@@ -424,6 +428,13 @@ export function createApp(options = {}) {
|
|
|
424
428
|
const address = req.socket.remoteAddress ?? '';
|
|
425
429
|
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
|
|
426
430
|
};
|
|
431
|
+
app.get('/api/diagnostics/runtime', requireAuth({ bypass: isTrustedLocalCliRequest }), async (_req, res) => {
|
|
432
|
+
if (!options.runtimeMonitor) {
|
|
433
|
+
res.status(503).json({ error: 'Runtime monitor is not active', code: 'RUNTIME_MONITOR_INACTIVE' });
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
res.json(await options.runtimeMonitor.diagnostics());
|
|
437
|
+
});
|
|
427
438
|
// 应用鉴权 + CSRF保护(在终端路由之前)
|
|
428
439
|
// The HTML preview route authenticates itself: document requests use the
|
|
429
440
|
// session cookie, then get redirected to a short-lived URL token so the
|
|
@@ -490,8 +501,20 @@ export function startServer(options = {}) {
|
|
|
490
501
|
const stopLogMaintenance = startTermdockLogMaintenance();
|
|
491
502
|
const port = options.port ?? Number(process.env.PORT || DEFAULT_PORT);
|
|
492
503
|
const host = options.host ?? (process.env.HOST || DEFAULT_HOST);
|
|
493
|
-
const
|
|
504
|
+
const runtimeMonitor = new RuntimeMonitor({
|
|
505
|
+
stateDirectory: path.join(homedir(), '.termdock'),
|
|
506
|
+
historyPath: path.join(homedir(), '.termdock', 'runtime-metrics.log'),
|
|
507
|
+
});
|
|
508
|
+
runtimeMonitor.start();
|
|
509
|
+
const app = createApp({
|
|
510
|
+
port: options.onboardingPort ?? port,
|
|
511
|
+
httpsCaPath: options.httpsCaPath,
|
|
512
|
+
localApiToken: options.localApiToken,
|
|
513
|
+
runtimeMonitor,
|
|
514
|
+
});
|
|
494
515
|
const { server, scheme } = createServerForApp(app, options);
|
|
516
|
+
server.on('connection', (socket) => runtimeMonitor.trackSocket(socket));
|
|
517
|
+
server.once('close', () => runtimeMonitor.stop());
|
|
495
518
|
setSecureCookieMode(scheme === 'https');
|
|
496
519
|
const certWatcher = new CertificateWatcher({
|
|
497
520
|
enabled: scheme === 'https' && Boolean(options.httpsCertPath && options.httpsKeyPath),
|
|
@@ -14,7 +14,7 @@ import { getPtyHostManager } from '../ptyhost/manager.js';
|
|
|
14
14
|
import { pathValidator } from '../utils/pathValidator.js';
|
|
15
15
|
import { TERMINAL, TMUX } from '../config.js';
|
|
16
16
|
import { localAccessManager } from '../utils/localAccess.js';
|
|
17
|
-
import { normalizeLocalAccessName, getLocaleSetting, setLocaleSetting, getContextDraftHeightSetting, setContextDraftHeightSetting, getAutoRenameAgentsSetting, setAutoRenameAgentsSetting, getAutoRenameNamerSetting, setAutoRenameNamerSetting, getAutoRenameModelsSetting, setAutoRenameModelsSetting, getAutoRenameIntervalMinutesSetting, setAutoRenameIntervalMinutesSetting, getAutoRenamePromptPreferenceSetting, setAutoRenamePromptPreferenceSetting, getAutoRenamePromptPayloadCharsSetting, setAutoRenamePromptPayloadCharsSetting, getNewSessionAgentSlugSetting, setNewSessionAgentSlugSetting, } from '../utils/settings.js';
|
|
17
|
+
import { normalizeLocalAccessName, getLocaleSetting, setLocaleSetting, getContextDraftHeightSetting, setContextDraftHeightSetting, getAutoRenameAgentsSetting, setAutoRenameAgentsSetting, getAutoRenameNamerSetting, setAutoRenameNamerSetting, getAutoRenameModelsSetting, setAutoRenameModelsSetting, getAutoRenameIntervalMinutesSetting, setAutoRenameIntervalMinutesSetting, getAutoRenamePromptPreferenceSetting, setAutoRenamePromptPreferenceSetting, getAutoRenamePromptPayloadCharsSetting, setAutoRenamePromptPayloadCharsSetting, getNewSessionAgentSlugSetting, setNewSessionAgentSlugSetting, getRunningSessionButtonEnabledSetting, setRunningSessionButtonEnabledSetting, } from '../utils/settings.js';
|
|
18
18
|
import { loadContextDraft, saveContextDraft } from '../utils/contextDraft.js';
|
|
19
19
|
import { getOnboardingServerUrl } from '../onboardingServer.js';
|
|
20
20
|
import { getFocusSequence, removeClientFocusState, scanFocusTrackingMode, setClientFocusState, } from '../utils/tmuxFocus.js';
|
|
@@ -1309,7 +1309,7 @@ function tryDeliverCollaborationInbox(frontendSessionId) {
|
|
|
1309
1309
|
|| (session.agentSession?.rich && !['idle', 'done'].includes(session.agentSession.status))) {
|
|
1310
1310
|
return { delivered: [], pending: pending.length };
|
|
1311
1311
|
}
|
|
1312
|
-
|
|
1312
|
+
session.ptyProcess.write(buildBracketedSubmitBytes(formatCollaborationDelivery(frontendSessionId, pending)));
|
|
1313
1313
|
collaborationStore.markDelivered(pending.map((message) => message.id));
|
|
1314
1314
|
return { delivered: pending.map((message) => message.id), pending: 0 };
|
|
1315
1315
|
}
|
|
@@ -5185,6 +5185,7 @@ async function getSettingsPayload() {
|
|
|
5185
5185
|
autoRenamePromptPreference: getAutoRenamePromptPreferenceSetting(),
|
|
5186
5186
|
autoRenamePromptPayloadChars: getAutoRenamePromptPayloadCharsSetting(),
|
|
5187
5187
|
newSessionAgentSlug: getNewSessionAgentSlugSetting(),
|
|
5188
|
+
runningSessionButtonEnabled: getRunningSessionButtonEnabledSetting(),
|
|
5188
5189
|
localAccess: {
|
|
5189
5190
|
...localAccess,
|
|
5190
5191
|
interfaces,
|
|
@@ -5293,6 +5294,9 @@ router.put('/settings', async (req, res) => {
|
|
|
5293
5294
|
}
|
|
5294
5295
|
setNewSessionAgentSlugSetting(slug);
|
|
5295
5296
|
}
|
|
5297
|
+
if (typeof body.runningSessionButtonEnabled === 'boolean') {
|
|
5298
|
+
setRunningSessionButtonEnabledSetting(body.runningSessionButtonEnabled);
|
|
5299
|
+
}
|
|
5296
5300
|
if (body.localAccess && typeof body.localAccess === 'object') {
|
|
5297
5301
|
const localAccessBody = body.localAccess;
|
|
5298
5302
|
if (localAccessBody.reset === true) {
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { monitorEventLoopDelay } from 'node:perf_hooks';
|
|
5
|
+
const SAMPLE_INTERVAL_MS = 10_000;
|
|
6
|
+
const HISTORY_INTERVAL_MS = 60_000;
|
|
7
|
+
const MAX_HISTORY_SAMPLES = 24 * 60;
|
|
8
|
+
function round(value, digits = 2) {
|
|
9
|
+
if (!Number.isFinite(value))
|
|
10
|
+
return 0;
|
|
11
|
+
const scale = 10 ** digits;
|
|
12
|
+
return Math.round(value * scale) / scale;
|
|
13
|
+
}
|
|
14
|
+
function percentile(values, quantile) {
|
|
15
|
+
if (values.length === 0)
|
|
16
|
+
return 0;
|
|
17
|
+
const ordered = [...values].sort((a, b) => a - b);
|
|
18
|
+
return ordered[Math.min(ordered.length - 1, Math.floor(ordered.length * quantile))] ?? 0;
|
|
19
|
+
}
|
|
20
|
+
function readProcessIo() {
|
|
21
|
+
if (process.platform !== 'linux')
|
|
22
|
+
return { readBytes: 0, writeBytes: 0 };
|
|
23
|
+
try {
|
|
24
|
+
const values = new Map(fs.readFileSync('/proc/self/io', 'utf8').trim().split('\n').map((line) => {
|
|
25
|
+
const separator = line.indexOf(':');
|
|
26
|
+
return [line.slice(0, separator), Number(line.slice(separator + 1).trim())];
|
|
27
|
+
}));
|
|
28
|
+
return { readBytes: values.get('read_bytes') ?? 0, writeBytes: values.get('write_bytes') ?? 0 };
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { readBytes: 0, writeBytes: 0 };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function collectStorageSnapshot(root, maxEntries = 20_000) {
|
|
35
|
+
const totals = new Map();
|
|
36
|
+
const queue = [root];
|
|
37
|
+
let totalBytes = 0;
|
|
38
|
+
let fileCount = 0;
|
|
39
|
+
let visited = 0;
|
|
40
|
+
while (queue.length > 0 && visited < maxEntries) {
|
|
41
|
+
const directory = queue.shift();
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (visited >= maxEntries)
|
|
51
|
+
break;
|
|
52
|
+
visited += 1;
|
|
53
|
+
const entryPath = path.join(directory, entry.name);
|
|
54
|
+
if (entry.isDirectory()) {
|
|
55
|
+
queue.push(entryPath);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!entry.isFile())
|
|
59
|
+
continue;
|
|
60
|
+
try {
|
|
61
|
+
const stat = await fs.promises.stat(entryPath);
|
|
62
|
+
totalBytes += stat.size;
|
|
63
|
+
fileCount += 1;
|
|
64
|
+
const topLevelName = path.relative(root, entryPath).split(path.sep)[0] || entry.name;
|
|
65
|
+
totals.set(topLevelName, (totals.get(topLevelName) ?? 0) + stat.size);
|
|
66
|
+
}
|
|
67
|
+
catch { /* The file may disappear while diagnostics walk the directory. */ }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
let freeBytes = null;
|
|
71
|
+
let totalFilesystemBytes = null;
|
|
72
|
+
try {
|
|
73
|
+
const stat = await fs.promises.statfs(root);
|
|
74
|
+
freeBytes = stat.bavail * stat.bsize;
|
|
75
|
+
totalFilesystemBytes = stat.blocks * stat.bsize;
|
|
76
|
+
}
|
|
77
|
+
catch { /* Not available on every supported filesystem. */ }
|
|
78
|
+
return {
|
|
79
|
+
root, totalBytes, fileCount, truncated: queue.length > 0 || visited >= maxEntries,
|
|
80
|
+
freeBytes, totalFilesystemBytes,
|
|
81
|
+
topLevel: [...totals.entries()].map(([name, bytes]) => ({ name, bytes })).sort((a, b) => b.bytes - a.bytes).slice(0, 20),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function evaluateRuntimeHealth(sample, storage) {
|
|
85
|
+
const findings = [];
|
|
86
|
+
const add = (severity, code, message, value, threshold) => {
|
|
87
|
+
findings.push({ severity, code, message, value: round(value), threshold });
|
|
88
|
+
};
|
|
89
|
+
if (sample.rssBytes >= 1024 ** 3)
|
|
90
|
+
add('critical', 'PROCESS_RSS_HIGH', 'Termdock 内存占用超过 1 GiB', sample.rssBytes, 1024 ** 3);
|
|
91
|
+
else if (sample.rssBytes >= 512 * 1024 ** 2)
|
|
92
|
+
add('warning', 'PROCESS_RSS_HIGH', 'Termdock 内存占用超过 512 MiB', sample.rssBytes, 512 * 1024 ** 2);
|
|
93
|
+
if (sample.eventLoopDelayP99Ms >= 500)
|
|
94
|
+
add('critical', 'EVENT_LOOP_STALLED', 'Node.js 事件循环 P99 延迟过高', sample.eventLoopDelayP99Ms, 500);
|
|
95
|
+
else if (sample.eventLoopDelayP99Ms >= 100)
|
|
96
|
+
add('warning', 'EVENT_LOOP_SLOW', 'Node.js 事件循环 P99 延迟偏高', sample.eventLoopDelayP99Ms, 100);
|
|
97
|
+
if (sample.diskWriteBytesPerSecond >= 25 * 1024 ** 2)
|
|
98
|
+
add('critical', 'DISK_WRITE_RATE_HIGH', 'Termdock 持续磁盘写入速率过高', sample.diskWriteBytesPerSecond, 25 * 1024 ** 2);
|
|
99
|
+
else if (sample.diskWriteBytesPerSecond >= 5 * 1024 ** 2)
|
|
100
|
+
add('warning', 'DISK_WRITE_RATE_HIGH', 'Termdock 磁盘写入速率偏高', sample.diskWriteBytesPerSecond, 5 * 1024 ** 2);
|
|
101
|
+
if (sample.activeConnections >= 1000)
|
|
102
|
+
add('critical', 'CONNECTION_COUNT_HIGH', 'Termdock 活跃连接数过高', sample.activeConnections, 1000);
|
|
103
|
+
else if (sample.activeConnections >= 200)
|
|
104
|
+
add('warning', 'CONNECTION_COUNT_HIGH', 'Termdock 活跃连接数偏高', sample.activeConnections, 200);
|
|
105
|
+
if (sample.requestsPerSecond >= 0.25 && sample.requestErrorRatio >= 0.25)
|
|
106
|
+
add('critical', 'REQUEST_ERROR_RATIO_HIGH', '最近采样窗口请求错误率过高', sample.requestErrorRatio, 0.25);
|
|
107
|
+
else if (sample.requestsPerSecond >= 0.25 && sample.requestErrorRatio >= 0.1)
|
|
108
|
+
add('warning', 'REQUEST_ERROR_RATIO_HIGH', '最近采样窗口请求错误率偏高', sample.requestErrorRatio, 0.1);
|
|
109
|
+
if (sample.requestLatencyP95Ms >= 5_000)
|
|
110
|
+
add('critical', 'REQUEST_LATENCY_HIGH', '请求 P95 延迟超过 5 秒', sample.requestLatencyP95Ms, 5_000);
|
|
111
|
+
else if (sample.requestLatencyP95Ms >= 1_000)
|
|
112
|
+
add('warning', 'REQUEST_LATENCY_HIGH', '请求 P95 延迟超过 1 秒', sample.requestLatencyP95Ms, 1_000);
|
|
113
|
+
if (storage.totalBytes >= 2 * 1024 ** 3)
|
|
114
|
+
add('critical', 'STATE_DIRECTORY_LARGE', 'Termdock 状态目录超过 2 GiB', storage.totalBytes, 2 * 1024 ** 3);
|
|
115
|
+
else if (storage.totalBytes >= 512 * 1024 ** 2)
|
|
116
|
+
add('warning', 'STATE_DIRECTORY_LARGE', 'Termdock 状态目录超过 512 MiB', storage.totalBytes, 512 * 1024 ** 2);
|
|
117
|
+
if (storage.freeBytes !== null && storage.totalFilesystemBytes) {
|
|
118
|
+
const freeRatio = storage.freeBytes / storage.totalFilesystemBytes;
|
|
119
|
+
if (storage.freeBytes <= 1024 ** 3 || freeRatio <= 0.05)
|
|
120
|
+
add('critical', 'DISK_SPACE_LOW', 'Termdock 所在磁盘剩余空间严重不足', freeRatio, 0.05);
|
|
121
|
+
else if (storage.freeBytes <= 5 * 1024 ** 3 || freeRatio <= 0.1)
|
|
122
|
+
add('warning', 'DISK_SPACE_LOW', 'Termdock 所在磁盘剩余空间偏低', freeRatio, 0.1);
|
|
123
|
+
}
|
|
124
|
+
return findings;
|
|
125
|
+
}
|
|
126
|
+
export class RuntimeMonitor {
|
|
127
|
+
stateDirectory;
|
|
128
|
+
historyPath;
|
|
129
|
+
sampleIntervalMs;
|
|
130
|
+
historyIntervalMs;
|
|
131
|
+
maxHistorySamples;
|
|
132
|
+
sockets = new Map();
|
|
133
|
+
eventLoopHistogram;
|
|
134
|
+
history = [];
|
|
135
|
+
current = null;
|
|
136
|
+
sampleTimer = null;
|
|
137
|
+
lastHistoryAt = 0;
|
|
138
|
+
previousAt = Date.now();
|
|
139
|
+
previousCpu = process.cpuUsage();
|
|
140
|
+
previousIo = readProcessIo();
|
|
141
|
+
closedSocketBytes = { read: 0, written: 0 };
|
|
142
|
+
previousSocketBytes = { read: 0, written: 0 };
|
|
143
|
+
requestDurations = [];
|
|
144
|
+
requestCount = 0;
|
|
145
|
+
requestErrors = 0;
|
|
146
|
+
constructor(options) {
|
|
147
|
+
this.stateDirectory = options.stateDirectory;
|
|
148
|
+
this.historyPath = options.historyPath ?? null;
|
|
149
|
+
this.sampleIntervalMs = options.sampleIntervalMs ?? SAMPLE_INTERVAL_MS;
|
|
150
|
+
this.historyIntervalMs = options.historyIntervalMs ?? HISTORY_INTERVAL_MS;
|
|
151
|
+
this.maxHistorySamples = options.maxHistorySamples ?? MAX_HISTORY_SAMPLES;
|
|
152
|
+
this.eventLoopHistogram = monitorEventLoopDelay({ resolution: 20 });
|
|
153
|
+
this.loadHistory();
|
|
154
|
+
}
|
|
155
|
+
start() {
|
|
156
|
+
if (this.sampleTimer)
|
|
157
|
+
return;
|
|
158
|
+
this.eventLoopHistogram.enable();
|
|
159
|
+
this.sample();
|
|
160
|
+
this.sampleTimer = setInterval(() => this.sample(), this.sampleIntervalMs);
|
|
161
|
+
this.sampleTimer.unref?.();
|
|
162
|
+
}
|
|
163
|
+
stop() {
|
|
164
|
+
if (this.sampleTimer)
|
|
165
|
+
clearInterval(this.sampleTimer);
|
|
166
|
+
this.sampleTimer = null;
|
|
167
|
+
this.eventLoopHistogram.disable();
|
|
168
|
+
}
|
|
169
|
+
trackSocket(socket) {
|
|
170
|
+
if (this.sockets.has(socket))
|
|
171
|
+
return;
|
|
172
|
+
this.sockets.set(socket, { read: socket.bytesRead, written: socket.bytesWritten });
|
|
173
|
+
socket.once('close', () => {
|
|
174
|
+
const baseline = this.sockets.get(socket);
|
|
175
|
+
if (!baseline)
|
|
176
|
+
return;
|
|
177
|
+
this.closedSocketBytes.read += Math.max(0, socket.bytesRead - baseline.read);
|
|
178
|
+
this.closedSocketBytes.written += Math.max(0, socket.bytesWritten - baseline.written);
|
|
179
|
+
this.sockets.delete(socket);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
recordRequest(statusCode, durationMs) {
|
|
183
|
+
this.requestCount += 1;
|
|
184
|
+
if (statusCode >= 400)
|
|
185
|
+
this.requestErrors += 1;
|
|
186
|
+
if (Number.isFinite(durationMs))
|
|
187
|
+
this.requestDurations.push(durationMs);
|
|
188
|
+
if (this.requestDurations.length > 10_000)
|
|
189
|
+
this.requestDurations.splice(0, this.requestDurations.length - 10_000);
|
|
190
|
+
}
|
|
191
|
+
sample(now = Date.now()) {
|
|
192
|
+
const elapsedSeconds = Math.max(0.001, (now - this.previousAt) / 1000);
|
|
193
|
+
const cpu = process.cpuUsage(this.previousCpu);
|
|
194
|
+
const cpuProcessPercent = (cpu.user + cpu.system) / 1_000_000 / elapsedSeconds * 100;
|
|
195
|
+
const io = readProcessIo();
|
|
196
|
+
const socketBytes = this.getSocketBytes();
|
|
197
|
+
const memory = process.memoryUsage();
|
|
198
|
+
const requestCount = this.requestCount;
|
|
199
|
+
const result = {
|
|
200
|
+
timestamp: now,
|
|
201
|
+
uptimeSeconds: round(process.uptime()),
|
|
202
|
+
cpuProcessPercent: round(cpuProcessPercent),
|
|
203
|
+
cpuHostCapacityPercent: round(cpuProcessPercent / Math.max(1, os.availableParallelism())),
|
|
204
|
+
rssBytes: memory.rss,
|
|
205
|
+
heapUsedBytes: memory.heapUsed,
|
|
206
|
+
heapTotalBytes: memory.heapTotal,
|
|
207
|
+
externalBytes: memory.external,
|
|
208
|
+
eventLoopDelayP99Ms: round(this.eventLoopHistogram.percentile(99) / 1_000_000),
|
|
209
|
+
diskReadBytesPerSecond: round(Math.max(0, io.readBytes - this.previousIo.readBytes) / elapsedSeconds),
|
|
210
|
+
diskWriteBytesPerSecond: round(Math.max(0, io.writeBytes - this.previousIo.writeBytes) / elapsedSeconds),
|
|
211
|
+
networkReadBytesPerSecond: round(Math.max(0, socketBytes.read - this.previousSocketBytes.read) / elapsedSeconds),
|
|
212
|
+
networkWriteBytesPerSecond: round(Math.max(0, socketBytes.written - this.previousSocketBytes.written) / elapsedSeconds),
|
|
213
|
+
activeConnections: this.sockets.size,
|
|
214
|
+
requestsPerSecond: round(requestCount / elapsedSeconds),
|
|
215
|
+
requestErrorRatio: round(requestCount > 0 ? this.requestErrors / requestCount : 0, 4),
|
|
216
|
+
requestLatencyP95Ms: round(percentile(this.requestDurations, 0.95)),
|
|
217
|
+
};
|
|
218
|
+
this.current = result;
|
|
219
|
+
this.previousAt = now;
|
|
220
|
+
this.previousCpu = process.cpuUsage();
|
|
221
|
+
this.previousIo = io;
|
|
222
|
+
this.previousSocketBytes = socketBytes;
|
|
223
|
+
this.requestCount = 0;
|
|
224
|
+
this.requestErrors = 0;
|
|
225
|
+
this.requestDurations = [];
|
|
226
|
+
this.eventLoopHistogram.reset();
|
|
227
|
+
if (now - this.lastHistoryAt >= this.historyIntervalMs) {
|
|
228
|
+
this.history.push(result);
|
|
229
|
+
this.history = this.history.slice(-this.maxHistorySamples);
|
|
230
|
+
this.lastHistoryAt = now;
|
|
231
|
+
this.appendHistory(result);
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
async diagnostics() {
|
|
236
|
+
const current = this.current ?? this.sample();
|
|
237
|
+
const storage = await collectStorageSnapshot(this.stateDirectory);
|
|
238
|
+
const findings = evaluateRuntimeHealth(current, storage);
|
|
239
|
+
return {
|
|
240
|
+
status: findings.some((item) => item.severity === 'critical') ? 'critical' : findings.length > 0 ? 'warning' : 'ok',
|
|
241
|
+
generatedAt: Date.now(), current, history: [...this.history], storage, findings,
|
|
242
|
+
scopes: { cpu: 'termdock-process', memory: 'termdock-process', diskIo: 'termdock-process', networkIo: 'termdock-http-sockets', storage: 'termdock-state-directory' },
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
getSocketBytes() {
|
|
246
|
+
let read = this.closedSocketBytes.read;
|
|
247
|
+
let written = this.closedSocketBytes.written;
|
|
248
|
+
for (const [socket, baseline] of this.sockets) {
|
|
249
|
+
read += Math.max(0, socket.bytesRead - baseline.read);
|
|
250
|
+
written += Math.max(0, socket.bytesWritten - baseline.written);
|
|
251
|
+
}
|
|
252
|
+
return { read, written };
|
|
253
|
+
}
|
|
254
|
+
loadHistory() {
|
|
255
|
+
if (!this.historyPath)
|
|
256
|
+
return;
|
|
257
|
+
try {
|
|
258
|
+
this.history = fs.readFileSync(this.historyPath, 'utf8').trim().split('\n').flatMap((line) => {
|
|
259
|
+
try {
|
|
260
|
+
const item = JSON.parse(line);
|
|
261
|
+
return Number.isFinite(item.timestamp) ? [item] : [];
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
}).slice(-this.maxHistorySamples);
|
|
267
|
+
this.lastHistoryAt = this.history.at(-1)?.timestamp ?? 0;
|
|
268
|
+
}
|
|
269
|
+
catch { /* First run or a missing/corrupt history file. */ }
|
|
270
|
+
}
|
|
271
|
+
appendHistory(sample) {
|
|
272
|
+
if (!this.historyPath)
|
|
273
|
+
return;
|
|
274
|
+
void fs.promises.mkdir(path.dirname(this.historyPath), { recursive: true })
|
|
275
|
+
.then(() => fs.promises.appendFile(this.historyPath, `${JSON.stringify(sample)}\n`, { mode: 0o600 }))
|
|
276
|
+
.then(async () => {
|
|
277
|
+
const stat = await fs.promises.stat(this.historyPath);
|
|
278
|
+
if (stat.size <= 2 * 1024 * 1024)
|
|
279
|
+
return;
|
|
280
|
+
const retained = this.history.map((item) => JSON.stringify(item)).join('\n') + '\n';
|
|
281
|
+
const temporary = `${this.historyPath}.${process.pid}.tmp`;
|
|
282
|
+
await fs.promises.writeFile(temporary, retained, { mode: 0o600 });
|
|
283
|
+
await fs.promises.rename(temporary, this.historyPath);
|
|
284
|
+
})
|
|
285
|
+
.catch(() => undefined);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -105,6 +105,7 @@ function normalizeSettings(value) {
|
|
|
105
105
|
? raw.autoRenamePromptPayloadChars
|
|
106
106
|
: 12_000,
|
|
107
107
|
newSessionAgentSlug: normalizeNewSessionAgentSlug(raw.newSessionAgentSlug),
|
|
108
|
+
runningSessionButtonEnabled: raw.runningSessionButtonEnabled === true,
|
|
108
109
|
updatedAt: typeof raw.updatedAt === 'number' ? raw.updatedAt : Date.now(),
|
|
109
110
|
};
|
|
110
111
|
}
|
|
@@ -399,3 +400,11 @@ export function setNewSessionAgentSlugSetting(slug) {
|
|
|
399
400
|
settings.newSessionAgentSlug = normalizeNewSessionAgentSlug(slug);
|
|
400
401
|
});
|
|
401
402
|
}
|
|
403
|
+
export function getRunningSessionButtonEnabledSetting() {
|
|
404
|
+
return loadSettings().runningSessionButtonEnabled;
|
|
405
|
+
}
|
|
406
|
+
export function setRunningSessionButtonEnabledSetting(enabled) {
|
|
407
|
+
return updateSettings((settings) => {
|
|
408
|
+
settings.runningSessionButtonEnabled = enabled;
|
|
409
|
+
});
|
|
410
|
+
}
|
package/package.json
CHANGED
package/runtime-manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "termdock",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.110",
|
|
5
5
|
"runtimeProtocolVersion": 1,
|
|
6
6
|
"minimumDesktopVersion": "1.4.46",
|
|
7
7
|
"nodeMajor": 22,
|
|
8
8
|
"dependencyHash": "sha256-3I9X7zF4J4BUYOvxi3lIuWSKfDy6S6GD61w8Ay95Ypw=",
|
|
9
|
-
"serverBundleHash": "sha256-
|
|
9
|
+
"serverBundleHash": "sha256-FzVprEN6mYO+IXk9xqujHpNv1s544akAZOVv28fGvww=",
|
|
10
10
|
"entrypoint": "dist/server/cli.js",
|
|
11
11
|
"clientEntrypoint": "dist/client/index.html"
|
|
12
12
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{U as a,C as n}from"./mermaid.core-DLidsBlx.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-LTTPDSzs.js";import{b as i}from"./mermaid.core-DLidsBlx.js";import"./chunk-5VM5RSS4-C984gGSO.js";import"./chunk-XXDRQBXY-DiUZr3na.js";import"./chunk-VR4S4FIN-dCZ9yGOH.js";import"./chunk-32BRIVSS-B50lSI4-.js";import"./purify.es-CC4Brkr_.js";import"./index-BwMQSNQq.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-LTTPDSzs.js";import{b as i}from"./mermaid.core-DLidsBlx.js";import"./chunk-5VM5RSS4-C984gGSO.js";import"./chunk-XXDRQBXY-DiUZr3na.js";import"./chunk-VR4S4FIN-dCZ9yGOH.js";import"./chunk-32BRIVSS-B50lSI4-.js";import"./purify.es-CC4Brkr_.js";import"./index-BwMQSNQq.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};
|