@keyou007/agent-bot 0.1.14 → 0.1.15-alpha.1
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 +21 -0
- package/README.md +29 -24
- package/README.zh.md +29 -24
- package/config.example.yaml +1 -0
- package/dist/cli/FeishuAppConfiguration.js +42 -3
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/help.js +6 -6
- package/dist/cli/initOptions.js +1 -4
- package/dist/cli/initOptions.js.map +1 -1
- package/dist/cli.js +8 -5
- package/dist/cli.js.map +1 -1
- package/dist/feishu/CardRenderer.js +91 -45
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +39 -12
- 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 +108 -4
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +24 -1
- 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/feishu/InboundText.js +13 -0
- package/dist/feishu/InboundText.js.map +1 -0
- package/dist/feishu/MergedForwardMessage.js +317 -0
- package/dist/feishu/MergedForwardMessage.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/presentation/OutboundRouter.js +21 -0
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +536 -58
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +161 -3
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +12 -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/skills/agent-bot/SKILL.md +1 -1
- package/src/cli/FeishuAppConfiguration.ts +76 -3
- package/src/cli/help.ts +6 -6
- package/src/cli/initOptions.ts +1 -10
- package/src/cli.ts +10 -6
- package/src/feishu/CardRenderer.ts +116 -45
- package/src/feishu/FeishuConnector.ts +55 -13
- package/src/feishu/FeishuMarkdown.ts +64 -5
- package/src/feishu/FeishuMessageClient.ts +151 -12
- package/src/feishu/FeishuTurnPresenter.ts +28 -4
- package/src/feishu/FinalResponseBranding.ts +18 -0
- package/src/feishu/InboundText.ts +12 -0
- package/src/feishu/MergedForwardMessage.ts +372 -0
- package/src/feishu/types.ts +15 -0
- package/src/logging/DailyLogStream.ts +88 -0
- package/src/logging/logger.ts +2 -1
- package/src/presentation/OutboundRouter.ts +28 -0
- package/src/proxy/ProxySessionController.ts +642 -55
- package/src/state/StateStore.ts +230 -2
- package/src/state/migrations.ts +12 -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
|
@@ -64,6 +64,17 @@ export interface TurnAnchorRecord {
|
|
|
64
64
|
contextKey?: string;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export interface AgentBotTurnMessageRecord extends TurnAnchorRecord {
|
|
68
|
+
messageKind: "progress" | "final";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface GoalCardDeliveryRecord {
|
|
72
|
+
localSessionId: string;
|
|
73
|
+
contextKey: string;
|
|
74
|
+
messageId: string;
|
|
75
|
+
updatedAt: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
export interface TurnRuntimeOriginRecord {
|
|
68
79
|
turnId: string;
|
|
69
80
|
localSessionId: string;
|
|
@@ -84,6 +95,7 @@ export interface QueuedPromptRecord {
|
|
|
84
95
|
localSessionId: string;
|
|
85
96
|
contextKey: string;
|
|
86
97
|
text: string;
|
|
98
|
+
displayPrompt?: string;
|
|
87
99
|
localImagePaths?: string[];
|
|
88
100
|
messageId?: string;
|
|
89
101
|
replyMessageId?: string;
|
|
@@ -181,6 +193,7 @@ interface QueuedPromptRow {
|
|
|
181
193
|
local_session_id: string;
|
|
182
194
|
context_key: string;
|
|
183
195
|
prompt_text: string;
|
|
196
|
+
display_prompt: string | null;
|
|
184
197
|
local_image_paths_json: string;
|
|
185
198
|
message_id: string | null;
|
|
186
199
|
reply_message_id: string | null;
|
|
@@ -221,6 +234,7 @@ export class StateStore {
|
|
|
221
234
|
this.ensureTurnSnapshotColumns();
|
|
222
235
|
this.ensureChatContextColumns();
|
|
223
236
|
this.ensureTurnAttemptColumns();
|
|
237
|
+
this.ensureQueuedPromptColumns();
|
|
224
238
|
this.initializeContextSessionMappings();
|
|
225
239
|
this.db.exec("DROP INDEX IF EXISTS idx_sessions_remote_session_id_unique");
|
|
226
240
|
this.reconcileDuplicateRemoteSessions();
|
|
@@ -235,6 +249,46 @@ export class StateStore {
|
|
|
235
249
|
`);
|
|
236
250
|
}
|
|
237
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
|
+
|
|
238
292
|
close(): void {
|
|
239
293
|
this.db.close();
|
|
240
294
|
}
|
|
@@ -374,6 +428,7 @@ export class StateStore {
|
|
|
374
428
|
localSessionId: string;
|
|
375
429
|
contextKey: string;
|
|
376
430
|
text: string;
|
|
431
|
+
displayPrompt?: string;
|
|
377
432
|
localImagePaths?: string[];
|
|
378
433
|
messageId?: string;
|
|
379
434
|
replyMessageId?: string;
|
|
@@ -381,14 +436,15 @@ export class StateStore {
|
|
|
381
436
|
const createdAt = new Date().toISOString();
|
|
382
437
|
this.db.prepare(`
|
|
383
438
|
INSERT INTO queued_prompts (
|
|
384
|
-
prompt_id, local_session_id, context_key, prompt_text, local_image_paths_json,
|
|
439
|
+
prompt_id, local_session_id, context_key, prompt_text, display_prompt, local_image_paths_json,
|
|
385
440
|
message_id, reply_message_id, created_at
|
|
386
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
441
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
387
442
|
`).run(
|
|
388
443
|
input.promptId,
|
|
389
444
|
input.localSessionId,
|
|
390
445
|
input.contextKey,
|
|
391
446
|
input.text,
|
|
447
|
+
input.displayPrompt ?? null,
|
|
392
448
|
JSON.stringify(input.localImagePaths ?? []),
|
|
393
449
|
input.messageId ?? null,
|
|
394
450
|
input.replyMessageId ?? null,
|
|
@@ -1049,6 +1105,91 @@ export class StateStore {
|
|
|
1049
1105
|
}));
|
|
1050
1106
|
}
|
|
1051
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
|
+
|
|
1052
1193
|
private backfillTurnParents(localSessionId: string): void {
|
|
1053
1194
|
const turns = this.db.prepare(`
|
|
1054
1195
|
SELECT
|
|
@@ -1586,6 +1727,35 @@ export class StateStore {
|
|
|
1586
1727
|
: undefined;
|
|
1587
1728
|
}
|
|
1588
1729
|
|
|
1730
|
+
findAgentBotTurnMessageById(messageId: string): AgentBotTurnMessageRecord | undefined {
|
|
1731
|
+
const row = this.db.prepare(`
|
|
1732
|
+
SELECT deliveries.turn_id, snapshots.local_session_id,
|
|
1733
|
+
CASE WHEN deliveries.progress_message_id = ? THEN 'progress' ELSE 'final' END AS message_kind
|
|
1734
|
+
FROM turn_deliveries AS deliveries
|
|
1735
|
+
JOIN turn_snapshots AS snapshots ON snapshots.turn_id = deliveries.turn_id
|
|
1736
|
+
WHERE deliveries.progress_message_id = ?
|
|
1737
|
+
OR EXISTS (
|
|
1738
|
+
SELECT 1
|
|
1739
|
+
FROM json_each(deliveries.final_message_ids_json)
|
|
1740
|
+
WHERE json_each.value = ?
|
|
1741
|
+
)
|
|
1742
|
+
ORDER BY deliveries.updated_at DESC
|
|
1743
|
+
LIMIT 1
|
|
1744
|
+
`).get(messageId, messageId, messageId) as {
|
|
1745
|
+
turn_id: string;
|
|
1746
|
+
local_session_id: string;
|
|
1747
|
+
message_kind: "progress" | "final";
|
|
1748
|
+
} | undefined;
|
|
1749
|
+
return row
|
|
1750
|
+
? {
|
|
1751
|
+
turnId: row.turn_id,
|
|
1752
|
+
localSessionId: row.local_session_id,
|
|
1753
|
+
contextKey: this.getTurnContextKey(row.turn_id),
|
|
1754
|
+
messageKind: row.message_kind,
|
|
1755
|
+
}
|
|
1756
|
+
: undefined;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1589
1759
|
private initializeContextSessionMappings(): void {
|
|
1590
1760
|
this.db.exec(`
|
|
1591
1761
|
INSERT OR IGNORE INTO context_sessions (context_key, local_session_id, created_at, updated_at)
|
|
@@ -1749,6 +1919,15 @@ export class StateStore {
|
|
|
1749
1919
|
this.db.exec("ALTER TABLE turn_attempts ADD COLUMN retry_count INTEGER NOT NULL DEFAULT 0");
|
|
1750
1920
|
}
|
|
1751
1921
|
}
|
|
1922
|
+
|
|
1923
|
+
private ensureQueuedPromptColumns(): void {
|
|
1924
|
+
const existing = new Set(
|
|
1925
|
+
(this.db.pragma("table_info(queued_prompts)") as Array<{ name: string }>).map((column) => column.name),
|
|
1926
|
+
);
|
|
1927
|
+
if (!existing.has("display_prompt")) {
|
|
1928
|
+
this.db.exec("ALTER TABLE queued_prompts ADD COLUMN display_prompt TEXT");
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1752
1931
|
}
|
|
1753
1932
|
|
|
1754
1933
|
const MAX_TASK_TITLE_LENGTH = 120;
|
|
@@ -1826,6 +2005,20 @@ function mapMessageReaction(row: MessageReactionRow): MessageReactionRecord {
|
|
|
1826
2005
|
};
|
|
1827
2006
|
}
|
|
1828
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
|
+
|
|
1829
2022
|
function mapQueuedPrompt(row: QueuedPromptRow): QueuedPromptRecord {
|
|
1830
2023
|
const parsedPaths = JSON.parse(row.local_image_paths_json) as unknown;
|
|
1831
2024
|
const localImagePaths = Array.isArray(parsedPaths)
|
|
@@ -1836,6 +2029,7 @@ function mapQueuedPrompt(row: QueuedPromptRow): QueuedPromptRecord {
|
|
|
1836
2029
|
localSessionId: row.local_session_id,
|
|
1837
2030
|
contextKey: row.context_key,
|
|
1838
2031
|
text: row.prompt_text,
|
|
2032
|
+
displayPrompt: row.display_prompt ?? undefined,
|
|
1839
2033
|
localImagePaths: localImagePaths.length > 0 ? localImagePaths : undefined,
|
|
1840
2034
|
messageId: row.message_id ?? undefined,
|
|
1841
2035
|
replyMessageId: row.reply_message_id ?? undefined,
|
|
@@ -1867,6 +2061,40 @@ function mapTurnAttempt(row: TurnAttemptRow): TurnAttemptRecord {
|
|
|
1867
2061
|
};
|
|
1868
2062
|
}
|
|
1869
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
|
+
|
|
1870
2098
|
function isIncompleteTurnAttemptStatus(status: TurnAttemptStatus): boolean {
|
|
1871
2099
|
return status === "accepted" || status === "running" || status === "recovering";
|
|
1872
2100
|
}
|
package/src/state/migrations.ts
CHANGED
|
@@ -118,6 +118,7 @@ export const migrations = [
|
|
|
118
118
|
local_session_id TEXT NOT NULL,
|
|
119
119
|
context_key TEXT NOT NULL,
|
|
120
120
|
prompt_text TEXT NOT NULL,
|
|
121
|
+
display_prompt TEXT,
|
|
121
122
|
local_image_paths_json TEXT NOT NULL DEFAULT '[]',
|
|
122
123
|
message_id TEXT,
|
|
123
124
|
reply_message_id TEXT,
|
|
@@ -172,4 +173,15 @@ export const migrations = [
|
|
|
172
173
|
CREATE INDEX IF NOT EXISTS idx_turn_attempts_turn
|
|
173
174
|
ON turn_attempts(turn_id);
|
|
174
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
|
+
`,
|
|
175
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
|
}
|