zigmini 0.8.0-beta.3 → 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zigmini",
3
- "version": "0.8.0-beta.3",
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": {
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
+ }
package/src/hub.ts CHANGED
@@ -1127,7 +1127,8 @@ export class Hub {
1127
1127
  ) {
1128
1128
  throw new Error("turn_running");
1129
1129
  }
1130
- if (forced) this.retireScopedQuestions(convId, sessionId);
1130
+ // Any rotation hands main's open waits of the closed session to the next one (inbox resume); bug 72.
1131
+ this.retireScopedQuestions(convId, sessionId);
1131
1132
  try {
1132
1133
  await this.requireOmo().command("close_session", { sessionId });
1133
1134
  const listed = await this.requireOmo().command<{
@@ -1228,9 +1229,9 @@ export class Hub {
1228
1229
  return owned;
1229
1230
  }
1230
1231
 
1231
- /** What holds automatic rotation: the rotation blockers plus an unconfirmed session close. */
1232
+ /** What holds automatic rotation: the age rotation holds plus an unconfirmed session close. */
1232
1233
  private rotationHolds(conv: string): RotationBlocker[] {
1233
- const holds = this.rotationBlockers(conv);
1234
+ const holds = this.rotationHoldsFor(conv, false);
1234
1235
  return this.closeUncertain(conv) ? [{ kind: "session_close_unknown" }, ...holds] : holds;
1235
1236
  }
1236
1237
 
@@ -1295,9 +1296,18 @@ export class Hub {
1295
1296
  }
1296
1297
  }
1297
1298
 
1298
- /** What holds a rotation now: every rotation blocker, or for a forced rotation only an unfinished main turn. */
1299
1299
  private rotationHeldFor(conv: string, forced: boolean): boolean {
1300
- return forced ? this.rotationBlockers(conv, false).some((blocker) => blocker.kind === "main_turn") : this.rotationBlocked(conv);
1300
+ return this.rotationHoldsFor(conv, forced).length > 0;
1301
+ }
1302
+
1303
+ /**
1304
+ * Bugs 72 and 86: what holds a rotation now. A main turn always holds; an age or manual rotation also waits for an open
1305
+ * approval dialog or native question. Works, their unreported reports and inbox decisions belong to the conversation,
1306
+ * not the session file, so they carry over to the next session (handoff, ZGW_WORK_STATE, work update wake) and never hold.
1307
+ */
1308
+ private rotationHoldsFor(conv: string, forced: boolean): RotationBlocker[] {
1309
+ return this.rotationBlockers(conv, false).filter((blocker) =>
1310
+ blocker.kind === "main_turn" || (!forced && (blocker.kind === "dialog" || blocker.kind === "question")));
1301
1311
  }
1302
1312
 
1303
1313
  /** Bug 72 proposal 4: the user-confirmed limit this conversation's session reached, if any. */
@@ -1335,10 +1345,10 @@ export class Hub {
1335
1345
  }
1336
1346
 
1337
1347
  /** Bug 72 proposal 4: what stays open across the rotation, so the next session owns it. */
1338
- private handoffOpenItems(conv: string, reason: RotationReason): string[] {
1348
+ private handoffOpenItems(conv: string): string[] {
1339
1349
  const open = this.ownedOpenDecisions(conv);
1340
1350
  return [
1341
- reason === "size" || reason === "compactions"
1351
+ this.workSummary(conv).length > 0
1342
1352
  ? "끝나지 않은 작업은 그대로 이어집니다. 목록은 ZGW_WORK_STATE와 zgw_work_list로 보고, 보고하지 않은 결과는 작업 알림으로 다시 받습니다."
1343
1353
  : "",
1344
1354
  ...(open.length > 0
@@ -1348,7 +1358,7 @@ export class Hub {
1348
1358
  }
1349
1359
 
1350
1360
  private rotationEligible(conv: ConversationInfo): boolean {
1351
- if (!conv.threadId?.startsWith("/") || this.closeUncertain(conv.id) || this.rotationBlocked(conv.id)) return false;
1361
+ if (!conv.threadId?.startsWith("/") || this.closeUncertain(conv.id) || this.rotationHeldFor(conv.id, false)) return false;
1352
1362
  const started = Date.parse(this.store.getSessionMeta(conv.id).startedAt ?? conv.createdAt);
1353
1363
  const used = this.store.lastSeq(conv.id, "message.user") > this.store.lastSeq(conv.id, "session.rotated");
1354
1364
  if (!used) return false;
@@ -1388,7 +1398,7 @@ export class Hub {
1388
1398
  return [
1389
1399
  `[게이트웨이 알림] 이 대화는 새 omo 세션으로 교체됐습니다 (${this.reasonLabel(reason)}).`,
1390
1400
  from ? `이전 세션의 전체 기록은 ${from} 에 남아 있습니다. 파일이 크니 필요할 때 필요한 부분만 검색해서 보세요.` : "",
1391
- ...this.handoffOpenItems(conv, reason),
1401
+ ...this.handoffOpenItems(conv),
1392
1402
  "아래는 직전 대화의 최근 부분입니다. 맥락으로만 참고하세요.",
1393
1403
  "---",
1394
1404
  ...lines,
package/src/main.ts CHANGED
@@ -39,7 +39,7 @@ import { Hub, type ScopedQuestionFrame } from "./hub.ts";
39
39
  import { parseSummaryModels, SpeechSummarizer } from "./speech-summary.ts";
40
40
  import { MEDIA_MAX_BYTES, saveUpload, serveMedia } from "./media.ts";
41
41
  import { OmoClient } from "./omo/client.ts";
42
- import { bridgeLiveness, GenerationRouter, type GenerationEndpoint } from "./omo/generations.ts";
42
+ import { bridgeLiveness, GenerationRouter, startupGenerations } from "./omo/generations.ts";
43
43
  import { activeRecord, readRegistry, slotUrl } from "./runtime/slots.ts";
44
44
  import { NATIVE_LIFECYCLE_PREFIX } from "./omo/lifecycle.ts";
45
45
  import { RECOVERY_RECEIPT_PREFIX } from "./omo/recovery-receipt.ts";
@@ -66,6 +66,7 @@ import { acceptUpdateConsent, requestUpdate, type UpdateApiDeps } from "./update
66
66
  import { PHASE_LABELS, readState, UPDATE_CONSENT_PATH, UPDATE_PATH, UPDATE_STATE_PATH, updateDir, UpdateWatcher } from "./update/state.ts";
67
67
  import { resolveTarget, tarEntry } from "./update/target.ts";
68
68
  import { registryUrl, UPDATE_CHECK_PATH, UPDATE_STATUS_PATH, UpdateChecker, versionAvailableEnvelope } from "./update/check.ts";
69
+ import { MANUAL_PATH, serveManual } from "./manual.ts";
69
70
  import { clean as gcClean, GC_CLEAN_PATH, GC_PATH, type GcRoots, parseSelection as parseGcSelection, report as gcReport, systemGcDeps } from "./gc.ts";
70
71
 
71
72
  // Exit 0 so launchd (KeepAlive SuccessfulExit=false) does not restart a gateway that has no consent.
@@ -189,14 +190,11 @@ function requesterId(slot: RuntimeSlot): string {
189
190
  store.setSetting(key, id);
190
191
  return id;
191
192
  }
192
- // Improvement 77: with run/runtime-slots.json the gateway connects to every listed generation; without it (or with an
193
- // explicit ZGW_OMO_URL, as in Docker and the tests) it has one generation, as before.
194
- const slotRegistry = process.env.ZGW_OMO_URL ? null : readRegistry(runtime.slotsFile);
195
- const endpoints: GenerationEndpoint[] = slotRegistry === null
196
- ? [{ slot: "a", url: config.omoUrl }]
197
- : slotRegistry.slots.map((record) => ({ slot: record.slot, url: slotUrl(record) }));
193
+ // Improvement 77: with run/runtime-slots.json the gateway connects to every listed generation; without it (or with a
194
+ // ZGW_OMO_URL that names another OMO, as in Docker and the tests) it has one generation, as before (bug 134).
195
+ const startup = startupGenerations(process.env.ZGW_OMO_URL, `ws://127.0.0.1:${runtime.ports.bridge}`, () => readRegistry(runtime.slotsFile));
198
196
  let connectionGeneration = 0;
199
- const omo = new GenerationRouter(endpoints, slotRegistry === null ? "a" : activeRecord(slotRegistry).slot, {
197
+ const omo = new GenerationRouter(startup.endpoints, startup.active, {
200
198
  connect: (endpoint, handlers) => new OmoClient(endpoint.url, omoToken, handlers, requesterId(endpoint.slot)),
201
199
  liveness: (endpoint) => bridgeLiveness(endpoint.url, omoToken),
202
200
  log: (line) => console.log(line),
@@ -629,6 +627,7 @@ async function handle(req: Request, srv: Server<Conn>, trusted: boolean): Promis
629
627
  const response = await handleProfileIcon(req, url, trusted, { icon: profileIcon, profile: () => readProfile(store), publish: (e) => publish(e) });
630
628
  if (response) return response;
631
629
  }
630
+ if (url.pathname === MANUAL_PATH) return serveManual(req, url);
632
631
  // Plan Todo 23: the update button. The app port may ask too, but never for an update that needs a new consent.
633
632
  if (url.pathname === UPDATE_PATH && req.method === "POST") {
634
633
  const body = await req.json().catch(() => null) as { origin?: unknown } | null;
package/src/manual.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { join } from "node:path";
2
+
3
+ /**
4
+ * The user manual (docs/manual.md, docs/manual.ko.md) for the web 설정 "매뉴얼" button and the app's work status screen.
5
+ * Read only, on the web port and, behind the app device gate, on the app port. Korean is the default language.
6
+ */
7
+ export const MANUAL_PATH = "/api/manual";
8
+ export const MANUAL_DIR = join(import.meta.dir, "..", "docs");
9
+ export type ManualLang = "ko" | "en";
10
+
11
+ /** The `lang` query value as a language: missing or empty is Korean; anything but `ko` or `en` is null. */
12
+ export function manualLang(value: string | null): ManualLang | null {
13
+ if (value === null || value === "" || value === "ko") return "ko";
14
+ return value === "en" ? "en" : null;
15
+ }
16
+
17
+ export function manualFile(lang: ManualLang): string {
18
+ return lang === "en" ? "manual.md" : "manual.ko.md";
19
+ }
20
+
21
+ /** `GET /api/manual?lang=ko|en`: the Markdown text; 400 for another language, 404 with a hint when the file is missing. */
22
+ export async function serveManual(req: Request, url: URL, dir: string = MANUAL_DIR): Promise<Response> {
23
+ if (req.method !== "GET") return Response.json({ error: "method_not_allowed" }, { status: 405 });
24
+ const lang = manualLang(url.searchParams.get("lang"));
25
+ if (lang === null) return Response.json({ error: "unknown_lang", message: "lang은 ko 또는 en입니다." }, { status: 400 });
26
+ const file = Bun.file(join(dir, manualFile(lang)));
27
+ if (!(await file.exists())) {
28
+ return Response.json({ error: "manual_missing", message: "이 설치에는 매뉴얼 파일이 없습니다. 게이트웨이를 업데이트하세요." }, { status: 404 });
29
+ }
30
+ return new Response(file, { headers: { "content-type": "text/markdown; charset=utf-8", "cache-control": "no-cache", "content-language": lang } });
31
+ }