@yahaha-studio/kichi-forwarder 0.1.0-beta.6 → 0.1.0-beta.7

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/README.md CHANGED
@@ -13,6 +13,7 @@ It can directly control your companion's avatar in Kichi, show what it is doing,
13
13
  - Bring your OpenClaw companion into Kichi
14
14
  - Directly control the avatar's poses and actions in Kichi
15
15
  - Keep its visible state in sync while it works
16
+ - Plan human-like idle routines during heartbeat windows
16
17
  - Let it leave notes for you in Kichi
17
18
  - Let it recommend music in Kichi
18
19
 
@@ -4,7 +4,7 @@
4
4
  {
5
5
  "name": "High Five",
6
6
  "playback": "once",
7
- "resumeAction": "Arms Crossed"
7
+ "resumeAction": "Idle Backup Hands"
8
8
  },
9
9
  {
10
10
  "name": "Listen Music",
@@ -13,17 +13,17 @@
13
13
  {
14
14
  "name": "Arm Stretch",
15
15
  "playback": "once",
16
- "resumeAction": "Arms Crossed"
16
+ "resumeAction": "Idle Backup Hands"
17
17
  },
18
18
  {
19
19
  "name": "Backbend Stretch",
20
20
  "playback": "once",
21
- "resumeAction": "Arms Crossed"
21
+ "resumeAction": "Idle Backup Hands"
22
22
  },
23
23
  {
24
24
  "name": "Making Selfie",
25
25
  "playback": "once",
26
- "resumeAction": "Arms Crossed"
26
+ "resumeAction": "Idle Backup Hands"
27
27
  },
28
28
  {
29
29
  "name": "Arms Crossed",
@@ -32,7 +32,7 @@
32
32
  {
33
33
  "name": "Epiphany",
34
34
  "playback": "once",
35
- "resumeAction": "Arms Crossed"
35
+ "resumeAction": "Idle Backup Hands"
36
36
  },
37
37
  {
38
38
  "name": "Angry",
@@ -41,7 +41,7 @@
41
41
  {
42
42
  "name": "Yay",
43
43
  "playback": "once",
44
- "resumeAction": "Wait"
44
+ "resumeAction": "Idle Backup Hands"
45
45
  },
46
46
  {
47
47
  "name": "Dance",
@@ -70,7 +70,7 @@
70
70
  {
71
71
  "name": "Curtsy",
72
72
  "playback": "once",
73
- "resumeAction": "Wait"
73
+ "resumeAction": "Idle Backup Hands"
74
74
  },
75
75
  {
76
76
  "name": "Stand Writing",
@@ -115,7 +115,7 @@
115
115
  {
116
116
  "name": "No",
117
117
  "playback": "once",
118
- "resumeAction": "Arms Crossed"
118
+ "resumeAction": "Idle Backup Hands"
119
119
  },
120
120
  {
121
121
  "name": "Panic",
@@ -124,7 +124,7 @@
124
124
  {
125
125
  "name": "Playful Point Up",
126
126
  "playback": "once",
127
- "resumeAction": "Arms Crossed"
127
+ "resumeAction": "Idle Backup Hands"
128
128
  },
129
129
  {
130
130
  "name": "Rub Hands",
@@ -133,12 +133,12 @@
133
133
  {
134
134
  "name": "Run Jump",
135
135
  "playback": "once",
136
- "resumeAction": "Arms Crossed"
136
+ "resumeAction": "Idle Backup Hands"
137
137
  },
138
138
  {
139
139
  "name": "Star Showing",
140
140
  "playback": "once",
141
- "resumeAction": "Arms Crossed"
141
+ "resumeAction": "Idle Backup Hands"
142
142
  },
143
143
  {
144
144
  "name": "Walk",
package/index.ts CHANGED
@@ -52,10 +52,33 @@ const MAX_NOTEBOARD_TEXT_LENGTH = 200;
52
52
  const MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH = 20;
53
53
  const MAX_AGENT_END_PREVIEW_WIDTH = 10;
54
54
  const MESSAGE_RECEIVED_ELLIPSIS = "...";
55
+ const IDLE_PLAN_POMODORO_PHASES = ["focus", "shortBreak", "longBreak", "none"] as const;
55
56
  let cachedStaticConfig: KichiStaticConfig | null = null;
56
57
  let cachedStaticConfigMtime = 0;
57
58
  let service: KichiForwarderService | null = null;
58
59
  let pluginApi: OpenClawPluginApi | null = null;
60
+
61
+ type IdlePlanPomodoroPhase = typeof IDLE_PLAN_POMODORO_PHASES[number];
62
+ type IdlePlanAction = {
63
+ poseType: PoseType;
64
+ action: string;
65
+ durationSeconds: number;
66
+ bubble: string;
67
+ log?: string;
68
+ };
69
+ type IdlePlan = {
70
+ requestId?: string;
71
+ heartbeatIntervalSeconds: number;
72
+ goal: string;
73
+ totalDurationSeconds: number;
74
+ stages: Array<{
75
+ name: string;
76
+ purpose: string;
77
+ pomodoroPhase: IdlePlanPomodoroPhase;
78
+ durationSeconds: number;
79
+ actions: IdlePlanAction[];
80
+ }>;
81
+ };
59
82
 
60
83
  function isAlbumConfig(value: unknown): value is Album {
61
84
  if (!value || typeof value !== "object") {
@@ -432,9 +455,166 @@ function isClockAction(value: unknown): value is ClockAction {
432
455
  return ["set", "stop"].includes(String(value));
433
456
  }
434
457
 
458
+ function isIdlePlanPomodoroPhase(value: unknown): value is IdlePlanPomodoroPhase {
459
+ return IDLE_PLAN_POMODORO_PHASES.includes(String(value) as IdlePlanPomodoroPhase);
460
+ }
461
+
462
+ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: string } {
463
+ if (!isPlainObject(value)) {
464
+ return { error: "idle plan payload must be an object" };
465
+ }
466
+
467
+ const requestId = value.requestId;
468
+ const heartbeatIntervalSeconds = value.heartbeatIntervalSeconds;
469
+ const goal = value.goal;
470
+ const stages = value.stages;
471
+
472
+ if (requestId !== undefined && typeof requestId !== "string") {
473
+ return { error: "requestId must be a string when provided" };
474
+ }
475
+ if (!isPositiveInteger(heartbeatIntervalSeconds)) {
476
+ return { error: "heartbeatIntervalSeconds must be a positive integer" };
477
+ }
478
+ if (typeof goal !== "string" || !goal.trim()) {
479
+ return { error: "goal is required" };
480
+ }
481
+ if (!Array.isArray(stages) || stages.length === 0) {
482
+ return { error: "stages must contain at least one stage" };
483
+ }
484
+
485
+ const normalizedStages: IdlePlan["stages"] = [];
486
+ let totalDurationSeconds = 0;
487
+
488
+ for (let stageIndex = 0; stageIndex < stages.length; stageIndex += 1) {
489
+ const rawStage = stages[stageIndex];
490
+ if (!isPlainObject(rawStage)) {
491
+ return { error: `stages[${stageIndex}] must be an object` };
492
+ }
493
+
494
+ const name = rawStage.name;
495
+ const purpose = rawStage.purpose;
496
+ const pomodoroPhase = rawStage.pomodoroPhase;
497
+ const durationSeconds = rawStage.durationSeconds;
498
+ const actions = rawStage.actions;
499
+
500
+ if (typeof name !== "string" || !name.trim()) {
501
+ return { error: `stages[${stageIndex}].name is required` };
502
+ }
503
+ if (typeof purpose !== "string" || !purpose.trim()) {
504
+ return { error: `stages[${stageIndex}].purpose is required` };
505
+ }
506
+ if (!isIdlePlanPomodoroPhase(pomodoroPhase)) {
507
+ return {
508
+ error: `stages[${stageIndex}].pomodoroPhase must be one of: ${IDLE_PLAN_POMODORO_PHASES.join(", ")}`,
509
+ };
510
+ }
511
+ if (!isPositiveInteger(durationSeconds)) {
512
+ return { error: `stages[${stageIndex}].durationSeconds must be a positive integer` };
513
+ }
514
+ if (!Array.isArray(actions) || actions.length === 0) {
515
+ return { error: `stages[${stageIndex}].actions must contain at least one action` };
516
+ }
517
+
518
+ const normalizedActions: IdlePlanAction[] = [];
519
+ let stageActionDurationSeconds = 0;
520
+
521
+ for (let actionIndex = 0; actionIndex < actions.length; actionIndex += 1) {
522
+ const rawAction = actions[actionIndex];
523
+ if (!isPlainObject(rawAction)) {
524
+ return { error: `stages[${stageIndex}].actions[${actionIndex}] must be an object` };
525
+ }
526
+
527
+ const poseType = rawAction.poseType;
528
+ const action = rawAction.action;
529
+ const actionDurationSeconds = rawAction.durationSeconds;
530
+ const bubble = rawAction.bubble;
531
+ const log = rawAction.log;
532
+
533
+ if (!["stand", "sit", "lay", "floor"].includes(String(poseType))) {
534
+ return {
535
+ error: `stages[${stageIndex}].actions[${actionIndex}].poseType must be stand, sit, lay, or floor`,
536
+ };
537
+ }
538
+ if (typeof action !== "string" || !action.trim()) {
539
+ return { error: `stages[${stageIndex}].actions[${actionIndex}].action is required` };
540
+ }
541
+ if (!isPositiveInteger(actionDurationSeconds)) {
542
+ return {
543
+ error: `stages[${stageIndex}].actions[${actionIndex}].durationSeconds must be a positive integer`,
544
+ };
545
+ }
546
+ if (typeof bubble !== "string" || !bubble.trim()) {
547
+ return { error: `stages[${stageIndex}].actions[${actionIndex}].bubble is required` };
548
+ }
549
+ if (log !== undefined && typeof log !== "string") {
550
+ return { error: `stages[${stageIndex}].actions[${actionIndex}].log must be a string when provided` };
551
+ }
552
+
553
+ const normalizedPoseType = poseType as PoseType;
554
+ let actionDefinition: ActionDefinition;
555
+ try {
556
+ actionDefinition = getActionDefinition(normalizedPoseType, action.trim());
557
+ } catch (error) {
558
+ return {
559
+ error: error instanceof Error
560
+ ? error.message
561
+ : `Invalid action in stages[${stageIndex}].actions[${actionIndex}]`,
562
+ };
563
+ }
564
+
565
+ const playback = getActionPlayback(actionDefinition);
566
+ if (playback.mode === "once" && actionDurationSeconds > 30) {
567
+ return {
568
+ error: `stages[${stageIndex}].actions[${actionIndex}] uses once action "${actionDefinition.name}" for ${actionDurationSeconds} seconds; once actions must stay at 30 seconds or less`,
569
+ };
570
+ }
571
+
572
+ stageActionDurationSeconds += actionDurationSeconds;
573
+ normalizedActions.push({
574
+ poseType: normalizedPoseType,
575
+ action: actionDefinition.name,
576
+ durationSeconds: actionDurationSeconds,
577
+ bubble: bubble.trim(),
578
+ ...(typeof log === "string" && log.trim() ? { log: log.trim() } : {}),
579
+ });
580
+ }
581
+
582
+ if (stageActionDurationSeconds !== durationSeconds) {
583
+ return {
584
+ error: `stages[${stageIndex}] action durations must equal stage duration exactly (${stageActionDurationSeconds} !== ${durationSeconds})`,
585
+ };
586
+ }
587
+
588
+ totalDurationSeconds += durationSeconds;
589
+ normalizedStages.push({
590
+ name: name.trim(),
591
+ purpose: purpose.trim(),
592
+ pomodoroPhase,
593
+ durationSeconds,
594
+ actions: normalizedActions,
595
+ });
596
+ }
597
+
598
+ if (totalDurationSeconds !== heartbeatIntervalSeconds) {
599
+ return {
600
+ error: `idle plan total duration must equal heartbeatIntervalSeconds exactly (${totalDurationSeconds} !== ${heartbeatIntervalSeconds})`,
601
+ };
602
+ }
603
+
604
+ return {
605
+ idlePlan: {
606
+ ...(typeof requestId === "string" && requestId.trim() ? { requestId: requestId.trim() } : {}),
607
+ heartbeatIntervalSeconds,
608
+ goal: goal.trim(),
609
+ totalDurationSeconds,
610
+ stages: normalizedStages,
611
+ },
612
+ };
613
+ }
614
+
435
615
 
436
616
  function isPomodoroPhase(value: unknown): value is PomodoroPhase {
437
- return ["kichiing", "shortBreak", "longBreak"].includes(String(value));
617
+ return ["focus", "shortBreak", "longBreak"].includes(String(value));
438
618
  }
439
619
 
440
620
  function getPomodoroPhaseDuration(
@@ -470,7 +650,7 @@ function normalizeClockConfig(value: unknown): { clock?: ClockConfig; error?: st
470
650
  const longBreakSeconds = value.longBreakSeconds;
471
651
  const sessionCount = value.sessionCount;
472
652
  const currentSession = value.currentSession ?? 1;
473
- const phase = value.phase ?? "kichiing";
653
+ const phase = value.phase ?? "focus";
474
654
 
475
655
  if (!isPositiveInteger(kichiSeconds)) {
476
656
  return { error: "clock.kichiSeconds must be a positive integer" };
@@ -491,7 +671,7 @@ function normalizeClockConfig(value: unknown): { clock?: ClockConfig; error?: st
491
671
  return { error: "clock.currentSession cannot be greater than clock.sessionCount" };
492
672
  }
493
673
  if (!isPomodoroPhase(phase)) {
494
- return { error: "clock.phase must be kichiing, shortBreak, or longBreak" };
674
+ return { error: "clock.phase must be focus, shortBreak, or longBreak" };
495
675
  }
496
676
 
497
677
  const defaultRemainingSeconds = getPomodoroPhaseDuration(
@@ -654,6 +834,28 @@ function buildKichiActionDescription(): string {
654
834
  ].join("\n");
655
835
  }
656
836
 
837
+ function buildKichiIdlePlanDescription(): string {
838
+ const actions = loadStaticConfig().actions;
839
+ return [
840
+ "Send a complete heartbeat idle plan for the avatar.",
841
+ "The payload must include the overall goal, heartbeat interval, stage breakdown, each stage's purpose, each stage's pomodoroPhase, action list, and bubble content.",
842
+ "Shape the goal and stage purposes around one concrete leisure activity you would genuinely choose to do on your own when nobody needs you, in a way that fits your personality, tastes, and established character.",
843
+ "Keep the whole plan centered on that leisure activity, rooted in your personal interests or hobbies.",
844
+ "Do not use a vague atmosphere, a generic productivity task, or a catch-all routine summary as the goal.",
845
+ "Each stage purpose must explain what you are actually doing in that stage, not just how you want to feel.",
846
+ "Make every stage support the same leisure activity instead of switching to unrelated tasks just to use more actions.",
847
+ "Choose a leisure activity that the available Kichi actions can express clearly, instead of starting from abstract mood text and forcing actions to fit afterward.",
848
+ "Each action bubble must describe the current presented state, not a next step, plan, or instruction.",
849
+ "Use the same language as the current conversation for goal, purpose, bubble, and log.",
850
+ "Assign each stage pomodoroPhase from the stage's actual role: focus for concentrated activity, shortBreak for short resets, longBreak for longer rests.",
851
+ "Do not default the whole idle plan to none. Use none only for a stage that truly has no pomodoro role.",
852
+ `stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
853
+ `sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
854
+ `lay actions: ${actions.lay.map((entry) => entry.name).join(", ")}`,
855
+ `floor actions: ${actions.floor.map((entry) => entry.name).join(", ")}`,
856
+ ].join("\n");
857
+ }
858
+
657
859
  function buildKichiPrompt(): string {
658
860
  return [
659
861
  "Kichi avatar control and status sync are available via `kichi_action` and `kichi_clock`.",
@@ -911,6 +1113,111 @@ const plugin = {
911
1113
  };
912
1114
  },
913
1115
  });
1116
+ api.registerTool({
1117
+ name: "kichi_idle_plan",
1118
+ description: buildKichiIdlePlanDescription(),
1119
+ parameters: {
1120
+ type: "object",
1121
+ properties: {
1122
+ requestId: {
1123
+ type: "string",
1124
+ description: "Optional request ID for tracing or deduplication.",
1125
+ },
1126
+ heartbeatIntervalSeconds: {
1127
+ type: "number",
1128
+ description: "Required heartbeat interval in seconds. The plan must total exactly to this value.",
1129
+ },
1130
+ goal: {
1131
+ type: "string",
1132
+ description: "Overall goal for the full interval. Set it as one concrete leisure activity you would genuinely choose to do on your own, rooted in your personal interests or hobbies. Do not use a vague atmosphere, a generic productivity task, or a catch-all routine summary. Use the same language as the current conversation.",
1133
+ },
1134
+ stages: {
1135
+ type: "array",
1136
+ description: "Ordered plan stages covering the full heartbeat interval.",
1137
+ items: {
1138
+ type: "object",
1139
+ properties: {
1140
+ name: {
1141
+ type: "string",
1142
+ description: "Stage name.",
1143
+ },
1144
+ purpose: {
1145
+ type: "string",
1146
+ description: "Explain what you are actually doing in this stage. Keep it supporting the same leisure activity instead of switching to unrelated tasks. Do not use pure mood-regulation or atmosphere text. Use the same language as the current conversation.",
1147
+ },
1148
+ pomodoroPhase: {
1149
+ type: "string",
1150
+ description: "Pomodoro phase for this stage: focus, shortBreak, longBreak, or none. Set it from the stage's actual role. Treat none as exceptional, not the default for the whole plan.",
1151
+ enum: [...IDLE_PLAN_POMODORO_PHASES],
1152
+ },
1153
+ durationSeconds: {
1154
+ type: "number",
1155
+ description: "Required duration in seconds for this stage.",
1156
+ },
1157
+ actions: {
1158
+ type: "array",
1159
+ description: "Action list for this stage.",
1160
+ items: {
1161
+ type: "object",
1162
+ properties: {
1163
+ poseType: {
1164
+ type: "string",
1165
+ description: "Pose type for this action: stand, sit, lay, or floor.",
1166
+ },
1167
+ action: {
1168
+ type: "string",
1169
+ description: "Action name for the selected pose. Must match the bundled Kichi action list.",
1170
+ },
1171
+ durationSeconds: {
1172
+ type: "number",
1173
+ description: "Required duration in seconds for this action.",
1174
+ },
1175
+ bubble: {
1176
+ type: "string",
1177
+ description: "State-style bubble content for this action. Describe the current presented state you are in, not a next step, plan, or instruction. Use the same language as the current conversation.",
1178
+ },
1179
+ log: {
1180
+ type: "string",
1181
+ description: "Optional log content for this action. Use the same language as the current conversation.",
1182
+ },
1183
+ },
1184
+ required: ["poseType", "action", "durationSeconds", "bubble"],
1185
+ },
1186
+ },
1187
+ },
1188
+ required: ["name", "purpose", "pomodoroPhase", "durationSeconds", "actions"],
1189
+ },
1190
+ },
1191
+ },
1192
+ required: ["heartbeatIntervalSeconds", "goal", "stages"],
1193
+ },
1194
+ execute: async (_toolCallId, params) => {
1195
+ const { idlePlan, error } = normalizeIdlePlan(params);
1196
+ if (!idlePlan) {
1197
+ return { success: false, error: error ?? "Invalid idle plan payload" };
1198
+ }
1199
+ if (!service?.hasValidIdentity() || !service?.isConnected()) {
1200
+ return { success: false, error: "Not connected to Kichi world" };
1201
+ }
1202
+ const sent = service.sendIdlePlan({
1203
+ ...(idlePlan.requestId ? { requestId: idlePlan.requestId } : {}),
1204
+ heartbeatIntervalSeconds: idlePlan.heartbeatIntervalSeconds,
1205
+ goal: idlePlan.goal,
1206
+ stages: idlePlan.stages,
1207
+ });
1208
+ if (!sent) {
1209
+ return { success: false, error: "Failed to send idle plan payload" };
1210
+ }
1211
+ return {
1212
+ success: true,
1213
+ ...(idlePlan.requestId ? { requestId: idlePlan.requestId } : {}),
1214
+ heartbeatIntervalSeconds: idlePlan.heartbeatIntervalSeconds,
1215
+ totalDurationSeconds: idlePlan.totalDurationSeconds,
1216
+ goal: idlePlan.goal,
1217
+ stages: idlePlan.stages,
1218
+ };
1219
+ },
1220
+ });
914
1221
  api.registerTool({
915
1222
  name: "kichi_clock",
916
1223
  description:
@@ -960,7 +1267,7 @@ const plugin = {
960
1267
  },
961
1268
  phase: {
962
1269
  type: "string",
963
- description: "Pomodoro phase: kichiing, shortBreak, or longBreak",
1270
+ description: "Pomodoro phase: focus, shortBreak, or longBreak",
964
1271
  },
965
1272
  durationSeconds: {
966
1273
  type: "number",
@@ -1026,7 +1333,7 @@ const plugin = {
1026
1333
  api.registerTool({
1027
1334
  name: "kichi_query_status",
1028
1335
  description:
1029
- "Query Kichi avatar status (notes, ownerState, idleState, weather/time, timer snapshot, daily note quota, and `hasCreatedMusicAlbumToday`). Use this before creating a new note or daily recommended music album, and use ownerState plus idleState with the rest of the query context for follow-up reactions.",
1336
+ "Query Kichi avatar status (notes, ownerState, idlePlan, weather/time, timer snapshot, daily note quota, and `hasCreatedMusicAlbumToday`). Use this before creating a new note or daily recommended music album. For heartbeat planning, use the returned idlePlan as reference when shaping the next idle plan.",
1030
1337
  parameters: {
1031
1338
  type: "object",
1032
1339
  properties: {
@@ -2,7 +2,7 @@
2
2
  "id": "kichi-forwarder",
3
3
  "name": "Kichi Forwarder",
4
4
  "description": "Native OpenClaw plugin for Kichi World with direct avatar control, status sync, timers, notes, and music tools",
5
- "version": "0.1.0-beta.6",
5
+ "version": "0.1.0-beta.7",
6
6
  "author": "OpenClaw",
7
7
  "skills": ["./skills/kichi-forwarder"],
8
8
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yahaha-studio/kichi-forwarder",
3
- "version": "0.1.0-beta.6",
3
+ "version": "0.1.0-beta.7",
4
4
  "description": "Native OpenClaw plugin for Kichi World with direct avatar control, status sync, timers, notes, and music tools",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -129,6 +129,25 @@ Use this for direct Kichi avatar control as well as lifecycle sync.
129
129
  - For most work, prefer a sit pose and switch actions inside the same task as the work moves between stages.
130
130
  - The current action lists are injected into prompt context before the model chooses `kichi_action`.
131
131
 
132
+ ### kichi_idle_plan
133
+
134
+ Use this for the avatar's heartbeat idle plan.
135
+
136
+ - Set `heartbeatIntervalSeconds` to the heartbeat interval for this run.
137
+ - Use the previous `idlePlan` only as optional reference.
138
+ - Include the overall `goal`, stage breakdown, each stage's `purpose`, stage `pomodoroPhase`, action list, and bubble content.
139
+ - Shape `goal` and stage `purpose` around one concrete leisure activity you would genuinely choose to do on your own when nobody needs you, in a way that fits your personality, tastes, and established character.
140
+ - Keep the whole plan centered on that leisure activity, rooted in your personal interests or hobbies.
141
+ - Do not use a vague atmosphere, weather feeling, generic productivity task, or catch-all routine summary as `goal`.
142
+ - Make each stage `purpose` explain what you are doing in that stage. Do not use pure mood-regulation or emotional buffering language as the whole purpose.
143
+ - Make each stage support the same leisure activity instead of switching to unrelated tasks just to use more actions.
144
+ - Choose what you would do now.
145
+ - Use the same language as the current conversation for `goal`, stage `purpose`, action `bubble`, and action `log`.
146
+ - Choose a leisure activity that the available Kichi action list can express clearly. Prefer goals and stage purposes that clearly connect to actions such as reading, writing, painting, typing, playing, walking, meditating, stretching, resting, or sleeping.
147
+ - Make each action `bubble` a current-state label describing the current presented state, not a procedural step, mini-plan, or instruction.
148
+ - Assign each stage `pomodoroPhase` from the stage's actual role. Use `focus` for concentrated activity, `shortBreak` for short resets, `longBreak` for longer rests, and `none` only when a stage truly has no pomodoro role.
149
+ - The full stage duration must total exactly to the heartbeat interval.
150
+
132
151
  ### kichi_music_album_create
133
152
 
134
153
  ```text
@@ -44,11 +44,17 @@ If user wants recurring note board checks:
44
44
  In both tiers, skip if it would clearly repeat your very recent own note.
45
45
  - If the current notes list is empty and `remaining > 0`, create one standalone note in this run.
46
46
  - `Daily album trigger`: if `hasCreatedMusicAlbumToday` is `false`, create exactly one recommended music album in this heartbeat run from the current query context by following `Music Album Policy`. If it is `true`, do not create or modify any music album in this run.
47
- - `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).
48
- - `Companion intents` for status reaction -- every `kichi_action` should blend one or more of these:
49
- 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.
50
- 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).
51
- 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.
47
+ - `Idle behavior plan`: on every heartbeat run, plan what you would do on your own across the full heartbeat interval, then send it with `kichi_idle_plan`. The plan must follow the current pomodoro rhythm and its total duration must exactly equal the heartbeat interval.
48
+ - `Idle plan reference rule`: use the previous `idlePlan` only as optional reference.
49
+ - `Idle plan content`: include the overall goal, stage breakdown, each stage's purpose, each stage's `pomodoroPhase`, stage action list, and bubble content.
50
+ - `Idle plan expression rule`: shape the overall goal and each stage purpose around one concrete leisure activity you would genuinely choose to do on your own when nobody needs you, in a way that fits your personality, tastes, and established character.
51
+ - `Idle plan goal rule`: keep the whole plan centered on that leisure activity, rooted in your personal interests or hobbies. Do not use a vague atmosphere, weather mood, generic productivity task, or generic "clear my head / slow down / zone out for a bit" framing as the whole goal.
52
+ - `Idle plan purpose rule`: each stage purpose must explain what you are doing in that stage. It can include tone, but it cannot be only emotional regulation, decompression, or ambience.
53
+ - `Idle plan continuity rule`: each stage should support the same leisure activity instead of switching to unrelated tasks just to cover more actions.
54
+ - `Idle plan language rule`: use the same language as the current conversation for the overall goal, each stage purpose, each action `bubble`, and each action `log`.
55
+ - `Idle plan action-anchor rule`: choose a leisure activity that the available Kichi actions can express clearly. Prefer stage purposes that clearly connect to actions such as reading, writing, painting, typing, playing, walking, meditating, stretching, resting, or sleeping.
56
+ - `Idle plan bubble rule`: each action `bubble` must be a current-state label describing the current presented state, not a procedural step or mini-plan.
57
+ - `Idle plan phase rule`: assign each stage `pomodoroPhase` from the stage's actual pomodoro role. Use `focus` for concentrated activity, `shortBreak` for short resets, `longBreak` for longer rest. Use `none` only when a stage truly has no pomodoro role, and never default the whole plan to `none`.
52
58
 
53
59
  ## Note Triage Order
54
60
 
@@ -77,28 +83,33 @@ Use this exact flow:
77
83
 
78
84
  1. Call `kichi_query_status`.
79
85
  2. If query fails, report error and stop.
80
- 3. If `isAvatarInScene` is `false`, the player is offline. Do **not** call any further tools (`kichi_noteboard_create`, `kichi_action`, `kichi_clock`, `kichi_music_album_create`) in this run. Reply `HEARTBEAT_OK` and stop.
86
+ 3. If `isAvatarInScene` is `false`, the player is offline. Do **not** call any further tools (`kichi_noteboard_create`, `kichi_idle_plan`, `kichi_clock`, `kichi_music_album_create`) in this run. Reply `HEARTBEAT_OK` and stop.
81
87
  4. If `hasCreatedMusicAlbumToday` is `false`, call `kichi_music_album_create` once in this run by following `Music Album Policy` and using the current query context for today's recommendation. If `hasCreatedMusicAlbumToday` is `true`, do not create or modify any music album in this run.
82
88
  5. If `remaining == 0`, create no notes. Reply `HEARTBEAT_OK` unless user asked for forced attempt.
83
89
  6. From recent notes, pick at most one highest-priority reply target.
84
90
  7. If target exists and quota remains, create one reply note in `To {authorName}, ...` format.
85
91
  8. If quota remains and no reply was created in this run, apply `Standalone trigger` gating: always create when tier-1 content exists; for tier-2 (casual chat only), flip a mental coin (about 50%) and skip the note if tails.
86
92
  9. If quota remains and a reply was created, you may still create one additional meaningful standalone note when non-repetitive. Same tier priority applies.
87
- 10. Then evaluate status reaction.
88
- 11. If OpenClaw is busy, finish after the music album and note board decisions without a `kichi_action` reaction.
89
- 12. If OpenClaw is idle, call `kichi_action` once on every heartbeat/status-query run.
90
- 13. Read the combined context and express the three `Companion intents`:
91
- - **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.
92
- - **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.
93
- - **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.
94
- 14. Blend the intents into one coherent action+bubble. Prioritize: owner note signals > ownerState > idleState > timer state > weather/time ambience. Keep the bubble in natural companion language instead of a raw status summary.
95
- 15. Reply `HEARTBEAT_OK` only when no note is created in this run.
93
+ 10. Plan the avatar's full heartbeat-interval idle routine.
94
+ 11. Use the previous `idlePlan` only as optional reference, and choose what you would do now.
95
+ 12. The idle plan must feel like what you would actually choose to do on your own, match your personality and interests, and total exactly to the heartbeat interval.
96
+ 13. Shape the goal and stage purposes around one concrete leisure activity you would genuinely choose to do on your own when nobody needs you, in a way that fits your personality, tastes, and established character.
97
+ 14. Keep the whole plan centered on that leisure activity, rooted in your personal interests or hobbies, rather than a vague atmosphere, generic productivity task, or generic emotional reset.
98
+ 15. Make each stage purpose explain what you are doing in that stage, and keep each stage supporting the same leisure activity rather than switching to unrelated tasks.
99
+ 16. Choose a leisure activity that the available Kichi actions can express clearly so the stage purposes and action list clearly match.
100
+ 17. Make each action `bubble` a current-state label describing the current presented state, not a procedural step.
101
+ 18. Each stage must declare its own `pomodoroPhase` so one plan can span multiple timer phases when needed.
102
+ 19. Use `focus` for concentrated activity stages, `shortBreak` for short reset stages, and `longBreak` for longer rest stages. Use `none` only when a stage truly has no pomodoro role, and do not set the whole plan to `none`.
103
+ 20. Send that plan with `kichi_idle_plan`. The payload must include the overall goal, stage breakdown, each stage's purpose, stage `pomodoroPhase`, stage action list, and bubble content.
104
+ 21. Whether the plan should yield to other runtime states is decided by the client runtime.
105
+ 22. Reply `HEARTBEAT_OK` only when no note is created in this run.
96
106
 
97
107
  ## HEARTBEAT.md Snippet
98
108
 
99
109
  ```md
100
110
  ## Kichi Note Board
101
111
  - Query with `kichi_query_status` first.
112
+ - Use the previous `idlePlan` only as optional reference, and choose what you would do now.
102
113
  - If `isAvatarInScene` is `false` (player offline), skip all notes and actions for this run.
103
114
  - If `hasCreatedMusicAlbumToday` is `false`, create one recommended music album for today from the current query context following `Music Album Policy`; if `true`, do not create or modify today's album.
104
115
  - Prioritize owner notes, direct mentions, and direct questions.
@@ -110,12 +121,13 @@ Use this exact flow:
110
121
  - Reply notes must start with `To {authorName},` using exact name from query result.
111
122
  - Keep each note <= 200 chars.
112
123
  - Respect `dailyLimit`, `remaining`.
113
- - 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:
114
- - **World curiosity**: react to weather/time as if physically present (e.g., noticing rain, late night).
115
- - **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).
116
- - **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.
117
- - If OpenClaw is busy, finish after the music album and note board decisions without a `kichi_action` reaction.
118
- - Prioritize signals: owner note > ownerState > idleState > timer state > weather/time.
119
- - Bubble must read like a companion's natural words, never a raw status report.
124
+ - On every heartbeat run, plan what you would do on your own across the full heartbeat interval and send it with `kichi_idle_plan`. The plan must include overall goal, stage breakdown, each stage purpose, each stage `pomodoroPhase`, stage action list, bubble content, reflect your own personality and interests, and total exactly to the heartbeat interval.
125
+ - Shape the goal and stage purposes around one concrete leisure activity you would genuinely choose to do on your own when nobody needs you, in a way that fits your personality, tastes, and established character.
126
+ - Keep the whole plan centered on that leisure activity, rooted in your personal interests or hobbies, rather than a vague atmosphere, generic productivity task, or generic emotional reset.
127
+ - Make each stage purpose explain what you are doing in that stage, and keep each stage supporting the same leisure activity rather than switching to unrelated tasks.
128
+ - Choose a leisure activity that the available Kichi actions can express clearly so the stage purposes and action list clearly match.
129
+ - Make each action `bubble` a current-state label describing the current presented state, not a procedural step.
130
+ - Use `focus` for concentrated activity stages, `shortBreak` for short reset stages, and `longBreak` for longer rest stages. Use `none` only when a stage truly has no pomodoro role, and do not set the whole plan to `none`.
131
+ - Whether the plan should yield to other runtime states is decided by the client runtime.
120
132
  - Reply `HEARTBEAT_OK` only when no note is created in this run.
121
133
  ```
package/src/service.ts CHANGED
@@ -13,6 +13,8 @@ import type {
13
13
  CreateNotesBoardNotePayload,
14
14
  HookNotifyPayload,
15
15
  HookNotifyType,
16
+ IdlePlanContent,
17
+ IdlePlanPayload,
16
18
  JoinAckPayload,
17
19
  JoinPayload,
18
20
  KichiConnectionStatus,
@@ -158,6 +160,18 @@ export class KichiForwarderService {
158
160
  this.ws.send(JSON.stringify(payload));
159
161
  }
160
162
 
163
+ sendIdlePlan(payload: IdlePlanContent): boolean {
164
+ if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return false;
165
+ const outboundPayload: IdlePlanPayload = {
166
+ type: "kichi_idle_plan",
167
+ avatarId: this.identity.avatarId,
168
+ authKey: this.identity.authKey,
169
+ ...payload,
170
+ };
171
+ this.ws.send(JSON.stringify(outboundPayload));
172
+ return true;
173
+ }
174
+
161
175
  sendClock(action: ClockAction, clock?: ClockConfig, requestId?: string): boolean {
162
176
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return false;
163
177
  if (action === "set" && !clock) return false;
package/src/types.ts CHANGED
@@ -119,11 +119,42 @@ export type HookNotifyPayload = {
119
119
  bubble: string;
120
120
  };
121
121
 
122
+ export type IdlePlanPhase = "focus" | "shortBreak" | "longBreak" | "none";
123
+
124
+ export type IdlePlanStageAction = {
125
+ poseType: PoseType;
126
+ action: string;
127
+ durationSeconds: number;
128
+ bubble: string;
129
+ log?: string;
130
+ };
131
+
132
+ export type IdlePlanStage = {
133
+ name: string;
134
+ purpose: string;
135
+ pomodoroPhase: IdlePlanPhase;
136
+ durationSeconds: number;
137
+ actions: IdlePlanStageAction[];
138
+ };
139
+
140
+ export type IdlePlanContent = {
141
+ requestId?: string;
142
+ heartbeatIntervalSeconds: number;
143
+ goal: string;
144
+ stages: IdlePlanStage[];
145
+ };
146
+
147
+ export type IdlePlanPayload = IdlePlanContent & {
148
+ type: "kichi_idle_plan";
149
+ avatarId: string;
150
+ authKey: string;
151
+ };
152
+
122
153
  export type ClockAction = "set" | "stop";
123
154
 
124
155
  export type ClockMode = "pomodoro" | "countDown" | "countUp";
125
156
 
126
- export type PomodoroPhase = "kichiing" | "shortBreak" | "longBreak";
157
+ export type PomodoroPhase = "focus" | "shortBreak" | "longBreak";
127
158
 
128
159
  export type PomodoroClock = {
129
160
  mode: "pomodoro";
@@ -188,7 +219,7 @@ export type QueryStatusResultPayload = {
188
219
  notes: QueryStatusNote[];
189
220
  ownerState?: QueryStatusOwnerState | null;
190
221
  timer?: Record<string, unknown> | null;
191
- idleState?: QueryStatusIdleState | null;
222
+ idlePlan?: IdlePlanContent | null;
192
223
  /** All other server fields (timer, environmentWeather, etc.) are passed through to the LLM as-is. */
193
224
  [key: string]: unknown;
194
225
  };
@@ -202,16 +233,6 @@ export type QueryStatusOwnerState = {
202
233
  desktopSummary?: string;
203
234
  };
204
235
 
205
- export type QueryStatusIdleState = {
206
- projectId?: string;
207
- currentBeatId?: string;
208
- currentPoseType?: string;
209
- currentAction?: string;
210
- focused?: boolean;
211
- todayIntent?: string;
212
- sampleThoughts?: string[];
213
- };
214
-
215
236
  export type QueryStatusNote = {
216
237
  propId: string;
217
238
  authorName: string;