@keyou007/agent-bot 0.1.15-alpha.0 → 0.1.15-alpha.2
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/CHANGELOG.md +14 -0
- package/README.md +23 -20
- package/README.zh.md +23 -20
- package/config.example.yaml +1 -0
- package/dist/cli/FeishuAppConfiguration.js +42 -3
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/feishu/CardRenderer.js +114 -16
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +21 -6
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +58 -6
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +33 -4
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +7 -6
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/FinalResponseBranding.js +15 -0
- package/dist/feishu/FinalResponseBranding.js.map +1 -0
- package/dist/logging/DailyLogStream.js +73 -0
- package/dist/logging/DailyLogStream.js.map +1 -0
- package/dist/logging/logger.js +2 -1
- package/dist/logging/logger.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +270 -40
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +126 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +11 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SupervisorDiagnostics.js +37 -21
- package/dist/supervision/SupervisorDiagnostics.js.map +1 -1
- package/dist/supervisor.js +11 -4
- package/dist/supervisor.js.map +1 -1
- package/dist/utils/executeShellCommand.js +83 -16
- package/dist/utils/executeShellCommand.js.map +1 -1
- package/docs/technical-reference.md +9 -8
- package/docs/technical-reference.zh.md +9 -8
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/cli/FeishuAppConfiguration.ts +76 -3
- package/src/cli.ts +6 -1
- package/src/feishu/CardRenderer.ts +150 -17
- package/src/feishu/FeishuConnector.ts +32 -7
- package/src/feishu/FeishuMarkdown.ts +64 -5
- package/src/feishu/FeishuMessageClient.ts +41 -12
- package/src/feishu/FeishuTurnPresenter.ts +9 -6
- package/src/feishu/FinalResponseBranding.ts +18 -0
- package/src/logging/DailyLogStream.ts +88 -0
- package/src/logging/logger.ts +2 -1
- package/src/proxy/ProxySessionController.ts +292 -39
- package/src/state/StateStore.ts +180 -0
- package/src/state/migrations.ts +11 -0
- package/src/supervision/SupervisorDiagnostics.ts +41 -25
- package/src/supervisor.ts +10 -4
- package/src/utils/executeShellCommand.ts +103 -15
package/src/state/StateStore.ts
CHANGED
|
@@ -68,6 +68,13 @@ export interface AgentBotTurnMessageRecord extends TurnAnchorRecord {
|
|
|
68
68
|
messageKind: "progress" | "final";
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export interface GoalCardDeliveryRecord {
|
|
72
|
+
localSessionId: string;
|
|
73
|
+
contextKey: string;
|
|
74
|
+
messageId: string;
|
|
75
|
+
updatedAt: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
export interface TurnRuntimeOriginRecord {
|
|
72
79
|
turnId: string;
|
|
73
80
|
localSessionId: string;
|
|
@@ -242,6 +249,46 @@ export class StateStore {
|
|
|
242
249
|
`);
|
|
243
250
|
}
|
|
244
251
|
|
|
252
|
+
saveGoalCardDelivery(localSessionId: string, contextKey: string, messageId: string): void {
|
|
253
|
+
const updatedAt = new Date().toISOString();
|
|
254
|
+
this.db.prepare(`
|
|
255
|
+
INSERT INTO goal_card_deliveries (local_session_id, context_key, message_id, updated_at)
|
|
256
|
+
VALUES (?, ?, ?, ?)
|
|
257
|
+
ON CONFLICT(local_session_id, context_key) DO UPDATE SET
|
|
258
|
+
message_id = excluded.message_id,
|
|
259
|
+
updated_at = excluded.updated_at
|
|
260
|
+
`).run(localSessionId, contextKey, messageId, updatedAt);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
getGoalCardDelivery(localSessionId: string, contextKey: string): GoalCardDeliveryRecord | undefined {
|
|
264
|
+
const row = this.db.prepare(`
|
|
265
|
+
SELECT local_session_id, context_key, message_id, updated_at
|
|
266
|
+
FROM goal_card_deliveries
|
|
267
|
+
WHERE local_session_id = ? AND context_key = ?
|
|
268
|
+
`).get(localSessionId, contextKey) as {
|
|
269
|
+
local_session_id: string;
|
|
270
|
+
context_key: string;
|
|
271
|
+
message_id: string;
|
|
272
|
+
updated_at: string;
|
|
273
|
+
} | undefined;
|
|
274
|
+
return row ? goalCardDeliveryFromRow(row) : undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
listGoalCardDeliveries(localSessionId: string): GoalCardDeliveryRecord[] {
|
|
278
|
+
const rows = this.db.prepare(`
|
|
279
|
+
SELECT local_session_id, context_key, message_id, updated_at
|
|
280
|
+
FROM goal_card_deliveries
|
|
281
|
+
WHERE local_session_id = ?
|
|
282
|
+
ORDER BY updated_at ASC
|
|
283
|
+
`).all(localSessionId) as Array<{
|
|
284
|
+
local_session_id: string;
|
|
285
|
+
context_key: string;
|
|
286
|
+
message_id: string;
|
|
287
|
+
updated_at: string;
|
|
288
|
+
}>;
|
|
289
|
+
return rows.map(goalCardDeliveryFromRow);
|
|
290
|
+
}
|
|
291
|
+
|
|
245
292
|
close(): void {
|
|
246
293
|
this.db.close();
|
|
247
294
|
}
|
|
@@ -1058,6 +1105,91 @@ export class StateStore {
|
|
|
1058
1105
|
}));
|
|
1059
1106
|
}
|
|
1060
1107
|
|
|
1108
|
+
listTaskTurnGraph(localSessionId: string): CompletedTurnSnapshotRecord[] {
|
|
1109
|
+
this.backfillTurnParents(localSessionId);
|
|
1110
|
+
const records = new Map(
|
|
1111
|
+
this.listCompletedTurnGraphRows(localSessionId).map((record) => [record.turnId, record]),
|
|
1112
|
+
);
|
|
1113
|
+
let ancestorTurnId = this.findForkSourceTurnId(localSessionId)
|
|
1114
|
+
?? this.findCrossSessionParentTurnId(localSessionId);
|
|
1115
|
+
const visited = new Set<string>();
|
|
1116
|
+
|
|
1117
|
+
while (ancestorTurnId && !visited.has(ancestorTurnId)) {
|
|
1118
|
+
visited.add(ancestorTurnId);
|
|
1119
|
+
const ancestor = this.getCompletedTurnGraphRecord(ancestorTurnId);
|
|
1120
|
+
if (!ancestor) break;
|
|
1121
|
+
records.set(ancestor.turnId, ancestor);
|
|
1122
|
+
ancestorTurnId = ancestor.parentTurnId;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
return [...records.values()].sort(compareCompletedTurnSnapshots);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
private listCompletedTurnGraphRows(localSessionId: string): CompletedTurnSnapshotRecord[] {
|
|
1129
|
+
const rows = this.db.prepare(`
|
|
1130
|
+
SELECT ts.turn_id, tpl.parent_turn_id, ts.snapshot_json, ts.updated_at
|
|
1131
|
+
FROM turn_snapshots ts
|
|
1132
|
+
LEFT JOIN turn_parent_links tpl ON tpl.turn_id = ts.turn_id
|
|
1133
|
+
WHERE ts.local_session_id = ?
|
|
1134
|
+
AND json_extract(ts.snapshot_json, '$.status') = 'completed'
|
|
1135
|
+
`).all(localSessionId) as Array<{
|
|
1136
|
+
turn_id: string;
|
|
1137
|
+
parent_turn_id: string | null;
|
|
1138
|
+
snapshot_json: string;
|
|
1139
|
+
updated_at: string;
|
|
1140
|
+
}>;
|
|
1141
|
+
return rows.map(mapCompletedTurnSnapshot);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
private getCompletedTurnGraphRecord(turnId: string): CompletedTurnSnapshotRecord | undefined {
|
|
1145
|
+
const owner = this.db.prepare(`
|
|
1146
|
+
SELECT local_session_id
|
|
1147
|
+
FROM turn_snapshots
|
|
1148
|
+
WHERE turn_id = ?
|
|
1149
|
+
AND json_extract(snapshot_json, '$.status') = 'completed'
|
|
1150
|
+
`).get(turnId) as { local_session_id: string } | undefined;
|
|
1151
|
+
if (!owner) return undefined;
|
|
1152
|
+
this.backfillTurnParents(owner.local_session_id);
|
|
1153
|
+
const row = this.db.prepare(`
|
|
1154
|
+
SELECT ts.turn_id, tpl.parent_turn_id, ts.snapshot_json, ts.updated_at
|
|
1155
|
+
FROM turn_snapshots ts
|
|
1156
|
+
LEFT JOIN turn_parent_links tpl ON tpl.turn_id = ts.turn_id
|
|
1157
|
+
WHERE ts.turn_id = ?
|
|
1158
|
+
AND json_extract(ts.snapshot_json, '$.status') = 'completed'
|
|
1159
|
+
`).get(turnId) as {
|
|
1160
|
+
turn_id: string;
|
|
1161
|
+
parent_turn_id: string | null;
|
|
1162
|
+
snapshot_json: string;
|
|
1163
|
+
updated_at: string;
|
|
1164
|
+
} | undefined;
|
|
1165
|
+
return row ? mapCompletedTurnSnapshot(row) : undefined;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
private findForkSourceTurnId(localSessionId: string): string | undefined {
|
|
1169
|
+
const row = this.db.prepare(`
|
|
1170
|
+
SELECT json_extract(payload_json, '$.sourceTurnId') AS source_turn_id
|
|
1171
|
+
FROM audit_events
|
|
1172
|
+
WHERE event_type IN ('session_forked', 'thread_forked')
|
|
1173
|
+
AND json_extract(payload_json, '$.forkedLocalSessionId') = ?
|
|
1174
|
+
ORDER BY id DESC
|
|
1175
|
+
LIMIT 1
|
|
1176
|
+
`).get(localSessionId) as { source_turn_id: string | null } | undefined;
|
|
1177
|
+
return row?.source_turn_id ?? undefined;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
private findCrossSessionParentTurnId(localSessionId: string): string | undefined {
|
|
1181
|
+
const row = this.db.prepare(`
|
|
1182
|
+
SELECT links.parent_turn_id
|
|
1183
|
+
FROM turn_parent_links links
|
|
1184
|
+
JOIN turn_snapshots parent ON parent.turn_id = links.parent_turn_id
|
|
1185
|
+
WHERE links.local_session_id = ?
|
|
1186
|
+
AND parent.local_session_id <> ?
|
|
1187
|
+
ORDER BY links.created_at ASC
|
|
1188
|
+
LIMIT 1
|
|
1189
|
+
`).get(localSessionId, localSessionId) as { parent_turn_id: string } | undefined;
|
|
1190
|
+
return row?.parent_turn_id;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1061
1193
|
private backfillTurnParents(localSessionId: string): void {
|
|
1062
1194
|
const turns = this.db.prepare(`
|
|
1063
1195
|
SELECT
|
|
@@ -1873,6 +2005,20 @@ function mapMessageReaction(row: MessageReactionRow): MessageReactionRecord {
|
|
|
1873
2005
|
};
|
|
1874
2006
|
}
|
|
1875
2007
|
|
|
2008
|
+
function goalCardDeliveryFromRow(row: {
|
|
2009
|
+
local_session_id: string;
|
|
2010
|
+
context_key: string;
|
|
2011
|
+
message_id: string;
|
|
2012
|
+
updated_at: string;
|
|
2013
|
+
}): GoalCardDeliveryRecord {
|
|
2014
|
+
return {
|
|
2015
|
+
localSessionId: row.local_session_id,
|
|
2016
|
+
contextKey: row.context_key,
|
|
2017
|
+
messageId: row.message_id,
|
|
2018
|
+
updatedAt: row.updated_at,
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
|
|
1876
2022
|
function mapQueuedPrompt(row: QueuedPromptRow): QueuedPromptRecord {
|
|
1877
2023
|
const parsedPaths = JSON.parse(row.local_image_paths_json) as unknown;
|
|
1878
2024
|
const localImagePaths = Array.isArray(parsedPaths)
|
|
@@ -1915,6 +2061,40 @@ function mapTurnAttempt(row: TurnAttemptRow): TurnAttemptRecord {
|
|
|
1915
2061
|
};
|
|
1916
2062
|
}
|
|
1917
2063
|
|
|
2064
|
+
function mapCompletedTurnSnapshot(row: {
|
|
2065
|
+
turn_id: string;
|
|
2066
|
+
parent_turn_id: string | null;
|
|
2067
|
+
snapshot_json: string;
|
|
2068
|
+
updated_at: string;
|
|
2069
|
+
}): CompletedTurnSnapshotRecord {
|
|
2070
|
+
return {
|
|
2071
|
+
turnId: row.turn_id,
|
|
2072
|
+
parentTurnId: row.parent_turn_id ?? undefined,
|
|
2073
|
+
snapshot: JSON.parse(row.snapshot_json),
|
|
2074
|
+
updatedAt: row.updated_at,
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
function compareCompletedTurnSnapshots(
|
|
2079
|
+
left: CompletedTurnSnapshotRecord,
|
|
2080
|
+
right: CompletedTurnSnapshotRecord,
|
|
2081
|
+
): number {
|
|
2082
|
+
const timeDifference = completedTurnTimestamp(right) - completedTurnTimestamp(left);
|
|
2083
|
+
return timeDifference || right.updatedAt.localeCompare(left.updatedAt);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
function completedTurnTimestamp(record: CompletedTurnSnapshotRecord): number {
|
|
2087
|
+
const snapshot = record.snapshot && typeof record.snapshot === "object"
|
|
2088
|
+
? record.snapshot as { completedAt?: unknown; startedAt?: unknown }
|
|
2089
|
+
: undefined;
|
|
2090
|
+
const timestamp = typeof snapshot?.completedAt === "number"
|
|
2091
|
+
? snapshot.completedAt
|
|
2092
|
+
: typeof snapshot?.startedAt === "number"
|
|
2093
|
+
? snapshot.startedAt
|
|
2094
|
+
: Date.parse(record.updatedAt);
|
|
2095
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
1918
2098
|
function isIncompleteTurnAttemptStatus(status: TurnAttemptStatus): boolean {
|
|
1919
2099
|
return status === "accepted" || status === "running" || status === "recovering";
|
|
1920
2100
|
}
|
package/src/state/migrations.ts
CHANGED
|
@@ -173,4 +173,15 @@ export const migrations = [
|
|
|
173
173
|
CREATE INDEX IF NOT EXISTS idx_turn_attempts_turn
|
|
174
174
|
ON turn_attempts(turn_id);
|
|
175
175
|
`,
|
|
176
|
+
`
|
|
177
|
+
CREATE TABLE IF NOT EXISTS goal_card_deliveries (
|
|
178
|
+
local_session_id TEXT NOT NULL,
|
|
179
|
+
context_key TEXT NOT NULL,
|
|
180
|
+
message_id TEXT NOT NULL,
|
|
181
|
+
updated_at TEXT NOT NULL,
|
|
182
|
+
PRIMARY KEY (local_session_id, context_key)
|
|
183
|
+
);
|
|
184
|
+
CREATE INDEX IF NOT EXISTS idx_goal_card_deliveries_session
|
|
185
|
+
ON goal_card_deliveries(local_session_id);
|
|
186
|
+
`,
|
|
176
187
|
] as const;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { AppConfig } from "../config/schema.js";
|
|
4
|
+
import { DailyLogStream, dailyLogPath } from "../logging/DailyLogStream.js";
|
|
4
5
|
|
|
5
6
|
const MAX_LOG_BYTES = 10 * 1024 * 1024;
|
|
6
7
|
const LOG_BACKUP_COUNT = 3;
|
|
@@ -42,8 +43,12 @@ type ExtendedProcessReport = typeof process.report & {
|
|
|
42
43
|
|
|
43
44
|
export class SupervisorDiagnostics {
|
|
44
45
|
readonly paths: SupervisorDiagnosticsPaths;
|
|
46
|
+
private workerStderrStream?: DailyLogStream;
|
|
45
47
|
|
|
46
|
-
constructor(
|
|
48
|
+
constructor(
|
|
49
|
+
config: Pick<AppConfig, "logging" | "storage">,
|
|
50
|
+
private readonly clock: () => Date = () => new Date(),
|
|
51
|
+
) {
|
|
47
52
|
this.paths = resolveSupervisorDiagnosticsPaths(config);
|
|
48
53
|
}
|
|
49
54
|
|
|
@@ -51,13 +56,13 @@ export class SupervisorDiagnostics {
|
|
|
51
56
|
try {
|
|
52
57
|
ensurePrivateDirectory(path.dirname(this.paths.supervisorLogPath));
|
|
53
58
|
ensurePrivateDirectory(this.paths.crashReportDirectory);
|
|
54
|
-
rotateLogFile(this.
|
|
55
|
-
rotateLogFile(this.
|
|
59
|
+
rotateLogFile(this.currentSupervisorLogPath());
|
|
60
|
+
rotateLogFile(this.currentWorkerStderrPath());
|
|
56
61
|
this.configureCurrentProcessReports();
|
|
57
62
|
this.writeEvent("diagnostics_initialized", {
|
|
58
63
|
nodeVersion: process.versions.node,
|
|
59
|
-
supervisorLogPath: this.
|
|
60
|
-
workerStderrPath: this.
|
|
64
|
+
supervisorLogPath: this.currentSupervisorLogPath(),
|
|
65
|
+
workerStderrPath: this.currentWorkerStderrPath(),
|
|
61
66
|
crashReportDirectory: this.paths.crashReportDirectory,
|
|
62
67
|
});
|
|
63
68
|
} catch (error) {
|
|
@@ -73,40 +78,51 @@ export class SupervisorDiagnostics {
|
|
|
73
78
|
...data,
|
|
74
79
|
})}\n`;
|
|
75
80
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
const logPath = this.currentSupervisorLogPath();
|
|
82
|
+
rotateLogFile(logPath);
|
|
83
|
+
fs.appendFileSync(logPath, line, { encoding: "utf8", mode: 0o600 });
|
|
84
|
+
restrictFilePermissions(logPath);
|
|
78
85
|
} catch (error) {
|
|
79
86
|
writeFallbackDiagnostic(event, error);
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
openWorkerStderr():
|
|
90
|
+
openWorkerStderr(): DailyLogStream | "ignore" {
|
|
84
91
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
const stream = new DailyLogStream(this.paths.workerStderrPath, {
|
|
93
|
+
clock: this.clock,
|
|
94
|
+
mode: 0o600,
|
|
95
|
+
beforeOpen: (filePath) => rotateLogFile(filePath),
|
|
96
|
+
});
|
|
97
|
+
stream.on("error", (error) => {
|
|
98
|
+
this.writeEvent("worker_stderr_write_failed", { error: errorMessage(error) });
|
|
99
|
+
});
|
|
100
|
+
stream.write(`${JSON.stringify({ event: "worker_started", time: this.clock().toISOString() })}\n`);
|
|
101
|
+
this.workerStderrStream = stream;
|
|
102
|
+
return stream;
|
|
93
103
|
} catch (error) {
|
|
94
104
|
this.writeEvent("worker_stderr_open_failed", { error: errorMessage(error) });
|
|
95
105
|
return "ignore";
|
|
96
106
|
}
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
closeWorkerStderr(target:
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
closeWorkerStderr(target: DailyLogStream | "ignore"): Promise<void> {
|
|
110
|
+
if (target === "ignore") return Promise.resolve();
|
|
111
|
+
return new Promise((resolve) => target.end(resolve));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
currentSupervisorLogPath(date = this.clock()): string {
|
|
115
|
+
return dailyLogPath(this.paths.supervisorLogPath, date);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
currentWorkerStderrPath(date = this.clock()): string {
|
|
119
|
+
return dailyLogPath(this.paths.workerStderrPath, date);
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
recordCrash(context: WorkerCrashContext): WorkerCrashRecord | undefined {
|
|
109
123
|
try {
|
|
124
|
+
const exitedAt = new Date(context.exitedAt);
|
|
125
|
+
const crashDate = Number.isNaN(exitedAt.getTime()) ? this.clock() : exitedAt;
|
|
110
126
|
const record: WorkerCrashRecord = {
|
|
111
127
|
version: 1,
|
|
112
128
|
...context,
|
|
@@ -114,8 +130,8 @@ export class SupervisorDiagnostics {
|
|
|
114
130
|
nodeVersion: process.versions.node,
|
|
115
131
|
executablePath: process.execPath,
|
|
116
132
|
cwd: process.cwd(),
|
|
117
|
-
supervisorLogPath: this.
|
|
118
|
-
workerStderrPath: this.
|
|
133
|
+
supervisorLogPath: this.currentSupervisorLogPath(crashDate),
|
|
134
|
+
workerStderrPath: this.workerStderrStream?.filePath ?? this.currentWorkerStderrPath(crashDate),
|
|
119
135
|
crashReportDirectory: this.paths.crashReportDirectory,
|
|
120
136
|
diagnosticReports: findDiagnosticReports(
|
|
121
137
|
this.paths.crashReportDirectory,
|
package/src/supervisor.ts
CHANGED
|
@@ -70,18 +70,24 @@ async function startChild(): Promise<void> {
|
|
|
70
70
|
AGENT_BOT_SUPERVISED: "1",
|
|
71
71
|
AGENT_BOT_RESTART_REASON: restartReason,
|
|
72
72
|
}, restartNotificationTargets),
|
|
73
|
-
stdio: ["ignore", "ignore",
|
|
73
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
74
74
|
windowsHide: true,
|
|
75
75
|
});
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
if (workerStderr === "ignore") {
|
|
77
|
+
child.stderr?.resume();
|
|
78
|
+
} else {
|
|
79
|
+
child.stderr?.pipe(workerStderr);
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
void diagnostics.closeWorkerStderr(workerStderr);
|
|
83
|
+
throw error;
|
|
78
84
|
}
|
|
79
85
|
const workerPid = child.pid;
|
|
80
86
|
writeSupervisorLog("started", {
|
|
81
87
|
pid: child.pid,
|
|
82
88
|
restartReason,
|
|
83
89
|
restartNotificationTargets,
|
|
84
|
-
workerStderrPath: diagnostics.
|
|
90
|
+
workerStderrPath: diagnostics.currentWorkerStderrPath(),
|
|
85
91
|
crashReportDirectory: diagnostics.paths.crashReportDirectory,
|
|
86
92
|
});
|
|
87
93
|
|
|
@@ -11,10 +11,16 @@ export interface ShellCommandResult {
|
|
|
11
11
|
outputTruncated: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export type ShellCommandOutputSnapshot = Pick<
|
|
15
|
+
ShellCommandResult,
|
|
16
|
+
"stdout" | "stderr" | "outputTruncated"
|
|
17
|
+
>;
|
|
18
|
+
|
|
14
19
|
export interface ShellCommandOptions {
|
|
15
20
|
timeoutMs?: number;
|
|
16
21
|
maxOutputBytes?: number;
|
|
17
22
|
platform?: NodeJS.Platform;
|
|
23
|
+
onOutput?: (snapshot: ShellCommandOutputSnapshot) => void;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
export function executeShellCommand(
|
|
@@ -40,8 +46,16 @@ export function executeShellCommand(
|
|
|
40
46
|
child.kill();
|
|
41
47
|
}, timeoutMs);
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
const appendOutput = (stream: "stdout" | "stderr", chunk: Buffer): void => {
|
|
50
|
+
output.append(stream, chunk);
|
|
51
|
+
try {
|
|
52
|
+
options.onOutput?.(output.result());
|
|
53
|
+
} catch {
|
|
54
|
+
// Output presentation must never interrupt the child process.
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
child.stdout.on("data", (chunk: Buffer) => appendOutput("stdout", chunk));
|
|
58
|
+
child.stderr.on("data", (chunk: Buffer) => appendOutput("stderr", chunk));
|
|
45
59
|
child.once("error", (error) => {
|
|
46
60
|
clearTimeout(timeout);
|
|
47
61
|
reject(error);
|
|
@@ -68,30 +82,104 @@ function prepareShellCommand(command: string, platform: NodeJS.Platform): { comm
|
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
class BoundedOutput {
|
|
71
|
-
private
|
|
72
|
-
private readonly stderr:
|
|
73
|
-
private
|
|
85
|
+
private retained: OutputChunk[] = [];
|
|
86
|
+
private readonly seenBytes = { stdout: 0, stderr: 0 };
|
|
87
|
+
private readonly headBytes: number;
|
|
88
|
+
private readonly tailBytes: number;
|
|
74
89
|
private truncated = false;
|
|
75
90
|
|
|
76
|
-
constructor(
|
|
91
|
+
constructor(maxBytes: number) {
|
|
92
|
+
const normalizedMaxBytes = Math.max(1, Math.floor(maxBytes));
|
|
93
|
+
this.headBytes = Math.ceil(normalizedMaxBytes / 3);
|
|
94
|
+
this.tailBytes = normalizedMaxBytes - this.headBytes;
|
|
95
|
+
}
|
|
77
96
|
|
|
78
97
|
append(stream: "stdout" | "stderr", chunk: Buffer): void {
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
if (chunk.length === 0) return;
|
|
99
|
+
this.seenBytes[stream] += chunk.length;
|
|
100
|
+
|
|
101
|
+
if (!this.truncated) {
|
|
102
|
+
const next = [...this.retained, { stream, data: chunk }];
|
|
103
|
+
if (outputChunksLength(next) <= this.headBytes + this.tailBytes) {
|
|
104
|
+
this.retained = next;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
81
107
|
this.truncated = true;
|
|
108
|
+
this.retained = [
|
|
109
|
+
...takeOutputChunkHead(next, this.headBytes),
|
|
110
|
+
...takeOutputChunkTail(next, this.tailBytes),
|
|
111
|
+
];
|
|
82
112
|
return;
|
|
83
113
|
}
|
|
84
|
-
|
|
85
|
-
this
|
|
86
|
-
this.
|
|
87
|
-
|
|
114
|
+
|
|
115
|
+
const head = takeOutputChunkHead(this.retained, this.headBytes);
|
|
116
|
+
const previousTail = takeOutputChunkTail(this.retained, this.tailBytes);
|
|
117
|
+
const tail = takeOutputChunkTail([...previousTail, { stream, data: chunk }], this.tailBytes);
|
|
118
|
+
this.retained = [...head, ...tail];
|
|
88
119
|
}
|
|
89
120
|
|
|
90
|
-
result():
|
|
121
|
+
result(): ShellCommandOutputSnapshot {
|
|
91
122
|
return {
|
|
92
|
-
stdout:
|
|
93
|
-
stderr:
|
|
123
|
+
stdout: this.streamResult("stdout"),
|
|
124
|
+
stderr: this.streamResult("stderr"),
|
|
94
125
|
outputTruncated: this.truncated,
|
|
95
126
|
};
|
|
96
127
|
}
|
|
128
|
+
|
|
129
|
+
private streamResult(stream: "stdout" | "stderr"): string {
|
|
130
|
+
if (!this.truncated) {
|
|
131
|
+
return Buffer.concat(
|
|
132
|
+
this.retained.filter((chunk) => chunk.stream === stream).map((chunk) => chunk.data),
|
|
133
|
+
).toString("utf8");
|
|
134
|
+
}
|
|
135
|
+
const head = takeOutputChunkHead(this.retained, this.headBytes)
|
|
136
|
+
.filter((chunk) => chunk.stream === stream);
|
|
137
|
+
const tail = takeOutputChunkTail(this.retained, this.tailBytes)
|
|
138
|
+
.filter((chunk) => chunk.stream === stream);
|
|
139
|
+
|
|
140
|
+
const retainedBytes = outputChunksLength([...head, ...tail]);
|
|
141
|
+
const marker = this.seenBytes[stream] > retainedBytes
|
|
142
|
+
? Buffer.from("\n... output truncated ...\n", "utf8")
|
|
143
|
+
: Buffer.alloc(0);
|
|
144
|
+
return Buffer.concat([
|
|
145
|
+
...head.map((chunk) => chunk.data),
|
|
146
|
+
marker,
|
|
147
|
+
...tail.map((chunk) => chunk.data),
|
|
148
|
+
]).toString("utf8");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface OutputChunk {
|
|
153
|
+
stream: "stdout" | "stderr";
|
|
154
|
+
data: Buffer;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function outputChunksLength(chunks: OutputChunk[]): number {
|
|
158
|
+
return chunks.reduce((total, chunk) => total + chunk.data.length, 0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function takeOutputChunkHead(chunks: OutputChunk[], maxBytes: number): OutputChunk[] {
|
|
162
|
+
const retained: OutputChunk[] = [];
|
|
163
|
+
let remaining = maxBytes;
|
|
164
|
+
for (const chunk of chunks) {
|
|
165
|
+
if (remaining <= 0) break;
|
|
166
|
+
const data = chunk.data.length <= remaining ? chunk.data : chunk.data.subarray(0, remaining);
|
|
167
|
+
retained.push({ stream: chunk.stream, data });
|
|
168
|
+
remaining -= data.length;
|
|
169
|
+
}
|
|
170
|
+
return retained;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function takeOutputChunkTail(chunks: OutputChunk[], maxBytes: number): OutputChunk[] {
|
|
174
|
+
const retained: OutputChunk[] = [];
|
|
175
|
+
let remaining = maxBytes;
|
|
176
|
+
for (let index = chunks.length - 1; index >= 0 && remaining > 0; index -= 1) {
|
|
177
|
+
const chunk = chunks[index]!;
|
|
178
|
+
const data = chunk.data.length <= remaining
|
|
179
|
+
? chunk.data
|
|
180
|
+
: chunk.data.subarray(chunk.data.length - remaining);
|
|
181
|
+
retained.unshift({ stream: chunk.stream, data });
|
|
182
|
+
remaining -= data.length;
|
|
183
|
+
}
|
|
184
|
+
return retained;
|
|
97
185
|
}
|