zigmini 0.8.0-beta.2 → 0.8.0-beta.4

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/docs/manual.md ADDED
@@ -0,0 +1,382 @@
1
+ English | [한국어](manual.ko.md)
2
+
3
+ # zigmini user manual
4
+
5
+ ## 1. Introduction
6
+
7
+ zigmini is a personal AI agent gateway that runs on your Mac. You talk with the agent, give it work, and get notifications. The default agent is called "Zig". You can change that name in the profile.
8
+
9
+ zigmini has two surfaces.
10
+
11
+ | Surface | Description |
12
+ | --- | --- |
13
+ | Web screen | Open it in a browser on the Mac. It has chat, the inbox (확인함), work status and settings. |
14
+ | Android app | Use it on your phone. It has chat, the inbox, work status and notifications. Register the phone with the gateway first. |
15
+
16
+ ## 2. Web screen
17
+
18
+ ### 2.1 Layout
19
+
20
+ The web screen has three parts.
21
+
22
+ - Left sidebar: the conversation list, the new conversation button (+), the work status area, your profile and the agent profile. The "omo" badge at the top shows the agent engine connection.
23
+ - Center chat: the header, the message list and the input box.
24
+ - Right panels: the inbox, settings and the event panel open here.
25
+
26
+ The header has these items. Labels are shown in Korean on screen.
27
+
28
+ | Item | What it does |
29
+ | --- | --- |
30
+ | Zig face button | Shows work status. At phone width it switches to the work status view. |
31
+ | Model picker | Picks the model the agent uses. If the switch can't happen right away, it happens after the conversation is compacted on your next message. |
32
+ | 새 세션 (new session) | Starts a fresh agent session for this conversation. A confirmation comes first. The history on screen stays, and part of the recent conversation is handed to the new session. |
33
+ | 음성 켜짐 / 음성 꺼짐 (voice on/off) | Turns the gateway's voice servers on or off. With voice off, the mic, wake word, clap detection and spoken answers are unavailable. |
34
+ | 음성 설치 (install voice) | Shown only on an npm install without voice. It downloads and installs the voice servers. |
35
+ | 박수 감지 (clap detection) | Two claps turn wake word listening on or off. |
36
+ | 호출어 (wake word) | When it hears "Hey Zig", it writes down what you say into the input box. |
37
+ | 음성 답변 (spoken answers) | Reads answers aloud. |
38
+ | 확인함 (inbox) | Opens questions and approval requests waiting for you. The badge shows how many need you. |
39
+ | 이벤트 (events) | Opens the event panel. Its 지우기 button clears the events shown. |
40
+ | 설정 (settings) | Opens settings. |
41
+
42
+ Notice lines can also show up in the header.
43
+
44
+ - "모델 계정에 로그인하세요": you aren't logged in to a model account.
45
+ - Schema notice: the agent engine's data format is newer than the gateway's.
46
+ - New version notice: click it to open the update area in settings.
47
+ - "세션 교체 중": a session switch is running. You can't send messages meanwhile.
48
+
49
+ The ▲ button beside the message list jumps to your previous message. The ▼ button jumps to your next one. Double-click ▼ to jump to the bottom.
50
+
51
+ ### 2.2 Chat
52
+
53
+ 1. Type a message in the input box.
54
+ 2. Press Enter or click 전송 (send).
55
+
56
+ Shift+Enter starts a new line. Enter pressed during Korean IME composition doesn't send.
57
+
58
+ While the agent works, the 중단 (stop) button shows. Click it to stop the running work.
59
+
60
+ To start a new conversation, click + in the sidebar. A prompt asks for its name.
61
+
62
+ #### Slash command menu
63
+
64
+ Type `/` in the input box to open the command and skill menu. Keep typing to narrow the list. The menu holds skills, commands, templates and web Zig commands.
65
+
66
+ Web Zig commands:
67
+
68
+ | Command | What it does |
69
+ | --- | --- |
70
+ | `/tts on`, `/tts off` | Turns spoken answers on or off. |
71
+ | `/wake on`, `/wake off` | Turns wake word listening on or off. |
72
+ | `/clap on`, `/clap off` | Turns clap detection on or off. |
73
+ | `/voice on`, `/voice off` | Turns the voice feature (voice servers) on or off. |
74
+ | `/옵션` | Shows the state of voice, spoken answers, wake word and clap detection. |
75
+ | `/read` | Reads the whole last answer aloud. |
76
+ | `/기기` | Lists registered devices. Mac only. |
77
+ | `/기기 삭제 <number>` | Deletes the registered device with that number. Mac only. |
78
+
79
+ With voice off, the voice commands leave the menu. Typed by hand, they get the reply "음성 기능이 꺼져 있습니다" (voice is off).
80
+
81
+ The README's Commands section says that sending `/usage` in chat makes the gateway reply with the model account's usage.
82
+
83
+ ### 2.3 Attachments and references
84
+
85
+ #### Images
86
+
87
+ - Click 이미지 (image) to pick images.
88
+ - Paste images into the input box.
89
+ - Drop image files on the message list or the input area.
90
+
91
+ You can send up to 4 images at once: PNG, JPEG, GIF or WebP. Images upload when you send.
92
+
93
+ #### Referencing earlier messages
94
+
95
+ Right-click a message to attach it as a reference tag above the input box. The message's 참조 (reference) button does the same. You can add up to 4 references. Images from referenced messages plus new attachments can't go over 4 in one send. To drop a tag, click its button, or focus it and press Delete or Backspace.
96
+
97
+ #### Memo
98
+
99
+ The 쪽지 (memo) button opens and closes the memo. Keep frequent prompts or notes there. It saves on its own. 이미지 보관 keeps up to 2 images in it.
100
+
101
+ #### Auto copy
102
+
103
+ Select text in the message list with the mouse and it's copied right away. A short "복사했습니다" (copied) notice appears. Text selected inside the input box isn't auto copied.
104
+
105
+ ### 2.4 Last sent strip
106
+
107
+ A line under the header shows the last message you sent in this conversation. Image and reference counts show too. Click the line to expand the full text. Click again to collapse it.
108
+
109
+ ### 2.5 Recalling sent messages
110
+
111
+ In the input box, ArrowUp brings back earlier messages you sent. ArrowDown walks forward again. Past the newest one, your unsent draft comes back.
112
+
113
+ It only works under these rules.
114
+
115
+ - ArrowUp works only with the caret on the first line.
116
+ - ArrowDown works only while recalling, with the caret on the last line.
117
+ - With Ctrl, Cmd, Alt or Shift held, it does nothing.
118
+ - With text selected, or during IME composition, it does nothing.
119
+ - While the slash menu is open, the arrows move in the menu.
120
+
121
+ ### 2.6 Inbox (확인함)
122
+
123
+ The inbox collects the questions and approval requests the agent sends you. Open it with the header's 확인함 button or Ctrl+C. The badge counts items that need you.
124
+
125
+ Each item shows some of these buttons, depending on its state.
126
+
127
+ | Button | What it does |
128
+ | --- | --- |
129
+ | Primary button (e.g. 답변 보내기, send answer) | Sends the answer you picked. |
130
+ | 다시 보내기 (send again) | Resends the same answer when delivery is uncertain. |
131
+ | 현재 상황 확인 (check current status) | Rechecks where the asking work stands now. |
132
+ | 최신 내용 확인 (review latest) | When the question changed, review the latest version so you can answer again. |
133
+ | 삭제 (delete) | Removes the item from the inbox. A second confirm step follows. 그대로 두기 keeps it. |
134
+ | 원문 보기 (view original) | Jumps to the original question card in chat. |
135
+
136
+ Expand 대화 맥락 (conversation context) to see the related chat. Deleting a question that's still waiting sends a cancel to the work that asked it. With a long list, click 더 보기 (more).
137
+
138
+ You can also answer from the question card in chat. Both places show the same state.
139
+
140
+ ### 2.7 Work status view
141
+
142
+ The work status area sits in the sidebar. Zig is in the middle, and each work you gave out shows as a slot. A slot shows the work title, state and time. An empty slot reads "빈 칸".
143
+
144
+ - Click a finished work's slot to clear it. With the keyboard, focus the slot and press Enter or Space.
145
+ - The fold button hides and shows the work status area.
146
+ - The gateway keeps the slots, so the web and the app show the same board.
147
+
148
+ At phone width the sidebar is hidden. Click the Zig face in the header to switch to a view with only work status. The top left button, the browser's Back, or Esc returns to chat.
149
+
150
+ ### 2.8 Settings
151
+
152
+ Open it with the header's 설정 button. Close it with Esc, the × button, or a click outside the card.
153
+
154
+ #### Profile
155
+
156
+ The profile sits at the bottom of the sidebar.
157
+
158
+ | Target | How | Rules |
159
+ | --- | --- | --- |
160
+ | Your nickname | Double-click your profile. | Up to 20 characters. Blank becomes "나" (me). |
161
+ | Agent name | Double-click the agent profile. | Up to 10 Korean or 20 Latin characters. Too long and it isn't changed. Blank becomes "지그" (Zig). |
162
+ | Agent icon | Double-click the agent icon. | PNG, JPEG or WebP, 1 MB at most. The center is cropped to a square. You can go back to the default icon. |
163
+
164
+ In a name field, Enter saves and Esc cancels. Leaving the field also saves. Names are stored on the gateway. If that fails, only this browser shows the new name.
165
+
166
+ #### Update (업데이트)
167
+
168
+ - The current and new versions are shown.
169
+ - 지금 확인 (check now) looks for a new version right away. On failure it shows why and asks you to try again.
170
+ - 변경 내용 보기 (view changes) shows when there's a new version.
171
+ - The 업데이트 (update) button shows only when there's a new version and the install came from npm. A repository install doesn't get it.
172
+
173
+ The update button is held, or asks for more, in these cases.
174
+
175
+ | Condition | What you see |
176
+ | --- | --- |
177
+ | Inbox questions are waiting for you | The button is disabled. A line says N questions are waiting, with a 확인함 열기 (open inbox) button. Answer them, then update. |
178
+ | The permission notice changed | The new notice shows. Type the consent word (동의) in the field to go on. |
179
+ | Shared data format change | The changes are shown and you confirm them. |
180
+ | Work is running | A notice says the gateway is busy. |
181
+
182
+ An update restarts the gateway and the bridge. Connections and running chats may drop for a moment. Progress shows below. When it's done, reload the page.
183
+
184
+ #### Device registration (기기 등록)
185
+
186
+ - A phone registration QR shows. Each QR works once, for 10 minutes.
187
+ - When it expires, click 새로 만들기 (make new).
188
+ - The 등록된 기기 (registered devices) list shows each device's name, model and last connection.
189
+ - To remove a device, click 등록 취소, then click again when it reads 다시 누르면 취소.
190
+
191
+ When a phone asks to register, a request line appears above the chat with the device name and a check number. Make sure it matches the number on the phone, then click 승인 (approve). Without approval in 30 seconds it's denied. After repeated denials or expiries, registration locks. Click 잠금 해제 (unlock) then.
192
+
193
+ #### External access (외부 접속)
194
+
195
+ Pick how the phone reaches the gateway.
196
+
197
+ | Choice | Meaning |
198
+ | --- | --- |
199
+ | 집 Wi-Fi만 (home Wi-Fi only) | Only on the same Wi-Fi. |
200
+ | Tailscale | Install Tailscale on the Mac and the phone, logged in to the same account. |
201
+ | 자기 도메인 (your own https domain) | Enter your https address. |
202
+
203
+ Then click 바꾸기 (change).
204
+
205
+ #### App download (앱 내려받기)
206
+
207
+ A QR for the Android app (APK) shows. Scan it with the phone camera, get the APK and install it. The phone must allow installs from unknown sources. When the QR expires, click 새로 만들기.
208
+
209
+ #### Cleanup (정리)
210
+
211
+ The gateway lists things that are safe to delete: DB backups, OMO version folders, leftover update files, temp folders and old RPC daemons. Tick the ones to remove and click 선택한 항목 정리 (clean selected).
212
+
213
+ #### Voice settings
214
+
215
+ Voice settings live in the header buttons, not in the settings card. See the table in 2.1 and section 2.9.
216
+
217
+ ### 2.9 Voice on the web
218
+
219
+ Voice features work only while voice is on (음성 켜짐).
220
+
221
+ - Mic: click 마이크 and speak. Click again to send.
222
+ - Spoken answers: when on, answers are read aloud.
223
+ - Wake word: when on, it waits for "Hey Zig". What you say next fills the input box and is sent after a few seconds. The send button counts down. Press Esc or edit the input to cancel the auto send.
224
+ - "Hey hey hey": stops the running work and waits for your next words.
225
+ - "귀 닫아" (close your ears): turns wake word listening off.
226
+ - Clap detection: when on, two claps turn wake word listening on or off.
227
+
228
+ ### 2.10 Viewing the manual
229
+
230
+ - Web: click the 매뉴얼 (manual) button at the top of the settings dialog. The Korean version opens first. The "English" button or the link on the first line of the document switches to the English version. Esc, the close button (×) or a click outside the dialog closes it.
231
+ - App: short press the header status tag (for example "작업 상태 확인 중") to open the gateway connection screen, then tap its 매뉴얼 button. The "English" button switches to the English version. 닫기 (close) or Back returns to the connection screen.
232
+ - When the gateway does not serve the manual, the notice "이 게이트웨이 버전에는 매뉴얼이 없습니다" (this gateway version has no manual) shows. Update the gateway to read it.
233
+
234
+ ## 3. Keyboard shortcuts
235
+
236
+ | Key | Where | What it does |
237
+ | --- | --- | --- |
238
+ | Ctrl+E | Anywhere | Opens or closes the event panel. |
239
+ | Ctrl+C | Anywhere, with no text selected | Opens or closes the inbox. With text selected, it copies as usual. |
240
+ | Enter | Input box | Sends the message. Not during IME composition. |
241
+ | Shift+Enter | Input box | New line. |
242
+ | Esc | Input box | Cancels the wake word auto send. |
243
+ | ArrowUp | Input box, first line | Recalls an earlier sent message. |
244
+ | ArrowDown | Input box, last line, while recalling | Goes to a newer message or back to your draft. |
245
+ | ArrowUp / ArrowDown | Slash menu | Moves through items. |
246
+ | Tab, Enter | Slash menu | Puts the chosen item in the input box. |
247
+ | Esc | Slash menu | Closes the menu. |
248
+ | Enter, Space | Last sent strip | Expands or collapses it. |
249
+ | Delete, Backspace | Reference tag | Removes the reference. |
250
+ | Enter, Space | Finished work slot in work status | Clears the slot. |
251
+ | Esc | Work status view (phone width) | Returns to chat. |
252
+ | Esc | Inbox | Closes the inbox. |
253
+ | Tab, Shift+Tab | Inbox | Focus cycles inside the inbox. |
254
+ | Esc | Settings | Closes settings. |
255
+ | Tab, Shift+Tab | Settings | Focus cycles inside settings. |
256
+ | Esc | Manual | Closes the manual and returns to settings. |
257
+ | Tab, Shift+Tab | Manual | Focus cycles inside the manual. |
258
+ | Enter | Nickname or agent name field | Saves. |
259
+ | Esc | Nickname or agent name field | Closes without change. |
260
+ | Esc | Agent icon editor | Closes without change. |
261
+
262
+ Ctrl shortcuts do nothing with Cmd, Alt or Shift held. They match by key position, so a Korean layout works too.
263
+
264
+ ## 4. Android app
265
+
266
+ ### 4.1 Connections
267
+
268
+ The app header shows the Zig face and a status tag on the left. The tag reads one of "게이트웨이 재연결 중" (reconnecting), "omo 끊김" (omo disconnected), "omo 연결됨" (omo connected) or "작업 상태 확인 중" (checking work status).
269
+
270
+ | Action | What it does |
271
+ | --- | --- |
272
+ | Tap the Zig face | Switches between chat and the work status screen. |
273
+ | Short press the status tag | Opens the gateway connection screen. It shows the app version, the gateway version and the 매뉴얼 (manual) button. |
274
+ | Long press the status tag | Opens the app version dialog. |
275
+
276
+ The connection screen manages registered gateways.
277
+
278
+ - 선택 (select) switches to that gateway. The one in use shows "사용 중".
279
+ - 삭제 (delete), then 삭제 확인 (confirm), removes the gateway from the phone.
280
+
281
+ ### 4.2 Device registration
282
+
283
+ There are two ways to register.
284
+
285
+ 1. Tap QR로 등록 (register by QR) and scan the registration QR from web settings.
286
+ 2. Or paste the gateway address and the registration token, then tap 등록 (register).
287
+
288
+ Then approve the request on the Mac's web screen within 30 seconds. Check that the numbers match.
289
+
290
+ ### 4.3 Inbox (확인함)
291
+
292
+ Open it from the inbox icon in the header. The top shows counts for "확인 필요" (needs you) and "전달 중" (delivering). Required questions carry a 필수 tag. You must answer all required questions before you can send.
293
+
294
+ - Pick an answer and send it.
295
+ - 삭제 removes the item after a confirm step. For a question still waiting, the button reads 질문 취소하고 삭제 (cancel question and delete).
296
+ - 원문 보기 jumps to the question card in chat.
297
+ - While disconnected, the list is the last one received. Answers can be sent after reconnecting.
298
+
299
+ ### 4.4 Work status screen
300
+
301
+ Tap the Zig face to open it. Slot states include running, waiting for input, waiting, waiting for subtasks, unresponsive, broken, completed, failed and cancelled.
302
+
303
+ - Tap a finished slot to clear it.
304
+ - Long press a slot to show the work's name.
305
+ - ID 복사 copies the work ID.
306
+
307
+ ### 4.5 Update and download
308
+
309
+ - New gateway version: a notice bar shows. On an npm install it has an 업데이트 button that asks for confirmation. If inbox questions are waiting, the button is disabled and 확인함 열기 shows. ✕ hides the bar until the next connection.
310
+ - New app version: tap 내려받기 (download) in the "새 앱 버전" row to get the APK in the browser. The phone must allow installs from unknown sources.
311
+
312
+ ### 4.6 Notifications
313
+
314
+ - Each registered gateway has its own notification connection.
315
+ - Channels are "일정 알림" (reminders), "확인 요청" (requests) and "연결 유지" (keep connected).
316
+ - Notifications vibrate with no sound.
317
+ - Tapping a notification switches to the gateway that sent it.
318
+ - The keep connected notification lists each gateway's state. If it says "인증 거부, 다시 등록하세요" (auth refused), register the phone again.
319
+
320
+ ### 4.7 Copying messages and references
321
+
322
+ - Long press message text to select it, and the selection is copied right away. Below Android 13, a "복사했습니다" toast shows.
323
+ - Swipe a message left to attach it as a reference. Up to 4 references.
324
+ - The memo's 복사 button copies the memo text.
325
+
326
+ ### 4.8 Voice
327
+
328
+ - Tap the mic button and speak. Tap again to send. 취소 discards the recording.
329
+ - The speaker icon in the header turns spoken answers on or off. It's hidden while the gateway's voice feature is off.
330
+ - Install voice and turn it on or off from the Mac's web screen.
331
+
332
+ ### 4.9 Manual button
333
+
334
+ The 매뉴얼 button on the gateway connection screen opens this document. See 2.10, Viewing the manual.
335
+
336
+ ## 5. Common
337
+
338
+ ### 5.1 Giving work to Zig
339
+
340
+ Tell Zig the work in chat. When Zig hands work out, a slot appears in the work status screen. When the work is done, tap or click the slot to clear it.
341
+
342
+ ### 5.2 Questions and approvals
343
+
344
+ 1. Zig or a work asks you a question or requests approval.
345
+ 2. The question shows both as a chat card and in the inbox. The app sends a "확인 요청" notification.
346
+ 3. You answer in either place.
347
+ 4. The gateway delivers the answer to whoever asked.
348
+
349
+ The conversation may keep going while a question waits. If you can't tell whether an answer arrived, send the same answer again; it's handled only once. If the question changed, use 최신 내용 확인 and answer again.
350
+
351
+ ### 5.3 How to update
352
+
353
+ - Web: click 업데이트 in settings (2.8).
354
+ - Mac terminal: run `zigmini update`. For a given version, run `zigmini update <version>`.
355
+
356
+ ### 5.4 Common problems
357
+
358
+ | Problem | Fix |
359
+ | --- | --- |
360
+ | The header says "모델 계정에 로그인하세요". | Run the login command from the README's Install section, then type `/login`. |
361
+ | A schema notice shows in the header. | The agent engine uses a newer data format than the gateway. Follow the notice text. |
362
+ | The connection dropped. | On the Mac, check with `zigmini status`. Run `zigmini doctor`, and `zigmini doctor --fix` to restart only what's broken. |
363
+ | The update button is disabled. | Inbox questions are waiting. Click 확인함 열기 and answer them first. |
364
+ | The gateway won't start and says consent is missing. | Run `zigmini consent` in the Mac terminal. |
365
+ | The app can't connect. | Check 외부 접속 in web settings. Without a choice, the phone can't connect. If the app shows auth refused, register the phone again. |
366
+ | Phone registration is locked. | Click 잠금 해제 on the request line in the web screen. |
367
+
368
+ ## 6. CLI
369
+
370
+ Main commands for the Mac terminal:
371
+
372
+ - `zigmini setup`: installs.
373
+ - `zigmini start`: starts the gateway and the bridge.
374
+ - `zigmini stop`: stops the gateway and the bridge. Running work stops too.
375
+ - `zigmini restart [gateway|bridge]`: restarts.
376
+ - `zigmini status`: shows status.
377
+ - `zigmini doctor [--fix]`: checks everything.
378
+ - `zigmini pair [--apk]`: shows the phone registration QR or the app download QR.
379
+ - `zigmini consent`: shows the permission notice and records consent.
380
+ - `zigmini update [version]`: updates.
381
+
382
+ For the full list, see the README's Commands section.
@@ -41,28 +41,6 @@ function record(value: unknown): value is RecordValue {
41
41
  return typeof value === "object" && value !== null && !Array.isArray(value);
42
42
  }
