zigmini 0.9.0-beta.3 → 1.0.0-beta.1

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.
Files changed (141) hide show
  1. package/README.ko.md +24 -4
  2. package/README.md +24 -4
  3. package/assets/agent/gateway-rules.md +1 -1
  4. package/assets/android/zigmini.apk +0 -0
  5. package/assets/android/zigmini.apk.json +3 -3
  6. package/assets/build.json +1 -1
  7. package/docs/manual.ko.md +51 -11
  8. package/docs/manual.md +52 -12
  9. package/host/omo-bridge/agent-control.ts +274 -0
  10. package/host/omo-bridge/bridge.ts +1 -1
  11. package/host/omo-bridge/continuity-control.ts +1 -1
  12. package/host/omo-bridge/task-control.ts +26 -16
  13. package/host/omo-check/patches/omo-5.1.51-compaction-long-context.patch +92 -0
  14. package/host/omo-check/patches/omo-5.1.51-compaction-port.patch +2635 -0
  15. package/host/omo-check/patches/omo-5.1.51-compaction-recovery.patch +911 -0
  16. package/host/omo-check/patches/omo-5.1.51-gateway-port.manifest.json +363 -0
  17. package/host/omo-check/patches/omo-5.1.51-provider-failover.patch +50 -0
  18. package/host/omo-check/patches/omo-5.1.51-provider-port.patch +441 -0
  19. package/host/omo-check/patches/omo-5.1.51-rpc-port.patch +799 -0
  20. package/host/omo-check/patches/omo-5.1.51-runtime-compat.patch +31 -0
  21. package/package.json +3 -2
  22. package/src/agent/api.ts +215 -0
  23. package/src/agent/definition.ts +177 -0
  24. package/src/agent/fields.ts +42 -0
  25. package/src/agent/files.ts +192 -0
  26. package/src/agent/legacy-files.ts +46 -0
  27. package/src/agent/list.ts +21 -0
  28. package/src/agent-rules.ts +14 -5
  29. package/src/approvals.ts +6 -1
  30. package/src/cli/main.ts +33 -0
  31. package/src/cli/messages.ts +10 -2
  32. package/src/cli/rotate-main.ts +2 -1
  33. package/src/cli/scan.ts +414 -0
  34. package/src/cli/security-import.ts +155 -0
  35. package/src/compact.ts +22 -0
  36. package/src/decisions.ts +6 -4
  37. package/src/engine/compat.json +3 -3
  38. package/src/engine/index.ts +1 -0
  39. package/src/engine/schema-guard.ts +12 -4
  40. package/src/engine/sdk.ts +8 -0
  41. package/src/formal-verification.ts +366 -0
  42. package/src/gc.ts +37 -6
  43. package/src/hub.ts +339 -79
  44. package/src/main.ts +95 -9
  45. package/src/maintenance.ts +1 -1
  46. package/src/manual.ts +9 -2
  47. package/src/profile-prompt.ts +73 -0
  48. package/src/profile.ts +33 -10
  49. package/src/protocol.ts +11 -2
  50. package/src/question-notifications.ts +4 -1
  51. package/src/reply-notifications.ts +7 -1
  52. package/src/repo-security-setting.ts +33 -0
  53. package/src/runtime/paths.ts +16 -0
  54. package/src/security/baseline.ts +170 -0
  55. package/src/security/dex-strings.ts +242 -0
  56. package/src/security/repo-scan.ts +369 -0
  57. package/src/security/revision-probe.ts +144 -0
  58. package/src/security/terms.ts +97 -0
  59. package/src/security-allow.ts +1 -0
  60. package/src/security-baseline.ts +113 -0
  61. package/src/security-check.ts +91 -16
  62. package/src/server-text.ts +38 -0
  63. package/src/store.ts +142 -16
  64. package/src/transcript.ts +7 -1
  65. package/src/ui-language.ts +33 -0
  66. package/src/update/check.ts +7 -3
  67. package/src/update/handoff-system.ts +24 -16
  68. package/src/update/handoff.ts +23 -12
  69. package/src/update/helper.ts +72 -36
  70. package/src/update/request.ts +46 -30
  71. package/src/update/state.ts +21 -11
  72. package/src/update/strategy.ts +12 -5
  73. package/src/update/target.ts +32 -15
  74. package/src/usage.ts +43 -20
  75. package/src/version.ts +39 -16
  76. package/src/voice/agents.ts +8 -3
  77. package/src/voice/install.ts +29 -18
  78. package/src/work-watch.ts +40 -18
  79. package/web/agent-icon.js +15 -9
  80. package/web/agents.d.ts +201 -0
  81. package/web/agents.js +813 -0
  82. package/web/app-download.js +32 -19
  83. package/web/app.js +1407 -278
  84. package/web/autocopy.js +6 -1
  85. package/web/boot.js +27 -8
  86. package/web/commands.js +20 -11
  87. package/web/decisions.js +70 -49
  88. package/web/devlog.js +25 -19
  89. package/web/diagram.js +8 -3
  90. package/web/enroll.js +43 -22
  91. package/web/gc.js +32 -19
  92. package/web/i18n/agent-icon.js +24 -0
  93. package/web/i18n/agents.js +48 -0
  94. package/web/i18n/app-download.js +26 -0
  95. package/web/i18n/autocopy.js +5 -0
  96. package/web/i18n/boot.js +24 -0
  97. package/web/i18n/commands.js +34 -0
  98. package/web/i18n/decisions.js +24 -0
  99. package/web/i18n/devlog.js +60 -0
  100. package/web/i18n/diagram.js +12 -0
  101. package/web/i18n/enroll.js +22 -0
  102. package/web/i18n/gc.js +38 -0
  103. package/web/i18n/lastsent.js +10 -0
  104. package/web/i18n/main.d.ts +4 -0
  105. package/web/i18n/main.js +527 -0
  106. package/web/i18n/manual.d.ts +4 -0
  107. package/web/i18n/manual.js +14 -0
  108. package/web/i18n/memo.js +30 -0
  109. package/web/i18n/notifications.js +16 -0
  110. package/web/i18n/profile.js +13 -0
  111. package/web/i18n/reference.js +26 -0
  112. package/web/i18n/security-inbox.js +40 -0
  113. package/web/i18n/slash.js +38 -0
  114. package/web/i18n/turn-stats.js +5 -0
  115. package/web/i18n/update.js +42 -0
  116. package/web/i18n/voice.js +28 -0
  117. package/web/i18n/wake.js +16 -0
  118. package/web/i18n/workid.js +10 -0
  119. package/web/i18n/workstatus.js +20 -0
  120. package/web/i18n.d.ts +20 -0
  121. package/web/i18n.js +63 -0
  122. package/web/index.html +156 -96
  123. package/web/lastsent.js +9 -4
  124. package/web/manual.d.ts +1 -1
  125. package/web/manual.js +11 -5
  126. package/web/memo.js +18 -13
  127. package/web/notifications.js +12 -7
  128. package/web/profile.js +42 -17
  129. package/web/reference.js +20 -15
  130. package/web/security-inbox.d.ts +18 -1
  131. package/web/security-inbox.js +173 -4
  132. package/web/slash.js +23 -13
  133. package/web/style.css +132 -3
  134. package/web/tooltip.js +5 -2
  135. package/web/turn-stats.js +6 -2
  136. package/web/update.js +31 -23
  137. package/web/voice.js +18 -8
  138. package/web/wake.d.ts +4 -0
  139. package/web/wake.js +7 -3
  140. package/web/workid.js +12 -2
  141. package/web/workstatus.js +36 -32
