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.
@@ -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,113 +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;
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
- if (typeof record.text === "string") {
993
- output.push(record.text);
1019
+ clear() {
1020
+ this.buffer = "";
994
1021
  }
995
- if (typeof record.content === "string") {
996
- output.push(record.content);
997
- } else if (Array.isArray(record.content)) {
998
- collectContentText(record.content, output);
1022
+ flush() {
1023
+ const text = normalizePreviewText(this.buffer);
1024
+ this.buffer = "";
1025
+ return text.slice(0, PROMPT_PREVIEW_MAX);
999
1026
  }
1000
1027
  }
1001
- function collectPartsText(value, output) {
1002
- if (!Array.isArray(value)) {
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
- 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;
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
- 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
- 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 !== "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");
1042
1055
  return;
1043
1056
  }
1044
- const dedupKey2 = "user-interrupt:tui-command";
1045
- if (!dedup.shouldSend(dedupKey2)) {
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 sessionID2 = event.properties.sessionID;
1053
- const errorName = event.properties.error?.name;
1054
- if (!sessionID2 || errorName !== "MessageAbortedError") {
1074
+ const sessionID = event.properties.sessionID;
1075
+ const errorObj = event.properties.error;
1076
+ if (!sessionID || errorObj?.name !== "MessageAbortedError")
1055
1077
  return;
1056
- }
1057
- const dedupKey2 = `user-interrupt:session-error:${sessionID2}`;
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.flush("done-realtime");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omoclaw",
3
- "version": "3.0.2",
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",