43
43
 
44
- const retirementAccountedTools = new Set([
45
- "read",
46
- "grep",
47
- "glob",
48
- "todo",
49
- "create_goal",
50
- "get_goal",
51
- "update_goal",
52
- "zgw_work_start",
53
- "zgw_work_send",
54
- "zgw_work_list",
55
- "zgw_work_get",
56
- "zgw_work_cancel",
57
- "zgw_work_report",
58
- "zgw_work_publish",
59
- "zgw_precomment",
60
- // A settled question has its tool result; an open one is in pendingQuestions (age rotation waits for it) and a
61
- // forced rotation turns it into an inbox item answered in the next session.
62
- "ask_user_question",
63
- "request_user_input",
64
- ]);
65
-
66
44
  function durableReadiness(ctx: ContinuityContext): string | null {
67
45
  if (!ctx.goalStoreFile) return "goal_store_unavailable";
68
46
  try {
@@ -119,6 +97,9 @@ function durableReadiness(ctx: ContinuityContext): string | null {
119
97
  }
120
98
  }
121
99
 
100
+ // Bug 86: only a tool call without its result can still be running. Work that outlives its tool call (task children,
101
+ // detached eval cells, background terminals, monitors, open questions) is owned through wake_source_state, which
102
+ // readiness checks before this; a settled call of any tool name leaves nothing behind.
122
103
  const entries = ctx.sessionManager.getEntries();
123
104
  const results = new Set(
124
105
  entries.flatMap((entry) =>
@@ -129,9 +110,6 @@ function durableReadiness(ctx: ContinuityContext): string | null {
129
110
  if (entry.message?.role !== "assistant" || !Array.isArray(entry.message.content)) continue;
130
111
  for (const part of entry.message.content) {
131
112
  if (!record(part) || part.type !== "toolCall") continue;
132
- if (typeof part.name !== "string" || !retirementAccountedTools.has(part.name)) {
133
- return "async_tool_ownership_unverified";
134
- }
135
113
  if (typeof part.id !== "string" || !results.has(part.id)) return "tool_result_unresolved";
136
114
  }
137
115
  }
@@ -47,8 +47,11 @@ function commandEntry(entries: Entry[], workId: string, seq: unknown): Params {
47
47
  throw new Error("report_command_not_received");
48
48
  }
49
49
 
50
- /** Main models have sent report details as a JSON string; only a string that parses to an object is unwrapped. */
51
- function mainReportDetails(details: unknown): unknown {
50
+ /**
51
+ * Main and work models have sent report details as a JSON string (bugs 3 and 127); only a string that parses to an
52
+ * object is unwrapped.
53
+ */
54
+ function reportDetails(details: unknown): unknown {
52
55
  if (typeof details !== "string") return details;
53
56
  try {
54
57
  const parsed: unknown = JSON.parse(details);
@@ -204,7 +207,7 @@ export default function taskControl(pi: ExtensionApi): void {
204
207
  Object.keys(params).some((key) => !["kind", "summary", "details"].includes(key)))) {
205
208
  throw new Error("main_question_only");
206
209
  }
207
- const details = isMain ? mainReportDetails(params.details) : params.details;
210
+ const details = reportDetails(params.details);
208
211
  const command = isMain
209
212
  ? mainDecisionEntry(ctx.sessionManager.getEntries(), details)
210
213
  : commandEntry(ctx.sessionManager.getEntries(), workId as string, params.command_seq);
@@ -214,7 +217,7 @@ export default function taskControl(pi: ExtensionApi): void {
214
217
  kind: "question", summary: params.summary, details,
215
218
  } : {
216
219
  report_id: requestKey, work_id: workId, command_id: command.command_id,
217
- kind: params.kind, command_seq: params.command_seq, summary: params.summary, details: params.details,
220
+ kind: params.kind, command_seq: params.command_seq, summary: params.summary, details,
218
221
  };
219
222
  let entry = ctx.sessionManager.getEntries().find((e) => e.type === "custom" && e.customType === "zgw.work_report" && record(e.data) && e.data.report_id === requestKey);
220
223
  if (entry && !isDeepStrictEqual(entry.data, data)) throw new Error("report_key_conflict");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zigmini",
3
- "version": "0.8.0-beta.2",
3
+ "version": "0.8.0-beta.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zigmini": "bin/zigmini"
@@ -21,9 +21,12 @@
21
21
  "!host/omo-check/*.ts",
22
22
  "!host/omo-check/patches/README.md",
23
23
  "assets/",
24
+ "docs/manual.md",
25
+ "docs/manual.ko.md",
24
26
  "LICENSE.md",
25
27
  "THIRD_PARTY_NOTICES.md",
26
- "README.md"
28
+ "README.md",
29
+ "README.ko.md"
27
30
  ],
28
31
  "license": "SEE LICENSE IN LICENSE.md",
29
32
  "zigmini": {
@@ -53,7 +56,7 @@
53
56
  "test:e2e": "bun test test/e2e",
54
57
  "test:contract": "bun test test/contract",
55
58
  "test:all": "tsc --noEmit && bun test test/unit && bun test test/contract",
56
- "test:mutation": "bun test test/mutation",
59
+ "test:mutation": "bun scripts/mutation-run.ts",
57
60
  "heavy": "bun host/heavy-slot.ts run"
58
61
  },
59
62
  "devDependencies": {
package/src/cli/main.ts CHANGED
@@ -7,6 +7,7 @@ import { LEGACY_HOME } from "../runtime/paths.ts";
7
7
  import { doctorCommand, systemDoctorDeps } from "./doctor.ts";
8
8
  import { setupCommand, stdinPrompt, systemSetupDeps } from "./setup.ts";
9
9
  import { pairCommand } from "./pair.ts";
10
+ import { setupHint } from "./setup-hint.ts";
10
11
  import { helperCommand, updateCommand } from "../update/cli.ts";
11
12
  import { handoffCommand } from "../update/handoff-system.ts";
12
13
  import { HANDOFF_COMMAND } from "../update/strategy.ts";
@@ -14,6 +15,8 @@ import { EXIT_USAGE, parseMigrateArgs, parseMigrateRunArgs, runMigrate, runMigra
14
15
 
15
16
  export const HELP = `zigmini - 개인 AI 에이전트 게이트웨이
16
17
 
18
+ 처음 설치했으면 먼저 \`zigmini setup\`을 실행하세요. setup이 권한 동의, 엔진 설치, 설정, 시작을 합니다.
19
+
17
20
  사용법: zigmini <명령> [옵션]
18
21
 
19
22
  명령:
@@ -104,6 +107,8 @@ if (import.meta.main) {
104
107
  const ctx = systemContext();
105
108
  const kind = installKind();
106
109
  const notice = () => loadNotice();
110
+ const hint = setupHint(argv[0], ctx.paths.home, kind);
111
+ if (hint !== null) deps.err(`${hint}\n\n`);
107
112
  if (argv[0] === "consent") {
108
113
  if (argv.length !== 1) {
109
114
  deps.err(`잘못된 인자: ${argv.join(" ")}\n\n${HELP}`);
@@ -0,0 +1,25 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { readConsent } from "../consent/consent.ts";
4
+ import type { InstallKind } from "../runtime/install-kind.ts";
5
+ import { HANDOFF_COMMAND } from "../update/strategy.ts";
6
+ import { UPDATER_COMMAND } from "./launchd.ts";
7
+
8
+ /**
9
+ * Bun 1.4 blocks the postinstall of `bun add -g zigmini` and the package ships no install-time script, so the CLI tells
10
+ * the user to run `zigmini setup` until setup has written the consent record and config.env.
11
+ */
12
+ export const SETUP_HINT = "아직 설치를 마치지 않았습니다. 먼저 `zigmini setup`을 실행하세요.";
13
+ export const UNHINTED_COMMANDS: readonly string[] = [
14
+ "setup", "consent", "migrate", "migrate-run", "--version", "-v", "--help", "-h", UPDATER_COMMAND, HANDOFF_COMMAND,
15
+ ];
16
+
17
+ export function setupDone(home: string): boolean {
18
+ return existsSync(join(home, "config.env")) && readConsent(home) !== null;
19
+ }
20
+
21
+ export function setupHint(command: string | undefined, home: string, kind: InstallKind): string | null {
22
+ if (kind === "checkout") return null;
23
+ if (command !== undefined && UNHINTED_COMMANDS.includes(command)) return null;
24
+ return setupDone(home) ? null : SETUP_HINT;
25
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Bug 119: which chat socket gets which live event. Every chat socket joins both topics; the app (hello client "android",
3
+ * the same in every app version) leaves the thinking topic, since it never shows thinking and each delta costs it work on
4
+ * its main thread. The web page and devlog show thinking and stay on it.
5
+ */
6
+ export const CHAT_TOPIC = "clients";
7
+ export const THINKING_TOPIC = "thinking";
8
+ export const APP_CLIENT = "android";
9
+
10
+ export function topicOf(type: string): string {
11
+ return type === "thinking.delta" ? THINKING_TOPIC : CHAT_TOPIC;
12
+ }
13
+
14
+ export function showsThinking(client: unknown): boolean {
15
+ return client !== APP_CLIENT;
16
+ }
package/src/decisions.ts CHANGED
@@ -44,6 +44,16 @@ function unresolved(decision: Decision): boolean {
44
44
  return decision.status !== "closed" && decision.status !== "delivered";
45
45
  }
46
46
 
47
+ /**
48
+ * Bug 127: an unanswered ask_user_question with waitForAnswer false. Its tool call already returned, so its owner never
49
+ * waits on it; when its native wait ends (a generation drain, an owner release) the answer still reaches the owner as a
50
+ * follow-up command.
51
+ */
52
+ function detachedQuestion(decision: Decision): boolean {
53
+ return decision.method === "question" && !decision.permission && decision.submissionId === null &&
54
+ record(decision.payload) && decision.payload.waitForAnswer === false;
55
+ }
56
+
47
57
  /** Chat card footer for an item the user deleted from the inbox (web markResolved, Android resolvedLabel). */
48
58
  const DISMISSED_REASONS: Record<string, string> = {
49
59
  user_cancelled: "확인함에서 삭제해 질문을 취소했습니다",
@@ -304,6 +314,8 @@ export class DecisionService {
304
314
  ...observation.payload, ...(workId === null ? { sourceEventSeq } : {}),
305
315
  ...(Array.isArray(sourceReferences) && sourceReferences.length ? { sourceReferences } : {}),
306
316
  };
317
+ // A decision stored before bug 127 has no waitForAnswer mark; observing it again must not read as a changed question.
318
+ if (decision && record(decision.payload) && !Object.hasOwn(decision.payload, "waitForAnswer")) delete payload.waitForAnswer;
307
319
  const bindingState = observation.permission && record(payload.nativePermission) &&
308
320
  !this.store.hasNativePermissionAsked(owner.scope, owner.sessionPath, payload.nativePermission) ? "unknown" : "live";
309
321
  if (!decision) {
@@ -358,7 +370,8 @@ export class DecisionService {
358
370
  if (observation.method !== "question" || observation.permission || observation.required === false || typeof toolCallId !== "string" || !toolCallId) return null;
359
371
  const siblings = this.store.allDecisions(owner.scope).filter((decision) => decision.sourceSessionPath === owner.sessionPath &&
360
372
  decision.method === "question" && !decision.permission && record(decision.payload) && decision.payload.toolCallId === toolCallId && unresolved(decision));
361
- const waiting = siblings.filter((decision) => decision.required && decision.bindingState === "unknown" && decision.reason === "owner_disconnected" &&
373
+ const waiting = siblings.filter((decision) => ((decision.required && decision.bindingState === "unknown" && decision.reason === "owner_disconnected") ||
374
+ decision.reason === "async_wait_closed") &&
362
375
  decision.submissionId === null && decision.answer === null && record(decision.payload) &&
363
376
  isDeepStrictEqual(decision.payload.questions, observation.payload.questions));
364
377
  if (siblings.length !== 1 || waiting.length !== 1) return null;
@@ -413,15 +426,17 @@ export class DecisionService {
413
426
  decision.submissionId !== null && record(answer) && record(answer.answers) && receiptOutcome &&
414
427
  isDeepStrictEqual(event.answers, answer.answers) && (event.comment ?? "") === (answer.comment ?? "") &&
415
428
  Array.isArray(event.unanswered) && event.unanswered.length === 0;
429
+ const detached = !received && event.outcome !== "timed_out" && detachedQuestion(decision);
416
430
  decision = this.store.setDecisionBinding(decision.id, decision.revision, {
417
431
  state: event.outcome === "timed_out" ? "expired" : "closed",
418
- reason: event.outcome === "timed_out" ? "timeout" : "engine_wait_closed",
432
+ reason: event.outcome === "timed_out" ? "timeout" : detached ? "async_wait_closed" : "engine_wait_closed",
419
433
  });
420
434
  if (received) {
421
435
  this.store.transitionDecision(decision.id, decision.revision, { status: "delivered", reason: "question_receipt" });
422
436
  } else if (decision.submissionId !== null) {
423
437
  this.review(decision.id, record(answer) && answer.cancelled === true ? "user_deferred" : "receipt_unknown");
424
- } else if (event.outcome !== "timed_out") {
438
+ } else if (event.outcome !== "timed_out" && !detached) {
439
+ // A detached question stays pending: its answer goes by resume, as a follow-up command.
425
440
  this.review(decision.id, "engine_wait_closed");
426
441
  }
427
442
  this.refresh();
@@ -446,7 +461,8 @@ export class DecisionService {
446
461
  for (const decision of this.store.allDecisions(owner.scope)) {
447
462
  if (decision.sourceSessionPath !== owner.sessionPath || decision.method !== "question" ||
448
463
  decision.bindingState !== "live" || !unresolved(decision) || ids.has(decision.sourceRequestId)) continue;
449
- this.store.setDecisionBinding(decision.id, decision.revision, { state: "unknown", reason: "request_missing" });
464
+ this.store.setDecisionBinding(decision.id, decision.revision, detachedQuestion(decision)
465
+ ? { state: "closed", reason: "async_wait_closed" } : { state: "unknown", reason: "request_missing" });
450
466
  }
451
467
  this.refresh();
452
468
  }
@@ -468,7 +484,8 @@ export class DecisionService {
468
484
  if (this.stopped) return;
469
485
  for (const decision of this.store.allDecisions(owner.scope)) {
470
486
  if (decision.sourceSessionPath !== owner.sessionPath || decision.bindingState !== "live" || !unresolved(decision)) continue;
471
- this.store.setDecisionBinding(decision.id, decision.revision, { state: "unknown", reason: "owner_disconnected" });
487
+ this.store.setDecisionBinding(decision.id, decision.revision, detachedQuestion(decision)
488
+ ? { state: "closed", reason: "async_wait_closed" } : { state: "unknown", reason: "owner_disconnected" });
472
489
  }
473
490
  this.refresh();
474
491
  }