omoclaw 3.0.2 → 3.1.0
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/dist/batch-queue.d.ts +8 -0
- package/dist/handlers/user-events.d.ts +3 -1
- package/dist/index.js +148 -107
- package/dist/state.d.ts +3 -0
- package/package.json +1 -1
package/dist/batch-queue.d.ts
CHANGED
|
@@ -9,14 +9,22 @@ export interface BatchFlushPayload {
|
|
|
9
9
|
count: number;
|
|
10
10
|
summary: string;
|
|
11
11
|
}
|
|
12
|
+
export interface DoneRealtimeContext {
|
|
13
|
+
promptHistory: string[];
|
|
14
|
+
batchSummary: string;
|
|
15
|
+
}
|
|
12
16
|
type BatchFlushHandler = (payload: BatchFlushPayload) => void;
|
|
13
17
|
export declare class BatchQueue {
|
|
14
18
|
private readonly maxAgeMs;
|
|
15
19
|
private readonly onFlush;
|
|
16
20
|
private readonly entries;
|
|
21
|
+
private readonly userPromptHistory;
|
|
17
22
|
private maxAgeTimer;
|
|
18
23
|
constructor(maxAgeMs: number, onFlush: BatchFlushHandler);
|
|
19
24
|
enqueue(entry: BatchQueueEntry): void;
|
|
25
|
+
recordUserPrompt(text: string): void;
|
|
26
|
+
getDoneRealtimeContext(): DoneRealtimeContext;
|
|
27
|
+
clearDoneRealtimeState(): void;
|
|
20
28
|
flush(reason: BatchFlushReason): BatchFlushPayload | undefined;
|
|
21
29
|
size(): number;
|
|
22
30
|
dispose(): void;
|
|
@@ -3,7 +3,9 @@ import type { MonitorEventType } from "../config";
|
|
|
3
3
|
import type { DedupEngine } from "../dedup";
|
|
4
4
|
import type { SessionStateTracker } from "../state";
|
|
5
5
|
interface UserEventNotifier {
|
|
6
|
-
(eventType: MonitorEventType, text: string, agentName?: string
|
|
6
|
+
(eventType: MonitorEventType, text: string, agentName?: string, options?: {
|
|
7
|
+
promptText?: string;
|
|
8
|
+
}): void;
|
|
7
9
|
}
|
|
8
10
|
export declare function handleUserEvent(event: Event, dedup: DedupEngine, state: SessionStateTracker, notify: UserEventNotifier): void;
|
|
9
11
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import os3 from "os";
|
|
|
5
5
|
import path5 from "path";
|
|
6
6
|
|
|
7
7
|
// src/batch-queue.ts
|
|
8
|
+
var USER_PROMPT_HISTORY_LIMIT = 5;
|
|
8
9
|
function formatCounts(counts) {
|
|
9
10
|
return [...counts.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([name, count]) => `${name} \xD7${count}`).join(", ");
|
|
10
11
|
}
|
|
@@ -12,11 +13,36 @@ function normalizedAgentName(agentName) {
|
|
|
12
13
|
const trimmed = agentName?.trim();
|
|
13
14
|
return trimmed ? trimmed : "unknown-agent";
|
|
14
15
|
}
|
|
16
|
+
function summarizeEntries(entries, includeCompletedSuffix) {
|
|
17
|
+
const doneByAgent = new Map;
|
|
18
|
+
const byEventType = new Map;
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (entry.eventType === "done") {
|
|
21
|
+
const agent = normalizedAgentName(entry.agentName);
|
|
22
|
+
doneByAgent.set(agent, (doneByAgent.get(agent) ?? 0) + 1);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
byEventType.set(entry.eventType, (byEventType.get(entry.eventType) ?? 0) + 1);
|
|
26
|
+
}
|
|
27
|
+
const segments = [];
|
|
28
|
+
if (doneByAgent.size > 0) {
|
|
29
|
+
const doneSummary = formatCounts(doneByAgent);
|
|
30
|
+
segments.push(includeCompletedSuffix ? `${doneSummary} completed` : doneSummary);
|
|
31
|
+
}
|
|
32
|
+
for (const [eventType, count] of [...byEventType.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
33
|
+
segments.push(`${eventType} \xD7${count}`);
|
|
34
|
+
}
|
|
35
|
+
return segments.join(", ");
|
|
36
|
+
}
|
|
37
|
+
function normalizePromptText(text) {
|
|
38
|
+
return text.replace(/\s+/g, " ").trim();
|
|
39
|
+
}
|
|
15
40
|
|
|
16
41
|
class BatchQueue {
|
|
17
42
|
maxAgeMs;
|
|
18
43
|
onFlush;
|
|
19
44
|
entries = [];
|
|
45
|
+
userPromptHistory = [];
|
|
20
46
|
maxAgeTimer;
|
|
21
47
|
constructor(maxAgeMs, onFlush) {
|
|
22
48
|
this.maxAgeMs = maxAgeMs;
|
|
@@ -31,33 +57,39 @@ class BatchQueue {
|
|
|
31
57
|
this.maxAgeTimer.unref?.();
|
|
32
58
|
}
|
|
33
59
|
}
|
|
60
|
+
recordUserPrompt(text) {
|
|
61
|
+
const normalized = normalizePromptText(text);
|
|
62
|
+
if (!normalized) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.userPromptHistory.push(normalized);
|
|
66
|
+
if (this.userPromptHistory.length > USER_PROMPT_HISTORY_LIMIT) {
|
|
67
|
+
const deleteCount = this.userPromptHistory.length - USER_PROMPT_HISTORY_LIMIT;
|
|
68
|
+
this.userPromptHistory.splice(0, deleteCount);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
getDoneRealtimeContext() {
|
|
72
|
+
return {
|
|
73
|
+
promptHistory: [...this.userPromptHistory],
|
|
74
|
+
batchSummary: summarizeEntries(this.entries, false)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
clearDoneRealtimeState() {
|
|
78
|
+
this.entries.length = 0;
|
|
79
|
+
this.userPromptHistory.length = 0;
|
|
80
|
+
this.clearTimer();
|
|
81
|
+
}
|
|
34
82
|
flush(reason) {
|
|
35
83
|
if (this.entries.length === 0) {
|
|
36
84
|
return;
|
|
37
85
|
}
|
|
38
86
|
const drained = this.entries.splice(0, this.entries.length);
|
|
39
87
|
this.clearTimer();
|
|
40
|
-
const
|
|
41
|
-
const byEventType = new Map;
|
|
42
|
-
for (const entry of drained) {
|
|
43
|
-
if (entry.eventType === "done") {
|
|
44
|
-
const agent = normalizedAgentName(entry.agentName);
|
|
45
|
-
doneByAgent.set(agent, (doneByAgent.get(agent) ?? 0) + 1);
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
byEventType.set(entry.eventType, (byEventType.get(entry.eventType) ?? 0) + 1);
|
|
49
|
-
}
|
|
50
|
-
const segments = [];
|
|
51
|
-
if (doneByAgent.size > 0) {
|
|
52
|
-
segments.push(`${formatCounts(doneByAgent)} completed`);
|
|
53
|
-
}
|
|
54
|
-
for (const [eventType, count] of [...byEventType.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
55
|
-
segments.push(`${eventType} \xD7${count}`);
|
|
56
|
-
}
|
|
88
|
+
const summaryBody = summarizeEntries(drained, true);
|
|
57
89
|
const payload = {
|
|
58
90
|
reason,
|
|
59
91
|
count: drained.length,
|
|
60
|
-
summary: `[batch] ${
|
|
92
|
+
summary: summaryBody ? `[batch] ${summaryBody}` : "[batch]"
|
|
61
93
|
};
|
|
62
94
|
this.onFlush(payload);
|
|
63
95
|
return payload;
|
|
@@ -68,6 +100,7 @@ class BatchQueue {
|
|
|
68
100
|
dispose() {
|
|
69
101
|
this.clearTimer();
|
|
70
102
|
this.entries.length = 0;
|
|
103
|
+
this.userPromptHistory.length = 0;
|
|
71
104
|
}
|
|
72
105
|
clearTimer() {
|
|
73
106
|
if (!this.maxAgeTimer) {
|
|
@@ -974,113 +1007,80 @@ function asObject3(value) {
|
|
|
974
1007
|
function asString(value) {
|
|
975
1008
|
return typeof value === "string" ? value : "";
|
|
976
1009
|
}
|
|
977
|
-
function
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
const record = asObject3(value);
|
|
989
|
-
if (!record) {
|
|
990
|
-
return;
|
|
1010
|
+
function normalizePreviewText(input) {
|
|
1011
|
+
return input.replace(/\s+/g, " ").trim();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
class PromptBuffer {
|
|
1015
|
+
buffer = "";
|
|
1016
|
+
append(text) {
|
|
1017
|
+
this.buffer += text;
|
|
991
1018
|
}
|
|
992
|
-
|
|
993
|
-
|
|
1019
|
+
clear() {
|
|
1020
|
+
this.buffer = "";
|
|
994
1021
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1022
|
+
flush() {
|
|
1023
|
+
const text = normalizePreviewText(this.buffer);
|
|
1024
|
+
this.buffer = "";
|
|
1025
|
+
return text.slice(0, PROMPT_PREVIEW_MAX);
|
|
999
1026
|
}
|
|
1000
1027
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1028
|
+
var promptBuffer = new PromptBuffer;
|
|
1029
|
+
function handleUserEvent(event, dedup, state, notify) {
|
|
1030
|
+
if (event.type === "session.created" || event.type === "session.updated") {
|
|
1031
|
+
const info = asObject3(event.properties.info);
|
|
1032
|
+
if (!info)
|
|
1033
|
+
return;
|
|
1034
|
+
const sessionID = asString(info.id);
|
|
1035
|
+
if (!sessionID)
|
|
1036
|
+
return;
|
|
1037
|
+
const parentID = asString(info.parentID);
|
|
1038
|
+
state.setParentID(sessionID, parentID || undefined);
|
|
1003
1039
|
return;
|
|
1004
1040
|
}
|
|
1005
|
-
|
|
1006
|
-
const
|
|
1007
|
-
if (
|
|
1008
|
-
|
|
1009
|
-
}
|
|
1010
|
-
const partType = asString(part.type);
|
|
1011
|
-
if (partType === "text" && typeof part.text === "string") {
|
|
1012
|
-
output.push(part.text);
|
|
1013
|
-
continue;
|
|
1041
|
+
if (event.type === "tui.prompt.append") {
|
|
1042
|
+
const text = event.properties.text;
|
|
1043
|
+
if (typeof text === "string") {
|
|
1044
|
+
promptBuffer.append(text);
|
|
1014
1045
|
}
|
|
1015
|
-
|
|
1016
|
-
output.push(part.text);
|
|
1017
|
-
}
|
|
1018
|
-
if (typeof part.content === "string") {
|
|
1019
|
-
output.push(part.content);
|
|
1020
|
-
} else if (Array.isArray(part.content)) {
|
|
1021
|
-
collectContentText(part.content, output);
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
function normalizePreviewText(input) {
|
|
1026
|
-
return input.replace(/\s+/g, " ").trim();
|
|
1027
|
-
}
|
|
1028
|
-
function extractPromptPreview(info) {
|
|
1029
|
-
const candidates = [];
|
|
1030
|
-
collectContentText(info.content, candidates);
|
|
1031
|
-
collectPartsText(info.parts, candidates);
|
|
1032
|
-
const merged = normalizePreviewText(candidates.join(" "));
|
|
1033
|
-
if (!merged) {
|
|
1034
|
-
return "[empty]";
|
|
1046
|
+
return;
|
|
1035
1047
|
}
|
|
1036
|
-
return merged.slice(0, PROMPT_PREVIEW_MAX);
|
|
1037
|
-
}
|
|
1038
|
-
function handleUserEvent(event, dedup, state, notify) {
|
|
1039
1048
|
if (event.type === "tui.command.execute") {
|
|
1040
1049
|
const command = event.properties.command;
|
|
1041
|
-
if (command
|
|
1050
|
+
if (command === "session.interrupt") {
|
|
1051
|
+
const dedupKey = "user-interrupt:tui-command";
|
|
1052
|
+
if (!dedup.shouldSend(dedupKey))
|
|
1053
|
+
return;
|
|
1054
|
+
notify("user-interrupt", "[OpenCode] User interrupt requested");
|
|
1042
1055
|
return;
|
|
1043
1056
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1057
|
+
if (command === "prompt.clear") {
|
|
1058
|
+
promptBuffer.clear();
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (command === "prompt.submit") {
|
|
1062
|
+
const preview = promptBuffer.flush();
|
|
1063
|
+
if (!preview)
|
|
1064
|
+
return;
|
|
1065
|
+
const dedupKey = `user-prompt:submit:${Date.now()}`;
|
|
1066
|
+
if (!dedup.shouldSend(dedupKey))
|
|
1067
|
+
return;
|
|
1068
|
+
notify("user-prompt", `[OpenCode] User prompt submitted: ${preview}`, undefined, { promptText: preview });
|
|
1046
1069
|
return;
|
|
1047
1070
|
}
|
|
1048
|
-
notify("user-interrupt", "[OpenCode] User interrupt requested");
|
|
1049
1071
|
return;
|
|
1050
1072
|
}
|
|
1051
1073
|
if (event.type === "session.error") {
|
|
1052
|
-
const
|
|
1053
|
-
const
|
|
1054
|
-
if (!
|
|
1074
|
+
const sessionID = event.properties.sessionID;
|
|
1075
|
+
const errorObj = event.properties.error;
|
|
1076
|
+
if (!sessionID || errorObj?.name !== "MessageAbortedError")
|
|
1055
1077
|
return;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
if (!dedup.shouldSend(dedupKey2)) {
|
|
1078
|
+
const dedupKey = `user-interrupt:session-error:${sessionID}`;
|
|
1079
|
+
if (!dedup.shouldSend(dedupKey))
|
|
1059
1080
|
return;
|
|
1060
|
-
}
|
|
1061
|
-
notify("user-interrupt", `[OpenCode] User interrupt (${shortSessionID3(sessionID2)})`, state.getAgent(sessionID2));
|
|
1062
|
-
return;
|
|
1063
|
-
}
|
|
1064
|
-
if (event.type !== "message.updated") {
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
const info = asObject3(event.properties.info);
|
|
1068
|
-
if (!info) {
|
|
1081
|
+
notify("user-interrupt", `[OpenCode] User interrupt (${shortSessionID3(sessionID)})`, state.getAgent(sessionID));
|
|
1069
1082
|
return;
|
|
1070
1083
|
}
|
|
1071
|
-
if (asString(info?.role) !== "user") {
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
const sessionID = asString(info?.sessionID);
|
|
1075
|
-
if (!sessionID) {
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
const messageID = asString(info?.id);
|
|
1079
|
-
const dedupKey = messageID ? `user-prompt:message:${messageID}` : `user-prompt:session:${sessionID}`;
|
|
1080
|
-
if (!dedup.shouldSend(dedupKey)) {
|
|
1081
|
-
return;
|
|
1082
|
-
}
|
|
1083
|
-
notify("user-prompt", `[OpenCode] User prompt submitted (${shortSessionID3(sessionID)}): ${extractPromptPreview(info)}`, state.getAgent(sessionID));
|
|
1084
1084
|
}
|
|
1085
1085
|
|
|
1086
1086
|
// src/project-config.ts
|
|
@@ -1432,6 +1432,7 @@ class SessionStateTracker {
|
|
|
1432
1432
|
this.states.set(sessionID, {
|
|
1433
1433
|
status: newStatus,
|
|
1434
1434
|
agent: undefined,
|
|
1435
|
+
parentID: undefined,
|
|
1435
1436
|
busySince: newStatus === "busy" ? now : undefined,
|
|
1436
1437
|
lastTransition: now
|
|
1437
1438
|
});
|
|
@@ -1471,6 +1472,7 @@ class SessionStateTracker {
|
|
|
1471
1472
|
this.states.set(sessionID, {
|
|
1472
1473
|
status: "unknown",
|
|
1473
1474
|
agent: agentName,
|
|
1475
|
+
parentID: undefined,
|
|
1474
1476
|
busySince: undefined,
|
|
1475
1477
|
lastTransition: Date.now()
|
|
1476
1478
|
});
|
|
@@ -1487,6 +1489,23 @@ class SessionStateTracker {
|
|
|
1487
1489
|
getState(sessionID) {
|
|
1488
1490
|
return this.states.get(sessionID);
|
|
1489
1491
|
}
|
|
1492
|
+
setParentID(sessionID, parentID) {
|
|
1493
|
+
const existing = this.states.get(sessionID);
|
|
1494
|
+
if (existing) {
|
|
1495
|
+
existing.parentID = parentID;
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
this.states.set(sessionID, {
|
|
1499
|
+
status: "unknown",
|
|
1500
|
+
agent: undefined,
|
|
1501
|
+
parentID,
|
|
1502
|
+
busySince: undefined,
|
|
1503
|
+
lastTransition: Date.now()
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
hasParentID(sessionID) {
|
|
1507
|
+
return Boolean(this.states.get(sessionID)?.parentID);
|
|
1508
|
+
}
|
|
1490
1509
|
getAgentHistory(sessionID) {
|
|
1491
1510
|
return [...this.agentHistory.get(sessionID) ?? []];
|
|
1492
1511
|
}
|
|
@@ -1499,6 +1518,7 @@ class SessionStateTracker {
|
|
|
1499
1518
|
this.states.set(sessionID, {
|
|
1500
1519
|
status: "busy",
|
|
1501
1520
|
agent: undefined,
|
|
1521
|
+
parentID: undefined,
|
|
1502
1522
|
busySince: time,
|
|
1503
1523
|
lastTransition: Date.now()
|
|
1504
1524
|
});
|
|
@@ -1674,6 +1694,20 @@ function buildPrefix(config) {
|
|
|
1674
1694
|
const project = path5.basename(process.cwd());
|
|
1675
1695
|
return `[OpenCode@${host}:${project}]`;
|
|
1676
1696
|
}
|
|
1697
|
+
function quotePrompt(text) {
|
|
1698
|
+
return `"${text.replace(/"/g, "\\\"")}"`;
|
|
1699
|
+
}
|
|
1700
|
+
function appendDoneRealtimeContext(baseText, context) {
|
|
1701
|
+
const lines = [baseText];
|
|
1702
|
+
if (context.promptHistory.length > 0) {
|
|
1703
|
+
lines.push(`- Prompts: ${context.promptHistory.map(quotePrompt).join(" -> ")}`);
|
|
1704
|
+
}
|
|
1705
|
+
if (context.batchSummary) {
|
|
1706
|
+
lines.push(`- Batch: ${context.batchSummary}`);
|
|
1707
|
+
}
|
|
1708
|
+
return lines.join(`
|
|
1709
|
+
`);
|
|
1710
|
+
}
|
|
1677
1711
|
var SessionMonitorPlugin = async (ctx) => {
|
|
1678
1712
|
const config = loadConfig();
|
|
1679
1713
|
const projectConfig = loadProjectConfig();
|
|
@@ -1733,7 +1767,10 @@ var SessionMonitorPlugin = async (ctx) => {
|
|
|
1733
1767
|
const batchQueue = new BatchQueue(config.batch.maxAgeMs, (payload) => {
|
|
1734
1768
|
sendBatchSummary(payload.summary, payload.reason, payload.count);
|
|
1735
1769
|
});
|
|
1736
|
-
const notifyByPolicy = (eventType, text, agentName) => {
|
|
1770
|
+
const notifyByPolicy = (eventType, text, agentName, options) => {
|
|
1771
|
+
if (eventType === "user-prompt" && options?.promptText) {
|
|
1772
|
+
batchQueue.recordUserPrompt(options.promptText);
|
|
1773
|
+
}
|
|
1737
1774
|
if (runtimeControl.isPaused()) {
|
|
1738
1775
|
debugLog?.(`Suppressed event=${eventType} paused=true`);
|
|
1739
1776
|
return;
|
|
@@ -1751,10 +1788,14 @@ var SessionMonitorPlugin = async (ctx) => {
|
|
|
1751
1788
|
debugLog?.(`Queued batch event=${eventType} agent=${agentName ?? ""}`);
|
|
1752
1789
|
return;
|
|
1753
1790
|
}
|
|
1754
|
-
sendRealtimeWebhook(text, eventType);
|
|
1755
1791
|
if (eventType === "done") {
|
|
1756
|
-
batchQueue.
|
|
1792
|
+
const doneContext = batchQueue.getDoneRealtimeContext();
|
|
1793
|
+
const doneText = appendDoneRealtimeContext(text, doneContext);
|
|
1794
|
+
sendRealtimeWebhook(doneText, eventType);
|
|
1795
|
+
batchQueue.clearDoneRealtimeState();
|
|
1796
|
+
return;
|
|
1757
1797
|
}
|
|
1798
|
+
sendRealtimeWebhook(text, eventType);
|
|
1758
1799
|
};
|
|
1759
1800
|
const timers = new TimerManager(config, sendRealtimeWebhook);
|
|
1760
1801
|
const flushBatchNow = () => {
|
package/dist/state.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ type SessionStatus = "idle" | "busy" | "retry" | "error" | "unknown";
|
|
|
2
2
|
interface SessionState {
|
|
3
3
|
status: SessionStatus;
|
|
4
4
|
agent: string | undefined;
|
|
5
|
+
parentID: string | undefined;
|
|
5
6
|
busySince: number | undefined;
|
|
6
7
|
lastTransition: number;
|
|
7
8
|
}
|
|
@@ -17,6 +18,8 @@ export declare class SessionStateTracker {
|
|
|
17
18
|
setAgent(sessionID: string, agentName: string): void;
|
|
18
19
|
getAgent(sessionID: string): string | undefined;
|
|
19
20
|
getState(sessionID: string): SessionState | undefined;
|
|
21
|
+
setParentID(sessionID: string, parentID: string | undefined): void;
|
|
22
|
+
hasParentID(sessionID: string): boolean;
|
|
20
23
|
getAgentHistory(sessionID: string): string[];
|
|
21
24
|
setBusySince(sessionID: string, time: number): void;
|
|
22
25
|
clearSession(sessionID: string): void;
|
package/package.json
CHANGED