@@ -0,0 +1,441 @@
1
+ --- a/@code-yeongyu/senpi/dist/core/auth-storage.js
2
+ +++ b/@code-yeongyu/senpi/dist/core/auth-storage.js
3
+ @@ -319,7 +319,6 @@ export class InMemoryAuthStorageBackend {
4
+ */
5
+ export class AuthStorage {
6
+ constructor(storage, authPath) {
7
+ - this.data = {};
8
+ this.runtimeOverrides = new Map();
9
+ this.extensionOAuthProviders = new Map();
10
+ this.errors = [];
11
+ @@ -342,7 +341,6 @@ export class AuthStorage {
12
+ // Another instance already loaded this store: start from its credentials, not an
13
+ // empty snapshot, so a busy read below keeps them. A repaired or migrated load
14
+ // leaves the revision unset, so only an exact revision match skips the re-read.
15
+ - this.data = this.readState.data;
16
+ this.dataLoaded = true;
17
+ const revision = getFileContentRevision(authPath);
18
+ if (revision !== undefined && revision === this.readState.revision)
19
+ @@ -350,6 +348,14 @@ export class AuthStorage {
20
+ }
21
+ this.reload();
22
+ }
23
+ + // Synchronous extension reads and async runtime reads share one snapshot.
24
+ + get data() {
25
+ + return this.readState.data;
26
+ + }
27
+ + set data(data) {
28
+ + this.readState.data = data;
29
+ + this.readState.revision = undefined;
30
+ + }
31
+ static create(authPath = join(getAgentDir(), "auth.json")) {
32
+ const normalizedAuthPath = normalizePath(authPath);
33
+ return new AuthStorage(new FileAuthStorageBackend(normalizedAuthPath), normalizedAuthPath);
34
+ @@ -420,7 +426,6 @@ export class AuthStorage {
35
+ return backupPath;
36
+ }
37
+ updateReadState(data, revision) {
38
+ - this.data = data;
39
+ this.dataLoaded = true;
40
+ this.dataFromBusyRead = false;
41
+ this.readState.data = data;
42
+ @@ -877,4 +882,4 @@ export function readStoredCredential(providerId, authPath = join(getAgentDir(),
43
+ return undefined;
44
+ }
45
+ }
46
+ -//# sourceMappingURL=auth-storage.js.map
47
+
48
+ +//# sourceMappingURL=auth-storage.js.map
49
+ --- a/@code-yeongyu/senpi/dist/core/credential-accounts.js
50
+ +++ b/@code-yeongyu/senpi/dist/core/credential-accounts.js
51
+ @@ -75,6 +75,8 @@ function blockedModels(now, ...sources) {
52
+ * here without auth.json ever carrying block state.
53
+ */
54
+ export async function getCredentialAccounts(storage, provider, env = process.env, repository) {
55
+ + // Refresh metadata without resolving keys or applying runtime overrides.
56
+ + await storage.list();
57
+ return summarizeCredentialAccounts(provider, storage.get(provider), env, repository ?? credentialPoolRepository(storage));
58
+ }
59
+ /** Storage-free variant for callers that already hold the credential (e.g. auth check). */
60
+ @@ -211,4 +213,4 @@ export async function removeCredentialAccount(storage, provider, name, env = pro
61
+ await repo.mutateSlotState(provider, "stored", name, () => undefined);
62
+ emitProviderAccountsChanged(provider);
63
+ }
64
+ -//# sourceMappingURL=credential-accounts.js.map
65
+
66
+ +//# sourceMappingURL=credential-accounts.js.map
67
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/anthropic-subscription/auth-lane.d.ts
68
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/anthropic-subscription/auth-lane.d.ts
69
+ @@ -9,7 +9,7 @@ export type { AuthenticatedAttemptInput } from "./auth-attempt.ts";
70
+ export type AuthenticatedQueryInput = {
71
+ prompt: Parameters<SdkQuery>[0]["prompt"];
72
+ query: SdkQuery;
73
+ - buildOptions: (lane: AnthropicSubscriptionTokenInjection) => Options;
74
+ + buildOptions: (lane: AnthropicSubscriptionTokenInjection) => Options | Promise<Options>;
75
+ providerSettings: AnthropicSubscriptionProviderSettings;
76
+ /** Effective request auth environment; overrides the host for account discovery and SDK spawn. */
77
+ env?: Record<string, string>;
78
+ @@ -27,4 +27,4 @@ export type AuthenticatedQueryInput = {
79
+ export declare function resolveEffectiveLane(settings: AnthropicSubscriptionProviderSettings, accounts: readonly AccountSlot[]): AnthropicSubscriptionTokenInjection;
80
+ /** Resolves managed OAuth immediately before each subprocess spawn and retries only pre-delta failures. */
81
+ export declare function queryWithAuthLane(input: AuthenticatedQueryInput): AsyncGenerator<SDKMessage>;
82
+ -//# sourceMappingURL=auth-lane.d.ts.map
83
+
84
+ +//# sourceMappingURL=auth-lane.d.ts.map
85
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js
86
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js
87
+ @@ -121,7 +121,7 @@ export async function* queryWithAuthLane(input) {
88
+ // Lane-owned variables from buildOptions (the compaction-owner overlay) win over the host.
89
+ let askedLegacyLongContext = false;
90
+ const ambientAttempt = async () => {
91
+ - const options = input.buildOptions("ambient");
92
+ + const options = await input.buildOptions("ambient");
93
+ askedLegacyLongContext = asksLegacyLongContext(options.model);
94
+ options.env = { ...ambientEnvironment, ...options.env };
95
+ return createAttemptMessages(input, { accountName: "ambient", accounts: [], authLane: "ambient", options });
96
+ @@ -160,7 +160,7 @@ export async function* queryWithAuthLane(input) {
97
+ ...(input.model === undefined ? {} : { model: input.model }),
98
+ }),
99
+ runAttempt: async (slot) => {
100
+ - const options = input.buildOptions(pool.lane);
101
+ + const options = await input.buildOptions(pool.lane);
102
+ askedLegacyLongContext = asksLegacyLongContext(options.model);
103
+ const accounts = pool.accounts.map((account) => ({ ...account }));
104
+ const prepared = await prepareSlot(pool, slot, signal);
105
+ @@ -192,4 +192,4 @@ export async function* queryWithAuthLane(input) {
106
+ },
107
+ });
108
+ }
109
+ -//# sourceMappingURL=auth-lane.js.map
110
+
111
+ +//# sourceMappingURL=auth-lane.js.map
112
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/anthropic-subscription/cold-seed-images.js
113
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/anthropic-subscription/cold-seed-images.js
114
+ @@ -21,6 +21,7 @@ export const coldSeedImageText = {
115
+ toolOutput: (toolName) => `[image returned by the ${toolName} tool to your own call (tool output, not a user attachment)]`,
116
+ userHistory: "[image the user attached in this earlier turn (history, not part of the current message)]",
117
+ duplicate: (origin, first) => `${subject(origin)}: identical to an image already shown above (${source(first)}); not attached again]`,
118
+ + userAnswered: "[image the user attached in an earlier turn: you already saw and answered it, so it is not attached again and is not part of the current message; ask the user to attach it again if you need it]",
119
+ answered: (origin) => `[image returned earlier by the ${origin.toolName} tool, id=${origin.toolCallId}: already seen earlier in this conversation; run the tool again if you need it]`,
120
+ capped: (origin) => `${subject(origin)}: omitted from this replay, which re-sends at most ${MAX_REPLAYED_HISTORY_IMAGES} earlier images; re-read the source if you need it]`,
121
+ unreadable: (origin) => `${subject(origin)}: omitted because its image data is missing or unreadable]`,
122
+ @@ -86,14 +87,16 @@ function selectUserFirstThenMostRecent(occurrences) {
123
+ return new Set(ordered.slice(0, MAX_REPLAYED_HISTORY_IMAGES).map(([hash]) => hash));
124
+ }
125
+ /**
126
+ - * A tool image the assistant already responded to is not re-sent (#2768): a cold
127
+ - * seed packs the whole history into one user message, and replaying earlier tool
128
+ - * reads there made the model answer plain questions as if the user had just sent
129
+ - * those images. The user's own uploads and images still awaiting a response are
130
+ - * replayed.
131
+ + * An image the assistant already responded to is not re-sent (#2768): a cold
132
+ + * seed packs the whole history into one user message, and replaying earlier
133
+ + * images there made the model answer plain questions as if the user had just
134
+ + * sent those images. zigmini: this covers the user's own earlier uploads too;
135
+ + * Claude Code saved each replayed upload as a new images/N.png on every model
136
+ + * call and the model read it as a new attachment. Images still awaiting a
137
+ + * response (the current turn) are replayed.
138
+ */
139
+ function isAnsweredToolImage(occurrence) {
140
+ - return occurrence.answered && occurrence.origin.kind === "tool";
141
+ + return occurrence.answered;
142
+ }
143
+ /**
144
+ * Rewrites the image entries of a cold-seed history (#2490): a tool-result image
145
+ @@ -124,8 +127,8 @@ export function replayHistoryImages(history, sdkToolName, options = {}) {
146
+ if (decoded === undefined || hash === undefined) {
147
+ replacement = [{ type: "text", text: coldSeedImageText.unreadable(origin) }];
148
+ }
149
+ - else if (origin.kind === "tool" && occurrence.answered && shownEarlier === undefined) {
150
+ - replacement = [{ type: "text", text: coldSeedImageText.answered(origin) }];
151
+ + else if (occurrence.answered && shownEarlier === undefined) {
152
+ + replacement = [{ type: "text", text: origin.kind === "tool" ? coldSeedImageText.answered(origin) : coldSeedImageText.userAnswered }];
153
+ }
154
+ else if (!replayed.has(hash) && shownEarlier === undefined) {
155
+ replacement = [{ type: "text", text: coldSeedImageText.capped(origin) }];
156
+ @@ -159,4 +162,4 @@ export function replayHistoryImages(history, sdkToolName, options = {}) {
157
+ }
158
+ return rewritten;
159
+ }
160
+ -//# sourceMappingURL=cold-seed-images.js.map
161
+
162
+ +//# sourceMappingURL=cold-seed-images.js.map
163
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/anthropic-subscription/errors.js
164
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/anthropic-subscription/errors.js
165
+ @@ -1,5 +1,6 @@
166
+ import { rateLimitModelFamily } from "../../../credential-pool/model-scope.js";
167
+ import { inheritAccountContext } from "./account-context.js";
168
+ +import { isClaudeSdkRefusal } from "./refusal.js";
169
+ const SDK_ERROR_CLASSIFICATIONS = {
170
+ authentication_failed: { kind: "auth_error", retryable: true },
171
+ oauth_org_not_allowed: { kind: "org_not_allowed", retryable: true },
172
+ @@ -77,6 +78,8 @@ export function sdkAssistantFailure(message) {
173
+ }
174
+ /** Classifies Anthropic Subscription error codes and HTTP-shaped fallback text in one place. */
175
+ export function classifySdkError(error) {
176
+ + if (isClaudeSdkRefusal(error))
177
+ + return OTHER_ERROR;
178
+ const classification = classifyErrorKind(error);
179
+ if (classification.kind !== "rate_limit")
180
+ return classification;
181
+ @@ -131,4 +134,4 @@ function classifyErrorKind(error) {
182
+ }
183
+ return OTHER_ERROR;
184
+ }
185
+ -//# sourceMappingURL=errors.js.map
186
+
187
+ +//# sourceMappingURL=errors.js.map
188
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/anthropic-subscription/refusal.js
189
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/anthropic-subscription/refusal.js
190
+ @@ -26,6 +26,13 @@ export function refusalError(message) {
191
+ return details ? new ClaudeSdkRefusalError(details.content, details.category) : undefined;
192
+ }
193
+ export function isClaudeSdkRefusal(error) {
194
+ - return error instanceof ClaudeSdkRefusalError;
195
+ + return getClaudeSdkRefusal(error) !== undefined;
196
+ }
197
+ -//# sourceMappingURL=refusal.js.map
198
+
199
+ +export function getClaudeSdkRefusal(error) {
200
+ + if (error instanceof ClaudeSdkRefusalError)
201
+ + return error;
202
+ + if (error !== null && typeof error === "object" && "original" in error)
203
+ + return getClaudeSdkRefusal(error.original);
204
+ + return undefined;
205
+ +}
206
+ +//# sourceMappingURL=refusal.js.map
207
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/anthropic-subscription/stream.js
208
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/anthropic-subscription/stream.js
209
+ @@ -9,7 +9,7 @@ import { buildAnthropicSubscriptionQueryOptions } from "./options.js";
210
+ import { buildDeferredPromptStream, buildPromptBlocks } from "./prompt-bridge.js";
211
+ import { pinOneShotPromptCacheTtl } from "./prompt-cache-ttl.js";
212
+ import { dedupeUltraworkBlocks } from "./prompt-directive-dedupe.js";
213
+ -import { refusalError } from "./refusal.js";
214
+ +import { getClaudeSdkRefusal, refusalError } from "./refusal.js";
215
+ import { getSdkBoundary, loadClaudeAgentSdk } from "./sdk-boundary.js";
216
+ import { emitContinuityObservation } from "./session-observability.js";
217
+ import { forgetBinding } from "./session-reattach.js";
218
+ @@ -76,7 +76,7 @@ export function streamAnthropicSubscription(model, context, options) {
219
+ claudeCodeRun = resolveClaudeCodeRun(defaultExecutableDeps());
220
+ const executable = claudeCodeRun.executable;
221
+ let oneShotCacheTtl;
222
+ - const buildOptions = (authLane) => {
223
+ + const buildOptions = async (authLane) => {
224
+ const queryOptions = buildAnthropicSubscriptionQueryOptions({
225
+ model,
226
+ context,
227
+ @@ -100,7 +100,8 @@ export function streamAnthropicSubscription(model, context, options) {
228
+ if (!useResidentSession) {
229
+ oneShotCacheTtl = pinOneShotPromptCacheTtl(queryOptions, authLane ?? providerSettings.tokenInjection ?? "ambient", { ...process.env, ...options?.env });
230
+ }
231
+ - return queryOptions;
232
+ + const replacement = await options?.onPayload?.(queryOptions, model);
233
+ + return replacement === undefined ? queryOptions : replacement;
234
+ };
235
+ const recordContinuity = (observation) => {
236
+ // Not a failure: carry the observation as details only, with no synthesized error.
237
+ @@ -255,6 +256,9 @@ export function streamAnthropicSubscription(model, context, options) {
238
+ if (nativeCompactionStopped && options?.sessionId)
239
+ forgetBinding(options.sessionId);
240
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
241
+ + const refusal = getClaudeSdkRefusal(error);
242
+ + if (refusal)
243
+ + output.stopDetails = { type: "refusal", explanation: refusal.message };
244
+ // A failed result still bills its tokens; managed and resident lanes
245
+ // throw before the result reaches this loop, so account for it here.
246
+ const billed = sdkResultFailureUsage(error);
247
+ @@ -272,4 +276,4 @@ export function streamAnthropicSubscription(model, context, options) {
248
+ })();
249
+ return stream;
250
+ }
251
+ -//# sourceMappingURL=stream.js.map
252
+
253
+ +//# sourceMappingURL=stream.js.map
254
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/tps.js
255
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/tps.js
256
+ @@ -16,6 +16,8 @@ export default function (pi) {
257
+ let lastDeltaMs = null;
258
+ let generationMs = 0;
259
+ let streamedOutput = 0;
260
+ + let turnId = null;
261
+ + let completedMessages = [];
262
+ const finishActiveAssistantTiming = () => {
263
+ if (activeAssistantStartMs === null)
264
+ return;
265
+ @@ -34,7 +36,11 @@ export default function (pi) {
266
+ lastDeltaMs = null;
267
+ thinkingStreamed = false;
268
+ };
269
+ - pi.on("agent_start", () => {
270
+ + pi.on("agent_start", (_event, ctx) => {
271
+ + turnId = ctx.mode === "rpc" ? crypto.randomUUID() : null;
272
+ + completedMessages = [];
273
+ + if (turnId !== null)
274
+ + pi.rpc.emit("zgw.turn_started", { version: 1, turnId });
275
+ activeAssistantStartMs = null;
276
+ assistantElapsedMs = 0;
277
+ generationMs = 0;
278
+ @@ -69,6 +75,7 @@ export default function (pi) {
279
+ // counted in usage.output may be hidden or only summarised after it finished, so it was generated
280
+ // before the first delta. Such a message (usage.reasoning, or a thinking block whose text never
281
+ // streamed: Bedrock and omitted display report no reasoning count) is timed from its start.
282
+ + completedMessages.push(event.message);
283
+ const output = event.message.usage?.output ?? 0;
284
+ const windowStart = reasonedBeforeFirstDelta(event.message, thinkingStreamed) ? messageStartMs : firstDeltaMs;
285
+ const counted = event.message.stopReason !== "aborted" && event.message.stopReason !== "error" && output > 0;
286
+ @@ -83,6 +90,43 @@ export default function (pi) {
287
+ const elapsedMs = assistantElapsedMs;
288
+ const measuredGenerationMs = generationMs;
289
+ const measuredOutput = streamedOutput;
290
+ + // Structured RPC metadata stays out of messages and extension_ui_request notifications.
291
+ + const completedTurnId = turnId;
292
+ + turnId = null;
293
+ + const messages = completedMessages;
294
+ + completedMessages = [];
295
+ + const finalMessage = messages.at(-1);
296
+ + const lastAssistant = event.messages.findLast(isAssistantMessage);
297
+ + if (ctx.mode === "rpc" && completedTurnId !== null && !event.aborted && !event.willRetry && !event.abortSource && !ctx.signal?.aborted
298
+ + && finalMessage && (finalMessage.stopReason === "stop" || finalMessage.stopReason === "length")
299
+ + && Number.isInteger(finalMessage.timestamp) && finalMessage.timestamp > 0
300
+ + && lastAssistant?.timestamp === finalMessage.timestamp
301
+ + && !messages.some((message) => message.stopReason === "error" || message.stopReason === "aborted")) {
302
+ + const stats = {};
303
+ + const hasOutput = messages.some((message) => Number.isFinite(message.usage?.output) && message.usage.output > 0);
304
+ + if (hasOutput) {
305
+ + if (Number.isFinite(elapsedMs) && elapsedMs > 0)
306
+ + stats.modelDurationMs = elapsedMs;
307
+ + const rate = measuredOutput / (measuredGenerationMs / 1000);
308
+ + if (measuredGenerationMs >= MIN_GENERATION_MS && Number.isFinite(rate) && rate > 0)
309
+ + stats.tokensPerSecond = rate;
310
+ + // Unknown usage must not look like a measured zero-percent cache hit.
311
+ + if (messages.every((message) => [message.usage?.input, message.usage?.cacheRead, message.usage?.cacheWrite]
312
+ + .every((value) => Number.isFinite(value) && value >= 0))) {
313
+ + let promptTokens = 0;
314
+ + let cacheRead = 0;
315
+ + for (const message of messages) {
316
+ + promptTokens += message.usage.input + message.usage.cacheRead + message.usage.cacheWrite;
317
+ + cacheRead += message.usage.cacheRead;
318
+ + }
319
+ + const hit = cacheRead / promptTokens * 100;
320
+ + if (Number.isFinite(promptTokens) && Number.isFinite(cacheRead) && Number.isFinite(hit) && hit >= 0 && hit <= 100)
321
+ + stats.cacheHitPercent = hit;
322
+ + }
323
+ + }
324
+ + if (Object.keys(stats).length > 0)
325
+ + pi.rpc.emit("zgw.turn_stats", { version: 1, turnId: completedTurnId, finalMessageTimestamp: finalMessage.timestamp, finalMessageIndex: messages.length, stats });
326
+ + }
327
+ activeAssistantStartMs = null;
328
+ assistantElapsedMs = 0;
329
+ generationMs = 0;
330
+ @@ -121,4 +165,4 @@ export default function (pi) {
331
+ ctx.ui.notify(`${rate}${stats}`, "info");
332
+ });
333
+ }
334
+ -//# sourceMappingURL=tps.js.map
335
+
336
+ +//# sourceMappingURL=tps.js.map
337
+ --- a/@code-yeongyu/senpi/dist/core/model-runtime.js
338
+ +++ b/@code-yeongyu/senpi/dist/core/model-runtime.js
339
+ @@ -76,6 +76,7 @@ function withPayloadRequestMetadata(options, model) {
340
+ onPayload: async (payload, providerModel) => await onPayload(payload, providerModel, {
341
+ model,
342
+ headers: options.headers ?? {},
343
+ + streamKind: options.streamKind,
344
+ }),
345
+ };
346
+ }
347
+ @@ -1165,4 +1166,4 @@ export class ModelRuntime {
348
+ return model && !isVirtualModel(model) ? model : undefined;
349
+ }
350
+ }
351
+ -//# sourceMappingURL=model-runtime.js.map
352
+
353
+ +//# sourceMappingURL=model-runtime.js.map
354
+ --- a/@earendil-works/pi-agent-core/dist/agent.d.ts
355
+ +++ b/@earendil-works/pi-agent-core/dist/agent.d.ts
356
+ @@ -194,6 +194,9 @@ export declare class Agent {
357
+ * the continuation input, while timeout overrides still apply to its first provider request.
358
+ */
359
+ continueWithQueuedMessages(options?: AgentContinuationOptions): Promise<void>;
360
+ + continueQueuedInput(clientInputId: string, options?: AgentContinuationOptions & {
361
+ + onStart?: () => void;
362
+ + }): Promise<"busy" | "not_head" | "resumed">;
363
+ /**
364
+ * Continue from the current transcript. The last message must be a user or tool-result message.
365
+ * Queue deferral and timeout overrides apply only to the first provider request; later requests in
366
+ @@ -242,4 +245,4 @@ export declare class Agent {
367
+ */
368
+ emitExternalEvent(event: AgentEvent, runSignal?: AbortSignal): Promise<void>;
369
+ }
370
+ -//# sourceMappingURL=agent.d.ts.map
371
+
372
+ +//# sourceMappingURL=agent.d.ts.map
373
+ --- a/@earendil-works/pi-agent-core/dist/agent.js
374
+ +++ b/@earendil-works/pi-agent-core/dist/agent.js
375
+ @@ -302,6 +302,21 @@ export class Agent {
376
+ * Queue-first recovery takes precedence over `deferQueuedMessages`: the selected queued message is
377
+ * the continuation input, while timeout overrides still apply to its first provider request.
378
+ */
379
+ + async continueQueuedInput(clientInputId, options = {}) {
380
+ + if (this.activeRun)
381
+ + return "busy";
382
+ + const steering = this.steeringQueue.hasItems();
383
+ + const queue = steering ? this.steeringQueue : this.followUpQueue;
384
+ + if (queue.messages[0]?.clientInputId !== clientInputId)
385
+ + return "not_head";
386
+ + options.onStart?.();
387
+ + await this.runQueuedMessages(steering ? "steering" : "followUp", {
388
+ + skipInitialSteeringPoll: steering,
389
+ + initialRequestTimeoutMs: options.timeoutMs,
390
+ + initialRequestStreamStartTimeoutMs: options.streamStartTimeoutMs,
391
+ + });
392
+ + return "resumed";
393
+ + }
394
+ async continueWithQueuedMessages(options = {}) {
395
+ if (this.activeRun) {
396
+ throw new Error("Agent is already processing. Wait for completion before continuing.");
397
+ @@ -681,4 +696,4 @@ export class Agent {
398
+ await this.processEvents(event);
399
+ }
400
+ }
401
+ -//# sourceMappingURL=agent.js.map
402
+
403
+ +//# sourceMappingURL=agent.js.map
404
+ --- a/@earendil-works/pi-ai/dist/types.d.ts
405
+ +++ b/@earendil-works/pi-ai/dist/types.d.ts
406
+ @@ -85,6 +85,7 @@ export type ProviderHeaders = Record<string, string | null>;
407
+ export type ProviderRequestMetadata = {
408
+ model: Model<Api>;
409
+ headers: ProviderHeaders;
410
+ + streamKind?: "main" | "auxiliary";
411
+ };
412
+ export type FetchFunction = typeof globalThis.fetch;
413
+ export interface ProviderResponse {
414
+ @@ -514,6 +515,8 @@ export interface SystemMessage {
415
+ timestamp: number;
416
+ }
417
+ export interface UserMessage {
418
+ + /** Gateway input identity preserved through expansion and queue consumption. */
419
+ + clientInputId?: string;
420
+ role: "user";
421
+ content: string | (TextContent | ImageContent)[];
422
+ timestamp: number;
423
+ @@ -1248,4 +1251,4 @@ export interface ModelTypeMap {
424
+ export type ModelType = keyof ModelTypeMap;
425
+ /** Anything a provider can list. Narrow with `isModelType()`. */
426
+ export type AnyModel = ModelTypeMap[ModelType];
427
+ -//# sourceMappingURL=types.d.ts.map
428
+
429
+ +//# sourceMappingURL=types.d.ts.map
430
+ --- a/omo-ai/bin/lib/package-paths.js
431
+ +++ b/omo-ai/bin/lib/package-paths.js
432
+ @@ -113,7 +113,8 @@ export function resolveSenpi(options = {}) {
433
+ // launcher change.
434
+ const bundlePath = join(distDir, "bundle", "cli.js")
435
+ const unbundledPath = join(distDir, "cli.js")
436
+ - const cliPath = existsSync(bundlePath) ? bundlePath : unbundledPath
437
+ + // Gateway package ports live in the unbundled graph, including relative RPC workers.
438
+ + const cliPath = unbundledPath
439
+ if (!existsSync(cliPath)) {
440
+ throw new Error(`senpi CLI is missing at ${cliPath}; reinstall with: ${updateTarget().command}`)
441
+ }