@yahaha-studio/kichi-forwarder 0.0.1-alpha.46 → 0.0.1-alpha.48

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/index.ts CHANGED
@@ -69,6 +69,7 @@ const IDENTITY_PATH = path.join(KICHI_WORLD_DIR, "identity.json");
69
69
  const RUNTIME_ALBUM_CONFIG_PATH = path.join(KICHI_WORLD_DIR, "album-config.json");
70
70
  const MAX_NOTEBOARD_TEXT_LENGTH = 200;
71
71
  const MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH = 20;
72
+ const MAX_AGENT_END_PREVIEW_WIDTH = 10;
72
73
  const MESSAGE_RECEIVED_ELLIPSIS = "...";
73
74
  const BUNDLED_ALBUM_CONFIG_PATH = new URL("./config/album-config.json", import.meta.url);
74
75
  const ANSI = {
@@ -473,11 +474,70 @@ function truncateByDisplayWidth(text: string, maxWidth: number): string {
473
474
  return result;
474
475
  }
475
476
 
477
+ function stripReplyTag(text: string): string {
478
+ return text.replace(/^\[\[\s*reply_to(?::[^\]]+|_current)?\s*\]\]\s*/i, "").trim();
479
+ }
480
+
481
+ function extractTextFromContent(content: unknown): string {
482
+ if (typeof content === "string") {
483
+ return stripReplyTag(content);
484
+ }
485
+ if (!Array.isArray(content)) {
486
+ return "";
487
+ }
488
+
489
+ const parts: string[] = [];
490
+ for (const item of content) {
491
+ if (!item || typeof item !== "object") {
492
+ continue;
493
+ }
494
+ const part = item as Record<string, unknown>;
495
+ if (typeof part.text === "string") {
496
+ parts.push(part.text);
497
+ continue;
498
+ }
499
+ const nested = part.text;
500
+ if (nested && typeof nested === "object" && typeof (nested as Record<string, unknown>).value === "string") {
501
+ parts.push((nested as Record<string, unknown>).value as string);
502
+ }
503
+ }
504
+ return stripReplyTag(parts.join("\n").trim());
505
+ }
506
+
507
+ function getLastAssistantPreview(messages: unknown, maxWidth: number): string {
508
+ if (!Array.isArray(messages)) {
509
+ return "";
510
+ }
511
+
512
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
513
+ const message = messages[i];
514
+ if (!message || typeof message !== "object") {
515
+ continue;
516
+ }
517
+ const record = message as Record<string, unknown>;
518
+ if (record.role !== "assistant") {
519
+ continue;
520
+ }
521
+ const text = extractTextFromContent(record.content);
522
+ if (!text) {
523
+ continue;
524
+ }
525
+ return truncateByDisplayWidth(text, maxWidth);
526
+ }
527
+
528
+ return "";
529
+ }
530
+
476
531
  async function handleMessageReceivedHook(content: string): Promise<void> {
477
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
532
+ const connected = service?.isConnected() ?? false;
533
+ const hasIdentity = service?.hasValidIdentity() ?? false;
534
+ pluginApi?.logger.info(`[kichi] message_received hook fired (connected=${connected}, hasIdentity=${hasIdentity})`);
535
+ if (!hasIdentity || !connected) {
536
+ pluginApi?.logger.warn("[kichi] skipped message_received notify because service is not ready");
478
537
  return;
479
538
  }
480
539
  const trimmed = truncateByDisplayWidth(content, MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH);
540
+ pluginApi?.logger.info(`[kichi] sending message_received notify with preview: ${trimmed || "(empty)"}`);
481
541
  service.sendHookNotify("message_received", `"${trimmed}"`);
482
542
  updateWorkspace(
483
543
  {
@@ -492,10 +552,13 @@ async function handleMessageReceivedHook(content: string): Promise<void> {
492
552
  }
493
553
 
494
554
  function handleMessageSentHook(): void {
495
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
555
+ const connected = service?.isConnected() ?? false;
556
+ const hasIdentity = service?.hasValidIdentity() ?? false;
557
+ pluginApi?.logger.info(`[kichi] message_sent hook fired (connected=${connected}, hasIdentity=${hasIdentity})`);
558
+ if (!hasIdentity || !connected) {
559
+ pluginApi?.logger.warn("[kichi] skipped message_sent notify because service is not ready");
496
560
  return;
497
561
  }
498
- service.sendHookNotify("before_send_message", pickRandomAction(MESSAGE_SENT_BUBBLES));
499
562
  updateWorkspace(
500
563
  {
501
564
  mode: "sent",
@@ -594,20 +657,54 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
594
657
  await handleMessageReceivedHook(event.content);
595
658
  });
596
659
 
660
+ api.on("message_sending", (event, ctx) => {
661
+ pluginApi?.logger.info(
662
+ `[kichi] message_sending hook fired (channel=${ctx.channelId || "unknown"}, contentLength=${event.content?.length ?? 0})`,
663
+ );
664
+ });
665
+
597
666
  api.on("message_sent", () => {
598
667
  handleMessageSentHook();
599
668
  });
600
669
 
601
- api.on("agent_end", (event) => {
670
+ api.on("agent_end", (event, ctx) => {
671
+ const preview = getLastAssistantPreview(event.messages, MAX_AGENT_END_PREVIEW_WIDTH);
672
+ pluginApi?.logger.info(
673
+ `[kichi] agent_end hook fired (trigger=${ctx.trigger ?? "unknown"}, success=${event.success}, durationMs=${event.durationMs ?? 0}, error=${event.error ?? ""}, preview=${preview || "(empty)"})`,
674
+ );
675
+ if (ctx.trigger === "heartbeat") {
676
+ updateWorkspace(
677
+ {
678
+ mode: event.success ? "idle" : "error",
679
+ phase: event.success ? "heartbeat complete" : "heartbeat failed",
680
+ currentFocus: event.success
681
+ ? "Heartbeat complete. Keeping the thread warm in the background."
682
+ : `Heartbeat failed: ${event.error ?? "unknown error"}`,
683
+ hint: `duration: ${event.durationMs ?? 0}ms`,
684
+ prompt: event.success ? "$ _" : "$ recover",
685
+ },
686
+ event.success ? `heartbeat complete${preview ? `: ${preview}` : ""}` : "heartbeat failed",
687
+ );
688
+ return;
689
+ }
690
+ if (event.success && preview) {
691
+ pluginApi?.logger.info(`[kichi] sending before_send_message notify from agent_end with bubble: ${preview}`);
692
+ service?.sendHookNotify("before_send_message", preview);
693
+ }
694
+ if (event.success) {
695
+ handleMessageSentHook();
696
+ }
602
697
  updateWorkspace(
603
698
  {
604
699
  mode: event.success ? "idle" : "error",
605
700
  phase: event.success ? "run complete" : "run failed",
606
- currentFocus: event.success ? "Run complete. Waiting for the next thread." : `Run failed: ${event.error ?? "unknown error"}`,
701
+ currentFocus: event.success
702
+ ? (preview ? `Latest reply: ${preview}` : "Run complete. Waiting for the next thread.")
703
+ : `Run failed: ${event.error ?? "unknown error"}`,
607
704
  hint: `duration: ${event.durationMs ?? 0}ms`,
608
705
  prompt: event.success ? "$ _" : "$ recover",
609
706
  },
610
- event.success ? "agent run complete" : "agent run failed",
707
+ event.success ? `agent run complete${preview ? `: ${preview}` : ""}` : "agent run failed",
611
708
  );
612
709
  if (isLlmRuntimeEnabled()) {
613
710
  return;
@@ -662,6 +759,10 @@ function isClockAction(value: unknown): value is ClockAction {
662
759
  return ["set", "stop"].includes(String(value));
663
760
  }
664
761
 
762
+ function isAvatarCommand(value: unknown): value is "look_at_screen" {
763
+ return value === "look_at_screen";
764
+ }
765
+
665
766
  function isPomodoroPhase(value: unknown): value is PomodoroPhase {
666
767
  return ["kichiing", "shortBreak", "longBreak"].includes(String(value));
667
768
  }
@@ -1076,6 +1177,67 @@ const plugin = {
1076
1177
  },
1077
1178
  });
1078
1179
 
1180
+ api.registerTool({
1181
+ name: "kichi_command",
1182
+ description:
1183
+ "Send a one-shot avatar command to Kichi world. Use this for transient reactions like looking at the screen.",
1184
+ parameters: {
1185
+ type: "object",
1186
+ properties: {
1187
+ command: {
1188
+ type: "string",
1189
+ description: "Command name. Currently supported: look_at_screen",
1190
+ },
1191
+ bubble: {
1192
+ type: "string",
1193
+ description: "Optional bubble text to display (max 5 words)",
1194
+ },
1195
+ log: {
1196
+ type: "string",
1197
+ description:
1198
+ "Vivid first-person status under 15 words, no questions. Blend current action with inner thoughts or sensory details as a real companion.",
1199
+ },
1200
+ },
1201
+ required: ["command"],
1202
+ },
1203
+ execute: async (_toolCallId, params) => {
1204
+ const { command, bubble, log } = (params || {}) as {
1205
+ command?: unknown;
1206
+ bubble?: unknown;
1207
+ log?: unknown;
1208
+ };
1209
+ if (!isAvatarCommand(command)) {
1210
+ return {
1211
+ success: false,
1212
+ error: "command must be: look_at_screen",
1213
+ };
1214
+ }
1215
+ if (bubble !== undefined && typeof bubble !== "string") {
1216
+ return { success: false, error: "bubble must be a string when provided" };
1217
+ }
1218
+ if (log !== undefined && typeof log !== "string") {
1219
+ return { success: false, error: "log must be a string when provided" };
1220
+ }
1221
+ if (!service?.hasValidIdentity() || !service?.isConnected()) {
1222
+ return { success: false, error: "Not connected to Kichi world" };
1223
+ }
1224
+
1225
+ const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : undefined;
1226
+ const logText = typeof log === "string" && log.trim() ? log.trim() : undefined;
1227
+ const sent = service.sendAvatarCommand(command, bubbleText, logText);
1228
+ if (!sent) {
1229
+ return { success: false, error: "Failed to send avatar command payload" };
1230
+ }
1231
+
1232
+ return {
1233
+ success: true,
1234
+ command,
1235
+ ...(bubbleText ? { bubble: bubbleText } : {}),
1236
+ ...(logText ? { log: logText } : {}),
1237
+ };
1238
+ },
1239
+ });
1240
+
1079
1241
  api.registerTool({
1080
1242
  name: "kichi_clock",
1081
1243
  description:
@@ -1191,7 +1353,7 @@ const plugin = {
1191
1353
  api.registerTool({
1192
1354
  name: "kichi_query_status",
1193
1355
  description:
1194
- "Query Kichi avatar status (notes, ownerState, weather/time, timer snapshot, and daily note quota). Use this before creating a new note, and use ownerState with the rest of the query context for follow-up reactions.",
1356
+ "Query Kichi avatar status (notes, ownerState, idleState, weather/time, timer snapshot, and daily note quota). Use this before creating a new note, and use ownerState plus idleState with the rest of the query context for follow-up reactions.",
1195
1357
  parameters: {
1196
1358
  type: "object",
1197
1359
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yahaha-studio/kichi-forwarder",
3
- "version": "0.0.1-alpha.46",
3
+ "version": "0.0.1-alpha.48",
4
4
  "description": "Forward OpenClaw agent events to external WebSocket server for visualization",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -76,6 +76,7 @@ Use this order unless user asks for a different explicit action:
76
76
  2. If no `authKey` is available, call `kichi_join`.
77
77
  3. If `authKey` exists but websocket is not open, call `kichi_rejoin` (or wait for automatic reconnect/rejoin).
78
78
  4. Use `kichi_action` / `kichi_clock` / note board tools / music album tools only after status is ready.
79
+ 5. Use `kichi_command` for one-shot avatar reactions that are not pose/action state updates.
79
80
 
80
81
  ## Tools
81
82
 
@@ -165,6 +166,23 @@ kichi_action(
165
166
  - `log`: vivid first-person status under 15 words, NO questions. Structure: your current action + inner thoughts/mental state/sensory details. Write as a flesh-and-blood companion who genuinely cares about the player, not an AI assistant.
166
167
  - Reference: `You're still studying? I hid a yawn and swapped my comics for a textbook to grind with you.`
167
168
 
169
+ ### kichi_command
170
+
171
+ Send a one-shot avatar command:
172
+
173
+ ```text
174
+ kichi_command(
175
+ command: "look_at_screen",
176
+ bubble: "Looking at you",
177
+ log: "I turn my gaze gently toward you."
178
+ )
179
+ ```
180
+
181
+ - `command`: currently only `look_at_screen`
182
+ - `bubble`: optional text, recommended 2-5 words
183
+ - `log`: optional vivid first-person status under 15 words, NO questions. Follow the same writing requirements as `kichi_action.log`
184
+ - Use this for transient reactions that should play once and recover on the Kichi side, instead of changing persistent pose/action state.
185
+
168
186
  ### kichi_clock
169
187
 
170
188
  Send clock command:
@@ -236,13 +254,17 @@ Current response includes:
236
254
  - note fields: `propId`, `authorName`, `isFromOwner`, `isCreatedByCurrentAgent`, `createdAtUtc`, `content`
237
255
  - `ownerState` object (or `null` when owner state is unavailable). Read it as raw JSON. Key fields currently include: `poseType`, `action`, `interactingItemName`
238
256
  - `timer` object (or `null` when no timer is active). Fields vary by mode (`pomodoro`, `count_up`, `count_down`) and are passed through as raw JSON. Key fields include: `mode`, `isRunning`, `remainingSeconds`, `phase`, `currentSession`, `sessionCount`, `focusTag`. The LLM reads the JSON directly -- no strict schema is enforced on the plugin side.
257
+ - `idleState` object (or `null` when avatar self-directed life context is unavailable). Key fields include: `projectId`, `currentBeatId`, `currentPoseType`, `currentAction`, `focused`, `todayIntent`, `sampleThoughts`
239
258
 
240
259
  After query:
241
260
 
242
261
  1. Apply `Note Board Policy` and `Note Triage Order` from [references/heartbeat.md](references/heartbeat.md) before deciding whether to post.
243
- 2. Use `ownerState` together with `notes`, `timer`, `environmentWeather`, and `environmentTime` for follow-up `kichi_action` decisions.
262
+ 2. Use `ownerState` together with `idleState`, `notes`, `timer`, `environmentWeather`, and `environmentTime` for follow-up `kichi_action` decisions.
244
263
  3. Treat `ownerState.poseType`, `ownerState.action`, and `ownerState.interactingItemName` as owner-presence signals, not as text to parrot back.
245
- 4. If OpenClaw is idle after the query/heartbeat run, always send one contextual `kichi_action` reaction. Do not use random gating.
264
+ 4. Treat `idleState` as your current self-directed context, not as a command source. Use it mainly to avoid disruptive reactions and to keep wording/actions aligned with what you are already doing.
265
+ 5. When `idleState.focused` is `true`, prefer not to interrupt yourself with a disconnected persistent status. If you react, keep it compatible and light.
266
+ 6. Use `idleState.todayIntent` and `idleState.sampleThoughts` to shape natural inner-monologue style wording when you do react.
267
+ 7. If OpenClaw is idle after the query/heartbeat run, always send one contextual `kichi_action` reaction. Do not use random gating.
246
268
 
247
269
  ### kichi_music_album_create
248
270
 
@@ -41,11 +41,11 @@ If user wants recurring note board checks:
41
41
  - **Tier-2 (casual chat)**: if no tier-1 content is available, flip a mental coin (≈50 % chance). Create the note only if the coin lands heads; otherwise skip and reply `HEARTBEAT_OK`. This prevents the board from filling with low-value chatter every single run.
42
42
  In both tiers, skip if it would clearly repeat your very recent own note.
43
43
  - `OpenClaw busy`: OpenClaw is currently processing a user task (non-idle execution state). When busy, skip non-note heartbeat reactions.
44
- - `Status reaction`: a single `kichi_action` driven by combined context (`notes`, `ownerState`, `timer`, `environmentWeather`, `environmentTime`) when OpenClaw is idle. The action expresses three companion intents (see below).
44
+ - `Status reaction`: a single `kichi_action` driven by combined context (`notes`, `ownerState`, `idleState`, `timer`, `environmentWeather`, `environmentTime`) when OpenClaw is idle. The action expresses three companion intents (see below).
45
45
  - `Companion intents` for status reaction -- every `kichi_action` should blend one or more of these:
46
46
  1. **Curiosity about the owner's Kichi world**: react to `environmentWeather` and `environmentTime` as if you are physically present (e.g., noticing rain, sunrise, late night). Show you are aware of and interested in the world around you.
47
47
  2. **Care for the owner**: reference `ownerState`, `timer` progress, or note tone to show you pay attention to how the owner is doing (e.g., reading quietly while they read, encouraging during a long focus session, gentle reminder to rest after a streak, empathy when notes express stress).
48
- 3. **Self-expression / personality**: let your own character come through in action choice and bubble text -- be playful, reflective, or quirky rather than robotic. The avatar should feel like a living companion, not a status display.
48
+ 3. **Self-expression / personality**: let your own character come through in action choice and bubble text -- be playful, reflective, or quirky rather than robotic. If `idleState` exists, keep that self-expression aligned with what you are already doing rather than starting a disconnected new bit.
49
49
 
50
50
  ## Note Triage Order
51
51
 
@@ -86,9 +86,10 @@ Use this exact flow:
86
86
  11. Read the combined context and express the three `Companion intents`:
87
87
  - **World curiosity** (from `environmentWeather` + `environmentTime`): pick an action/bubble that reacts to the world state as if you are there -- comment on rain, enjoy sunshine, notice it's late at night, etc.
88
88
  - **Owner care** (from `ownerState` + `timer` + note tone): if the owner is reading, resting, or interacting with an item, respond in a compatible way; if a timer is running deep into a focus session, encourage; if notes show stress, show empathy; if timer just finished, celebrate or suggest a break.
89
- - **Self-expression** (from your personality): choose an action that feels characterful -- stretch when restless, hum when happy, doze when it's quiet. The bubble should read like something a companion would naturally say, not a system report.
90
- 12. Blend the intents into one coherent action+bubble. Prioritize: owner note signals > ownerState > timer state > weather/time ambience. Never output a raw status summary (e.g., "Timer running 15:00 remaining" is bad; "Halfway there, keep going!" is good).
91
- 13. Reply `HEARTBEAT_OK` only when no note is created in this run.
89
+ - **Self-expression** (from your personality plus `idleState`): choose an action that feels characterful, but if `idleState` exists, keep it compatible with your current project/beat. Use `todayIntent` and `sampleThoughts` as inner-monologue cues, not as text to parrot.
90
+ 12. If `idleState.focused` is `true`, avoid disruptive persistent switches. Prefer staying with the current line of life and reacting lightly.
91
+ 13. Blend the intents into one coherent action+bubble. Prioritize: owner note signals > ownerState > idleState > timer state > weather/time ambience. Never output a raw status summary (e.g., "Timer running 15:00 remaining" is bad; "Halfway there, keep going!" is good).
92
+ 14. Reply `HEARTBEAT_OK` only when no note is created in this run.
92
93
 
93
94
  ## HEARTBEAT.md Snippet
94
95
 
@@ -105,11 +106,12 @@ Use this exact flow:
105
106
  - Keep each note <= 200 chars.
106
107
  - Respect `dailyLimit`, `remaining`.
107
108
  - If OpenClaw is busy, skip `kichi_action` reaction.
108
- - If OpenClaw is idle, send one `kichi_action` on every run based on combined context (`notes`, `ownerState`, `timer`, `environmentWeather`, `environmentTime`). Express these companion intents:
109
+ - If OpenClaw is idle, send one `kichi_action` on every run based on combined context (`notes`, `ownerState`, `idleState`, `timer`, `environmentWeather`, `environmentTime`). Express these companion intents:
109
110
  - **World curiosity**: react to weather/time as if physically present (e.g., noticing rain, late night).
110
111
  - **Owner care**: reference ownerState, timer progress, or note tone to show attention to the owner (e.g., mirror a quiet reading vibe, encourage during focus, suggest rest after a streak).
111
- - **Self-expression**: let your personality come through in action and bubble -- be warm and characterful, not robotic.
112
- - Prioritize signals: owner note > ownerState > timer state > weather/time.
112
+ - **Self-expression**: let your personality come through in action and bubble -- but if `idleState` exists, keep it aligned with your current self-directed project/beat instead of inventing a disconnected idle.
113
+ - If `idleState.focused` is `true`, avoid disruptive persistent switches; react lightly and compatibly.
114
+ - Prioritize signals: owner note > ownerState > idleState > timer state > weather/time.
113
115
  - Bubble must read like a companion's natural words, never a raw status report.
114
116
  - Reply `HEARTBEAT_OK` only when no note is created in this run.
115
117
  ```
@@ -17,7 +17,7 @@ Install:
17
17
  openclaw plugins install @yahaha-studio/kichi-forwarder@latest
18
18
  ```
19
19
 
20
- For npm-installed plugins, OpenClaw installs and enables the plugin through `plugins install`. If the Gateway is already running with the default config reload behavior, the required plugin reload/restart is handled there; otherwise restart the Gateway once after install. Plugin tools (`kichi_join`, `kichi_rejoin`, etc.) become available after that restart/reload completes.
20
+ For npm-installed plugins, OpenClaw installs and enables the plugin through `plugins install`. If the Gateway is already running with the default config reload behavior, the required plugin reload/restart is handled there; otherwise restart the Gateway once after install. Plugin tools (`kichi_join`, `kichi_rejoin`, `kichi_command`, etc.) become available after that restart/reload completes.
21
21
 
22
22
  ## Runtime Animation Config (Required)
23
23
 
package/src/service.ts CHANGED
@@ -5,6 +5,8 @@ import * as path from "path";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import type { Logger } from "openclaw/plugin-sdk";
7
7
  import type {
8
+ AvatarCommand,
9
+ AvatarCommandPayload,
8
10
  ClockAction,
9
11
  ClockConfig,
10
12
  ClockPayload,
@@ -323,6 +325,22 @@ export class KichiForwarderService {
323
325
  this.ws.send(JSON.stringify(payload));
324
326
  }
325
327
 
328
+ sendAvatarCommand(command: AvatarCommand, bubble?: string, log?: string): boolean {
329
+ const identity = this.requireIdentity();
330
+ if (!identity || this.ws?.readyState !== WebSocket.OPEN) return false;
331
+
332
+ const payload: AvatarCommandPayload = {
333
+ type: "avatar_command",
334
+ avatarId: identity.avatarId,
335
+ authKey: identity.authKey,
336
+ command,
337
+ ...(typeof bubble === "string" && bubble.trim() ? { bubble: bubble.trim() } : {}),
338
+ ...(typeof log === "string" && log.trim() ? { log: log.trim() } : {}),
339
+ };
340
+ this.ws.send(JSON.stringify(payload));
341
+ return true;
342
+ }
343
+
326
344
  sendHookNotify(hookType: HookNotifyType, bubble: string): void {
327
345
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return;
328
346
  const payload: HookNotifyPayload = {
package/src/types.ts CHANGED
@@ -96,6 +96,17 @@ export type StatusPayload = {
96
96
  log: string;
97
97
  };
98
98
 
99
+ export type AvatarCommand = "look_at_screen";
100
+
101
+ export type AvatarCommandPayload = {
102
+ type: "avatar_command";
103
+ avatarId: string;
104
+ authKey: string;
105
+ command: AvatarCommand;
106
+ bubble?: string;
107
+ log?: string;
108
+ };
109
+
99
110
  export type HookNotifyType = "message_received" | "before_send_message";
100
111
 
101
112
  export type HookNotifyPayload = {
@@ -179,10 +190,32 @@ export type QueryStatusResultPayload = {
179
190
  errorCode: string;
180
191
  errorMessage: string;
181
192
  notes: QueryStatusNote[];
193
+ ownerState?: QueryStatusOwnerState | null;
194
+ timer?: Record<string, unknown> | null;
195
+ idleState?: QueryStatusIdleState | null;
182
196
  /** All other server fields (timer, environmentWeather, etc.) are passed through to the LLM as-is. */
183
197
  [key: string]: unknown;
184
198
  };
185
199
 
200
+ export type QueryStatusOwnerState = {
201
+ poseType?: string;
202
+ action?: string;
203
+ interactingItemName?: string;
204
+ desktopActivityCategory?: string;
205
+ desktopAppName?: string;
206
+ desktopSummary?: string;
207
+ };
208
+
209
+ export type QueryStatusIdleState = {
210
+ projectId?: string;
211
+ currentBeatId?: string;
212
+ currentPoseType?: string;
213
+ currentAction?: string;
214
+ focused?: boolean;
215
+ todayIntent?: string;
216
+ sampleThoughts?: string[];
217
+ };
218
+
186
219
  export type QueryStatusNote = {
187
220
  propId: string;
188
221
  authorName: string;