omoclaw 3.0.3 → 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.
@@ -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): void;
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 doneByAgent = new Map;
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] ${segments.join(", ")}`
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,142 +1007,80 @@ function asObject3(value) {
974
1007
  function asString(value) {
975
1008
  return typeof value === "string" ? value : "";
976
1009
  }
977
- function collectContentText(value, output) {
978
- if (typeof value === "string") {
979
- output.push(value);
980
- return;
981
- }
982
- if (Array.isArray(value)) {
983
- for (const item of value) {
984
- collectContentText(item, output);
985
- }
986
- return;
987
- }
988
- const record = asObject3(value);
989
- if (!record) {
990
- return;
991
- }
992
- if (typeof record.text === "string") {
993
- output.push(record.text);
994
- }
995
- if (typeof record.content === "string") {
996
- output.push(record.content);
997
- } else if (Array.isArray(record.content)) {
998
- collectContentText(record.content, output);
999
- }
1000
- }
1001
- function collectPartsText(value, output) {
1002
- if (!Array.isArray(value)) {
1003
- return;
1004
- }
1005
- for (const item of value) {
1006
- const part = asObject3(item);
1007
- if (!part) {
1008
- continue;
1009
- }
1010
- const partType = asString(part.type);
1011
- if (partType === "text" && typeof part.text === "string") {
1012
- output.push(part.text);
1013
- continue;
1014
- }
1015
- if (typeof part.text === "string") {
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
1010
  function normalizePreviewText(input) {
1026
1011
  return input.replace(/\s+/g, " ").trim();
1027
1012
  }
1028
- function extractPromptPreview(info) {
1029
- const candidates = [];
1030
- const summary = asObject3(info.summary);
1031
- if (summary) {
1032
- if (typeof summary.title === "string") {
1033
- candidates.push(summary.title);
1034
- }
1035
- if (typeof summary.body === "string") {
1036
- candidates.push(summary.body);
1037
- }
1013
+
1014
+ class PromptBuffer {
1015
+ buffer = "";
1016
+ append(text) {
1017
+ this.buffer += text;
1018
+ }
1019
+ clear() {
1020
+ this.buffer = "";
1038
1021
  }
1039
- if (typeof info.text === "string") {
1040
- candidates.push(info.text);
1022
+ flush() {
1023
+ const text = normalizePreviewText(this.buffer);
1024
+ this.buffer = "";
1025
+ return text.slice(0, PROMPT_PREVIEW_MAX);
1041
1026
  }
1042
- collectContentText(info.content, candidates);
1043
- collectPartsText(info.parts, candidates);
1044
- const merged = normalizePreviewText(candidates.join(" "));
1045
- return merged.slice(0, PROMPT_PREVIEW_MAX);
1046
1027
  }
1028
+ var promptBuffer = new PromptBuffer;
1047
1029
  function handleUserEvent(event, dedup, state, notify) {
1048
1030
  if (event.type === "session.created" || event.type === "session.updated") {
1049
- const info2 = asObject3(event.properties.info);
1050
- if (!info2) {
1031
+ const info = asObject3(event.properties.info);
1032
+ if (!info)
1051
1033
  return;
1052
- }
1053
- const sessionID2 = asString(info2.id);
1054
- if (!sessionID2) {
1034
+ const sessionID = asString(info.id);
1035
+ if (!sessionID)
1055
1036
  return;
1037
+ const parentID = asString(info.parentID);
1038
+ state.setParentID(sessionID, parentID || undefined);
1039
+ return;
1040
+ }
1041
+ if (event.type === "tui.prompt.append") {
1042
+ const text = event.properties.text;
1043
+ if (typeof text === "string") {
1044
+ promptBuffer.append(text);
1056
1045
  }
1057
- const parentID = asString(info2.parentID);
1058
- state.setParentID(sessionID2, parentID || undefined);
1059
1046
  return;
1060
1047
  }
1061
1048
  if (event.type === "tui.command.execute") {
1062
1049
  const command = event.properties.command;
1063
- if (command !== "session.interrupt") {
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");
1055
+ return;
1056
+ }
1057
+ if (command === "prompt.clear") {
1058
+ promptBuffer.clear();
1064
1059
  return;
1065
1060
  }
1066
- const dedupKey2 = "user-interrupt:tui-command";
1067
- if (!dedup.shouldSend(dedupKey2)) {
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 });
1068
1069
  return;
1069
1070
  }
1070
- notify("user-interrupt", "[OpenCode] User interrupt requested");
1071
1071
  return;
1072
1072
  }
1073
1073
  if (event.type === "session.error") {
1074
- const sessionID2 = event.properties.sessionID;
1075
- const errorName = event.properties.error?.name;
1076
- if (!sessionID2 || errorName !== "MessageAbortedError") {
1074
+ const sessionID = event.properties.sessionID;
1075
+ const errorObj = event.properties.error;
1076
+ if (!sessionID || errorObj?.name !== "MessageAbortedError")
1077
1077
  return;
1078
- }
1079
- const dedupKey2 = `user-interrupt:session-error:${sessionID2}`;
1080
- if (!dedup.shouldSend(dedupKey2)) {
1078
+ const dedupKey = `user-interrupt:session-error:${sessionID}`;
1079
+ if (!dedup.shouldSend(dedupKey))
1081
1080
  return;
1082
- }
1083
- notify("user-interrupt", `[OpenCode] User interrupt (${shortSessionID3(sessionID2)})`, state.getAgent(sessionID2));
1084
- return;
1085
- }
1086
- if (event.type !== "message.updated") {
1087
- return;
1088
- }
1089
- const info = asObject3(event.properties.info);
1090
- if (!info) {
1081
+ notify("user-interrupt", `[OpenCode] User interrupt (${shortSessionID3(sessionID)})`, state.getAgent(sessionID));
1091
1082
  return;
1092
1083
  }
1093
- if (asString(info?.role) !== "user") {
1094
- return;
1095
- }
1096
- const sessionID = asString(info?.sessionID);
1097
- if (!sessionID) {
1098
- return;
1099
- }
1100
- if (state.hasParentID(sessionID)) {
1101
- return;
1102
- }
1103
- const promptPreview = extractPromptPreview(info);
1104
- if (!promptPreview) {
1105
- return;
1106
- }
1107
- const messageID = asString(info?.id);
1108
- const dedupKey = messageID ? `user-prompt:message:${messageID}` : `user-prompt:session:${sessionID}`;
1109
- if (!dedup.shouldSend(dedupKey)) {
1110
- return;
1111
- }
1112
- notify("user-prompt", `[OpenCode] User prompt submitted (${shortSessionID3(sessionID)}): ${promptPreview}`, state.getAgent(sessionID));
1113
1084
  }
1114
1085
 
1115
1086
  // src/project-config.ts
@@ -1723,6 +1694,20 @@ function buildPrefix(config) {
1723
1694
  const project = path5.basename(process.cwd());
1724
1695
  return `[OpenCode@${host}:${project}]`;
1725
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
+ }
1726
1711
  var SessionMonitorPlugin = async (ctx) => {
1727
1712
  const config = loadConfig();
1728
1713
  const projectConfig = loadProjectConfig();
@@ -1782,7 +1767,10 @@ var SessionMonitorPlugin = async (ctx) => {
1782
1767
  const batchQueue = new BatchQueue(config.batch.maxAgeMs, (payload) => {
1783
1768
  sendBatchSummary(payload.summary, payload.reason, payload.count);
1784
1769
  });
1785
- 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
+ }
1786
1774
  if (runtimeControl.isPaused()) {
1787
1775
  debugLog?.(`Suppressed event=${eventType} paused=true`);
1788
1776
  return;
@@ -1800,10 +1788,14 @@ var SessionMonitorPlugin = async (ctx) => {
1800
1788
  debugLog?.(`Queued batch event=${eventType} agent=${agentName ?? ""}`);
1801
1789
  return;
1802
1790
  }
1803
- sendRealtimeWebhook(text, eventType);
1804
1791
  if (eventType === "done") {
1805
- batchQueue.flush("done-realtime");
1792
+ const doneContext = batchQueue.getDoneRealtimeContext();
1793
+ const doneText = appendDoneRealtimeContext(text, doneContext);
1794
+ sendRealtimeWebhook(doneText, eventType);
1795
+ batchQueue.clearDoneRealtimeState();
1796
+ return;
1806
1797
  }
1798
+ sendRealtimeWebhook(text, eventType);
1807
1799
  };
1808
1800
  const timers = new TimerManager(config, sendRealtimeWebhook);
1809
1801
  const flushBatchNow = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omoclaw",
3
- "version": "3.0.3",
3
+ "version": "3.1.0",
4
4
  "description": "OpenCode session monitor plugin — native event-driven webhook notifications for session state changes",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",