zigmini 0.8.0-beta.6 → 0.9.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 (62) hide show
  1. package/README.ko.md +90 -19
  2. package/README.md +91 -19
  3. package/assets/agent/gateway-request-rules.md +3 -0
  4. package/assets/agent/gateway-rules.md +6 -0
  5. package/assets/android/zigmini.apk +0 -0
  6. package/assets/android/zigmini.apk.json +3 -3
  7. package/assets/build.json +1 -1
  8. package/docs/manual.ko.md +69 -1
  9. package/docs/manual.md +67 -1
  10. package/host/omo-bridge/task-control.ts +4 -0
  11. package/host/omo-check/patches/omo-5.1.35-compaction-idle.patch +77 -0
  12. package/host/omo-check/patches/omo-5.1.35-compaction-intent.patch +212 -0
  13. package/host/omo-check/patches/omo-5.1.35-compaction-model.patch +299 -0
  14. package/host/omo-check/patches/omo-5.1.35-compaction-preservation.patch +1063 -0
  15. package/host/omo-check/patches/omo-5.1.35-compaction-refusal.patch +83 -0
  16. package/host/omo-check/patches/omo-5.1.35-gateway-port.manifest.json +169 -7
  17. package/host/omo-check/patches/omo-5.1.35-request-rules.patch +109 -0
  18. package/host/omo-check/patches/omo-5.1.35-turn-stats.patch +76 -0
  19. package/host/omo-check/patches/omo-5.1.44-compaction-port.patch +2630 -0
  20. package/host/omo-check/patches/omo-5.1.44-gateway-port.manifest.json +329 -0
  21. package/host/omo-check/patches/omo-5.1.44-provider-port.patch +440 -0
  22. package/host/omo-check/patches/omo-5.1.44-rpc-port.patch +778 -0
  23. package/package.json +4 -1
  24. package/scripts/secret-rules.ts +101 -0
  25. package/scripts/secret-scan.allow.json +155 -0
  26. package/scripts/secret-scan.rules.json +30 -0
  27. package/src/agent-request-rules.ts +82 -0
  28. package/src/cli/access.ts +31 -34
  29. package/src/cli/launchd.ts +30 -4
  30. package/src/cli/main.ts +3 -43
  31. package/src/cli/messages.ts +160 -0
  32. package/src/cli/pair.ts +32 -25
  33. package/src/cli/setup.ts +164 -75
  34. package/src/decisions.ts +48 -10
  35. package/src/engine/compat.json +3 -3
  36. package/src/hub.ts +183 -47
  37. package/src/main.ts +60 -4
  38. package/src/notification-history.ts +56 -0
  39. package/src/omo/client.ts +28 -0
  40. package/src/protocol.ts +32 -2
  41. package/src/reply-notifications.ts +144 -0
  42. package/src/runtime/paths.ts +2 -0
  43. package/src/security-allow.ts +126 -0
  44. package/src/security-check.ts +559 -0
  45. package/src/store.ts +236 -6
  46. package/src/turn-stats.ts +49 -0
  47. package/src/work-watch.ts +36 -10
  48. package/src/work.ts +45 -6
  49. package/web/DESIGN.md +39 -0
  50. package/web/app.js +289 -2
  51. package/web/decisions.d.ts +1 -1
  52. package/web/decisions.js +6 -0
  53. package/web/diagram.d.ts +28 -0
  54. package/web/diagram.js +228 -0
  55. package/web/index.html +29 -0
  56. package/web/notifications.d.ts +25 -0
  57. package/web/notifications.js +118 -0
  58. package/web/security-inbox.d.ts +35 -0
  59. package/web/security-inbox.js +237 -0
  60. package/web/style.css +43 -1
  61. package/web/turn-stats.d.ts +26 -0
  62. package/web/turn-stats.js +58 -0
package/docs/manual.md CHANGED
@@ -57,6 +57,16 @@ Shift+Enter starts a new line. Enter pressed during Korean IME composition doesn
57
57
 
58
58
  While the agent works, the 중단 (stop) button shows. Click it to stop the running work.
59
59
 
60
+ #### Conversation summaries
61
+
62
+ Compaction summarizes older conversation content so the same session can continue. When a summary blocks your work, chat shows one start notice and one completion, cancellation or failure notice for that request. Background preparation alone does not add chat notices. Applying a prepared summary while the session is idle doesn't ask you to wait either, so it adds no chat notice. A manual summary keeps its start and terminal notices. Reconnecting replays the stored notices, not a new summary request.
63
+
64
+ No new setting is required. With `compaction.model` absent or set to `"auto"`, the engine selects an eligible summary model from the current session's provider. The selected model must fit the input. Existing explicit `provider/model` settings stay in effect. An explicit model from another provider sends the conversation being summarized to that provider. The engine does not silently replace an invalid or unavailable explicit model. Existing global, trusted-project and runtime setting precedence stays in effect.
65
+
66
+ If a summary fails, the engine keeps the previous working context and stops instead of applying an emergency shortened substitute. The notice tells you what to do: check the model setting, log in to the summary provider again, wait for the usage limit to recover, or check account and organization permissions. Other failures require you to resolve the cause and explicitly continue or retry. The gateway does not resend the previous input or start a new session. A cancellation is separate from a failure. A successful summary is not a guarantee that every detail survives; check critical requirements when you continue.
67
+
68
+ The gateway opens main and work sessions in the engine's existing app mode, so replies are shaped for the chat screen rather than a terminal. You don't need to change any setting. App mode doesn't guarantee that every model answer follows a requested format such as valid JSON, and it doesn't change the summary recovery above.
69
+
60
70
  To start a new conversation, click + in the sidebar. A prompt asks for its name.
61
71
 
62
72
  #### Slash command menu
@@ -82,6 +92,42 @@ The README's Commands section says that sending `/usage` in chat makes the gatew
82
92
 
83
93
  ### 2.3 Attachments and references
84
94
 
95
+ #### Flow and structure diagrams
96
+
97
+ Web and Android chat render complete `zigmini-diagram` fenced blocks as diagrams.
98
+ Labels remain readable on narrow screens: scroll inside the diagram to reach
99
+ off-screen nodes. Open its source control to select and copy the original JSON.
100
+ Referencing the message sends its original text, not an image or a local path.
101
+ History and reconnect use the same stored text. No upload or external renderer is needed.
102
+
103
+ For agents, use this complete block in the assistant's message body:
104
+
105
+ ````markdown
106
+ ```zigmini-diagram
107
+ {"version":1,"title":"Request flow","nodes":[{"id":"request","label":"Request"},{"id":"gateway","label":"Gateway"},{"id":"reply","label":"Reply"}],"edges":[{"from":"request","to":"gateway"},{"from":"gateway","to":"reply"}]}
108
+ ```
109
+ ````
110
+
111
+ The supported format is a small directed acyclic graph, not Mermaid or arbitrary
112
+ SVG/HTML. Branching edges express a structure. Nodes appear top-to-bottom; nodes
113
+ at the same depth follow their input order.
114
+
115
+ - Root keys: required `version: 1`, `nodes`, `edges`; optional `title`.
116
+ - Each node has only `id` and `label`. Use 1–16 nodes with unique IDs matching
117
+ `[A-Za-z][A-Za-z0-9_-]{0,31}`. Labels are nonblank plain text, at most 48 Unicode code points.
118
+ - Each edge has only `from` and `to`, naming existing nodes. Use 0–24 edges.
119
+ Duplicate edges, self-links and cycles are not supported.
120
+ - The optional title is nonblank plain text, at most 80 Unicode code points.
121
+ Titles and labels cannot contain control characters U+0000–U+001F or U+007F.
122
+ - JSON is at most 16,384 UTF-16 code units. Extra keys are not supported.
123
+ Use unindented triple backticks and the exact language name shown above.
124
+
125
+ Unfinished, invalid or unsupported blocks remain visible as source. When a streamed
126
+ block becomes complete and valid, chat can render it. Diagram text cannot run
127
+ scripts, load URLs or read files. An older client displays the code block instead.
128
+ The concise per-request instruction asks for a diagram only when it helps explain
129
+ a flow or structure; it does not require diagrams in every reply.
130
+
85
131
  #### Images
86
132
 
87
133
  - Click 이미지 (image) to pick images.
@@ -163,6 +209,20 @@ The profile sits at the bottom of the sidebar.
163
209
 
164
210
  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
211
 
212
+ #### Security check (보안검사)
213
+
214
+ - "보안검사 켜짐/꺼짐" (security check on/off) turns the check of your messages on or off. It is on by default and the gateway keeps the choice.
215
+ - When a message or inbox answer contains a detected value that is not allowed, the gateway holds it before sending it to the agent. The same security item appears in the inbox and chat. It shows the complete values to be remembered and their types.
216
+ - Approving sends the original message and remembers the displayed exact values on this gateway. The same values do not prompt again. A different value still needs approval. Existing held messages do not send automatically.
217
+ - Cancel stops only this transmission. It does not stop the existing work. If the held transmission is an inbox answer, the original question remains unanswered and its answer form stays available.
218
+ - On web, a cancelled message can restore its text only in an empty input box of the same conversation. A newer draft is never replaced. Android does not restore a cancelled message to its input box. A transmission with an unconfirmed result is not automatically restored or sent again.
219
+ - Controls lock while the request is processing. HTTP replies, live events and reconnects update the same item. An unconfirmed result means the client has no proof of delivery; use the status check instead of sending again.
220
+ - Remembering happens before dispatch. If saving fails, the message is not sent. If saving succeeds but dispatch fails or cannot be confirmed, the values remain remembered and the result reports both facts.
221
+ - The allowed-value list is private to this installation and survives gateway restarts. Web settings show the list under Security check. On Android, open the gateway settings screen that contains Manual, then open the allowed values.
222
+ - Each entry shows its full value, type, approval time and approving client. Remove one entry or clear the list to check those values again on future transmissions. Removal does not cancel a transmission already approved or erase a sent message.
223
+ - An older app's send-only approval still sends once without remembering values. Remembering requires the new send-and-remember choice. Unapproved original messages are not saved in ordinary question records or history; pending originals are lost on gateway restart.
224
+ - This list affects runtime message and answer checking only. It does not change repository or release security checks.
225
+
166
226
  #### Update (업데이트)
167
227
 
168
228
  - The current and new versions are shown.
@@ -312,7 +372,13 @@ Tap the Zig face to open it. Slot states include running, waiting for input, wai
312
372
  ### 4.6 Notifications
313
373
 
314
374
  - Each registered gateway has its own notification connection.
315
- - Channels are "일정 알림" (reminders), "확인 요청" (requests) and "연결 유지" (keep connected).
375
+ - Open web settings to choose the app notification level. In the app, short press the header status tag to open the existing settings screen. Both choices are above Manual, not in work status.
376
+ - "확인함 알림만" (Inbox notifications only) is the default. It keeps the existing confirmation notifications.
377
+ - "모든 답장 알림" (All reply notifications) includes confirmation notifications and each completed main/work agent reply. User messages and streaming fragments do not alert.
378
+ - Both screens edit the same saved gateway value. The app edits only its active gateway. Changing the value does not replay old replies.
379
+ - If saving fails, the last confirmed choice stays selected. Retry reloads the saved value. Select the desired choice after reloading.
380
+ - Schedule reminders and explicit notifications are independent of this choice. The existing visible-web rule for confirmation notifications still applies.
381
+ - Reply notification taps open chat on the sending gateway. Replayed delivery of the same reply does not alert again.
316
382
  - Notifications vibrate with no sound.
317
383
  - Tapping a notification switches to the gateway that sent it.
318
384
  - The keep connected notification lists each gateway's state. If it says "인증 거부, 다시 등록하세요" (auth refused), register the phone again.
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { isDeepStrictEqual } from "node:util";
3
3
  import { runtimePaths } from "../../src/runtime/paths.ts";
4
4
  import { agentRules } from "../../src/agent-rules.ts";
5
+ import { applyRequestRules, loadRequestRules, type ProviderRequestEvent } from "../../src/agent-request-rules.ts";
5
6
 
6
7
  // The pinned runtime supplies this SDK surface, just as it does for model-control.ts.
7
8
  type Entry = { id: string; type: string; customType?: string; data?: unknown; message?: { role: string; content: unknown; toolName?: string; details?: unknown; toolCallId?: string } };
@@ -18,6 +19,7 @@ interface ExtensionApi {
18
19
  readonly events: { on(name: string, handler: (data: unknown) => void): () => void };
19
20
  on(event: "session_start", handler: (event: unknown, ctx: Context) => void): void;
20
21
  on(event: "before_agent_start", handler: (event: { systemPrompt: string }) => unknown, options: { previewSafe: boolean }): void;
22
+ on(event: "before_provider_request", handler: (event: ProviderRequestEvent) => unknown): void;
21
23
  registerTool(tool: Tool): void;
22
24
  appendEntry(customType: string, data: unknown): void;
23
25
  }
@@ -147,6 +149,8 @@ export default function taskControl(pi: ExtensionApi): void {
147
149
  const rules = agentRules();
148
150
  console.warn(rules.log);
149
151
  const rulesText = rules.text === "" ? "" : `\n\n${rules.text}`;
152
+ const requestRules = loadRequestRules();
153
+ pi.on("before_provider_request", (event) => applyRequestRules(event, requestRules));
150
154
 
151
155
  for (const kind of ["asked", "replied"] as const) {
152
156
  pi.events.on(`permission_${kind}`, (data) => {
@@ -0,0 +1,77 @@
1
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/index.js
2
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/index.js
3
+ @@ -138,1 +138,1 @@
4
+ - const result = await applyGeneratedCompaction(ctx, job.snapshot, () => speculativeGeneration, compaction);
5
+ + const result = await applyGeneratedCompaction(ctx, job.snapshot, () => speculativeGeneration, compaction, undefined, false);
6
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative.js
7
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative.js
8
+ @@ -303,1 +303,1 @@
9
+ -export async function applyGeneratedCompaction(context, snapshot, getCurrentGeneration, compaction, signal) {
10
+ +export async function applyGeneratedCompaction(context, snapshot, getCurrentGeneration, compaction, signal, blocking = true) {
11
+ @@ -319,3 +319,4 @@
12
+ return await context.applyCompaction(compaction, {
13
+ reason: "extension",
14
+ + blocking,
15
+ ...(revisionUnchanged || !warmAnchor
16
+ --- a/@code-yeongyu/senpi/dist/core/agent-session.js
17
+ +++ b/@code-yeongyu/senpi/dist/core/agent-session.js
18
+ @@ -6195,4 +6195,5 @@
19
+ this._emit({
20
+ type: "compaction_start",
21
+ reason: options.reason,
22
+ + blocking: options.blocking,
23
+ requestId,
24
+ @@ -6208,4 +6209,5 @@
25
+ owner: "compaction",
26
+ reason: options.reason,
27
+ + blocking: options.blocking,
28
+ requestId,
29
+ willRetry: false,
30
+ @@ -6227,4 +6229,5 @@
31
+ this._emit({
32
+ type: "compaction_end",
33
+ reason: options.reason,
34
+ + blocking: options.blocking,
35
+ result: undefined,
36
+ @@ -6479,4 +6482,5 @@
37
+ this._emit({
38
+ type: "compaction_end",
39
+ reason: request.reason,
40
+ + blocking: request.blocking,
41
+ result: compactionResult,
42
+ @@ -6591,4 +6595,5 @@
43
+ this._emit({
44
+ type: "compaction_end",
45
+ reason: request.reason,
46
+ + blocking: request.blocking,
47
+ result: undefined,
48
+ --- a/@code-yeongyu/senpi/dist/core/extensions/types.d.ts
49
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/types.d.ts
50
+ @@ -297,3 +297,4 @@
51
+ export interface ApplyCompactionOptions {
52
+ reason: CompactionReason;
53
+ + blocking?: boolean;
54
+ expectedRevision?: number;
55
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative.d.ts
56
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative.d.ts
57
+ @@ -22,3 +22,4 @@
58
+ applyCompaction(precomputed: CompactionResult, options: {
59
+ reason: "extension";
60
+ + blocking?: boolean;
61
+ expectedRevision?: number;
62
+ @@ -110,1 +111,1 @@
63
+ -export declare function applyGeneratedCompaction(context: SpeculativeCompactionContext, snapshot: SpeculativeCompactionSnapshot | undefined, getCurrentGeneration: () => number, compaction: CompactionResult | undefined, signal?: AbortSignal): Promise<SpeculativeCompactionResult>;
64
+ +export declare function applyGeneratedCompaction(context: SpeculativeCompactionContext, snapshot: SpeculativeCompactionSnapshot | undefined, getCurrentGeneration: () => number, compaction: CompactionResult | undefined, signal?: AbortSignal, blocking?: boolean): Promise<SpeculativeCompactionResult>;
65
+ --- a/@code-yeongyu/senpi/dist/core/agent-session.d.ts
66
+ +++ b/@code-yeongyu/senpi/dist/core/agent-session.d.ts
67
+ @@ -140,3 +140,4 @@
68
+ type: "compaction_start";
69
+ reason: CompactionReason;
70
+ + blocking?: boolean;
71
+ requestId?: string;
72
+ @@ -229,4 +230,5 @@
73
+ type: "compaction_end";
74
+ failure?: CompactionFailure;
75
+ reason: CompactionReason;
76
+ + blocking?: boolean;
77
+ result: CompactionResult | undefined;
@@ -0,0 +1,212 @@
1
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative.js
2
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative.js
3
+ @@ -13,1 +13,1 @@
4
+ -import { extractTaskIntent, resolveInheritedTaskIntent } from "./task-intent.js";
5
+ +import { extractTaskIntent, resolveInheritedTaskIntent, taskIntentSummary, taskIntentMetadata } from "./task-intent.js";
6
+ @@ -268,1 +268,5 @@
7
+ - const summary = parsedSummary.summaryText;
8
+ + const inheritedIntent = resolveInheritedTaskIntent(snapshot.branchEntries ?? []);
9
+ + if (inheritedIntent && parsedSummary.taskIntent && parsedSummary.taskIntent !== inheritedIntent)
10
+ + throw new SummaryGenerationError("incomplete", "Compaction changed protected task intent");
11
+ + const taskIntent = inheritedIntent ?? parsedSummary.taskIntent;
12
+ + const summary = taskIntentSummary(parsedSummary.summaryText, taskIntent);
13
+ @@ -273,1 +277,0 @@
14
+ - const taskIntent = parsedSummary.taskIntent ?? resolveInheritedTaskIntent(snapshot.branchEntries ?? []);
15
+ @@ -275,1 +278,1 @@
16
+ - summary: parsedSummary.summaryText,
17
+ + summary,
18
+ @@ -291,1 +294,1 @@
19
+ - summary: parsedSummary.summaryText,
20
+ + summary,
21
+ @@ -295,1 +298,1 @@
22
+ - ...(taskIntent ? { taskIntent } : {}),
23
+ + ...(taskIntent ? { taskIntent: taskIntentMetadata(taskIntent) } : {}),
24
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote.js
25
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote.js
26
+ @@ -4,0 +4,3 @@
27
+ +import { collectIntentProtection, protectedRemoteInput, taskIntentSummary, retainedUserText } from "./task-intent.js";
28
+ +import { estimateTokens } from "../../../compaction/index.js";
29
+ +import { resolveEffectiveReserveTokens } from "./policy.js";
30
+ @@ -23,1 +26,1 @@
31
+ - const input = options.messages
32
+ + const converted = options.messages
33
+ @@ -26,0 +29,1 @@
34
+ + const input = protectedRemoteInput(converted, collectIntentProtection(options.branchEntries));
35
+ @@ -55,0 +59,1 @@
36
+ + ...(typeof options.value.model === "string" ? { observedModel: options.value.model } : {}),
37
+ @@ -71,0 +76,1 @@
38
+ + ...(options.response.observedModel ? { observedProvider: options.model.provider, observedModel: options.response.observedModel } : {}),
39
+ @@ -140,0 +146,1 @@
40
+ + ...(options.response.responseModel ? { observedProvider: options.response.provider, observedModel: options.response.responseModel } : {}),
41
+ @@ -177,0 +184,1 @@
42
+ + streamKind: "auxiliary",
43
+ @@ -334,0 +342,22 @@
44
+ + const protection = collectIntentProtection(event.branchEntries);
45
+ + const protectedInput = protectedRemoteInput([], protection);
46
+ + const model = ctx.model;
47
+ + const settings = ctx.getCompactionSettings?.() ?? event.preparation.settings;
48
+ + const protectionTokens = estimateTokens({ role: "user", content: JSON.stringify(protectedInput) });
49
+ + if (model && protectionTokens > model.contextWindow - resolveEffectiveReserveTokens(model.contextWindow, settings))
50
+ + return { status: "ineligible" }; // Before dispatch only: same-model text compaction is permitted.
51
+ + const attempt = await runOpenAiRemoteCompactionAttempt(ctx, event, emit, dependencies);
52
+ + if (attempt.status !== "accepted") return attempt;
53
+ + const result = attempt.compaction;
54
+ + const checkpointTexts = new Set(convertPendingMessages(buildSessionContext(event.branchEntries).messages.filter((message) => message.role === "compactionSummary"), model).map(retainedUserText));
55
+ + // The accepted native envelope already covers the request checkpoint. Keep its original intent outside it, not a second raw copy of the old summary.
56
+ + const retained = result.details.replacementInput.filter((item) => !checkpointTexts.has(retainedUserText(item)));
57
+ + const replacementInput = protectedRemoteInput(retained, protection);
58
+ + return { status: "accepted", compaction: { ...result,
59
+ + summary: taskIntentSummary(result.summary, protection.taskIntent) + (protection.sources.length ? `\n<source-user-text>\n${JSON.stringify(protection.sources)}\n</source-user-text>` : ""),
60
+ + details: { ...result.details, intentProtection: protection, replacementInput, retainedInputItemCount: replacementInput.length,
61
+ + selectionMode: settings.model === undefined || settings.model === "auto" ? "auto" : "explicit",
62
+ + requestedProvider: model.provider, requestedModel: model.id },
63
+ + } };
64
+ +}
65
+ +async function runOpenAiRemoteCompactionAttempt(ctx, event, emit, dependencies = {}) {
66
+ @@ -410,0 +440,4 @@
67
+ + const requestTokens = estimateTokens({ role: "user", content: JSON.stringify(request.body) });
68
+ + const requestSettings = ctx.getCompactionSettings?.() ?? event.preparation.settings;
69
+ + if (requestTokens > requestModel.contextWindow - resolveEffectiveReserveTokens(requestModel.contextWindow, requestSettings))
70
+ + return undefined;
71
+ @@ -765,1 +799,1 @@
72
+ - ...remote.details.replacementInput,
73
+ + ...protectedRemoteInput(remote.details.replacementInput, remote.details.intentProtection),
74
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-responses-v2.js
75
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-responses-v2.js
76
+ @@ -109,0 +109,1 @@
77
+ + ...(assistant.responseModel ? { observedProvider: assistant.provider, observedModel: assistant.responseModel } : {}),
78
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/rejected-recovery.js
79
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/rejected-recovery.js
80
+ @@ -6,0 +6,2 @@
81
+ +import { getOpenAiRemoteCompactionDetails, convertPendingMessages } from "./openai-remote-convert.js";
82
+ +import { protectedRemoteInput } from "./task-intent.js";
83
+ @@ -33,1 +35,6 @@
84
+ - const tokens = filterContextExcludedMessages(messages).reduce((total, message) => total + estimateTokens(message), 0);
85
+ + const remote = getOpenAiRemoteCompactionDetails(compactionResult.details);
86
+ + const budgetMessages = remote ? [{ role: "user", content: JSON.stringify([
87
+ + ...protectedRemoteInput(remote.replacementInput, remote.intentProtection),
88
+ + ...convertPendingMessages(pendingMessages, model),
89
+ + ]) }] : messages;
90
+ + const tokens = filterContextExcludedMessages(budgetMessages).reduce((total, message) => total + estimateTokens(message), 0);
91
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-convert.js
92
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-convert.js
93
+ @@ -1,0 +1,1 @@
94
+ +import { protectedRemoteInput } from "./task-intent.js";
95
+ @@ -2,1 +3,1 @@
96
+ -import { sessionEntryToContextMessages } from "../../../session-manager.js";
97
+ +import { sessionEntryToContextMessages, getSessionContextEntryId, SESSION_CONTEXT_ENTRY_ID } from "../../../session-manager.js";
98
+ @@ -142,2 +143,6 @@
99
+ - case "user":
100
+ - return [{ role: "user", content: convertUserContent(message.content) }];
101
+ + case "user": {
102
+ + const item = { role: "user", content: convertUserContent(message.content) };
103
+ + const sourceId = getSessionContextEntryId(message) ?? message[SESSION_CONTEXT_ENTRY_ID];
104
+ + if (sourceId) Object.defineProperty(item, "__senpiCompactionSourceId", { value: sourceId });
105
+ + return [item];
106
+ + }
107
+ @@ -188,1 +193,1 @@
108
+ - items.push(...details.replacementInput);
109
+ + items.push(...protectedRemoteInput(details.replacementInput, details.intentProtection));
110
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/prompts.js
111
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/prompts.js
112
+ @@ -9,2 +9,2 @@
113
+ -Immutable provenance of the original task. Do not rewrite it. Newer explicit user steering overrides it.
114
+ -</task-intent>`;
115
+ +</task-intent>
116
+ +Immutable provenance of the original task. Do not rewrite it. Newer explicit user steering overrides it.`;
117
+ @@ -13,2 +13,2 @@
118
+ -Immutable provenance of the original task. Do not rewrite it. Newer explicit user steering overrides it.
119
+ -</task-intent>`;
120
+ +</task-intent>
121
+ +Immutable provenance of the original task. Do not rewrite it. Newer explicit user steering overrides it.`;
122
+ @@ -22,1 +22,1 @@
123
+ -R3. Where a previous summary is supplied, treat its User Requests, Final Goal, and Constraints fields as IMMUTABLE. Append, never rewrite, those three sections.
124
+ +R3. Preserve original requests and durable constraints as quoted provenance. Latest explicit user steering governs the current goal and next action. An answer-local restriction applies only to that answer.
125
+ @@ -86,1 +86,1 @@
126
+ -R3 enforcement: R3. Where a previous summary is supplied, treat its User Requests, Final Goal, and Constraints fields as IMMUTABLE. Append, never rewrite, those three sections.
127
+ +Preserve original requests and durable constraints as quoted provenance. Latest explicit user steering governs the current goal and next action. Keep answer-local restrictions local to their answer.
128
+ @@ -179,1 +179,1 @@
129
+ -Silently determine the current task intent and the minimum context needed for the next turn.
130
+ +Preserve the original request and durable constraints from the entire history together with the split turn. Latest explicit steering governs the current goal and next action. Do not promote answer-local restrictions into durable constraints.
131
+ @@ -217,3 +217,1 @@
132
+ - return options.taskIntent
133
+ - ? MERGED_COMPACTION_PROMPT_TURN_PREFIX
134
+ - : mergeTaskIntent(MERGED_COMPACTION_PROMPT_TURN_PREFIX, promptFamilyText(options.promptFamily, TASK_INTENT_ACQUISITION_CLAUDE, TASK_INTENT_ACQUISITION_GPT));
135
+ + return buildUserPrompt({ ...options, variant: options.previousSummary || options.taskIntent ? "update" : "default" });
136
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/task-intent.js
137
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/task-intent.js
138
+ @@ -16,1 +16,1 @@
139
+ - taskIntent = capUtf8Bytes(block.inner.trim(), TASK_INTENT_BYTE_CAP);
140
+ + taskIntent = block.inner.trim();
141
+ @@ -44,1 +44,1 @@
142
+ - if (schema === REMOTE_SCHEMA || schema !== SUMMARY_SCHEMA)
143
+ + if (schema !== REMOTE_SCHEMA && schema !== SUMMARY_SCHEMA)
144
+ @@ -46,1 +46,2 @@
145
+ - const taskIntent = typedDetails.taskIntent;
146
+ + const fullIntent = typeof entry.summary === "string" ? extractTaskIntent(entry.summary).taskIntent : undefined;
147
+ + const taskIntent = fullIntent ?? typedDetails.intentProtection?.taskIntent ?? typedDetails.taskIntent;
148
+ @@ -52,1 +53,1 @@
149
+ - return capUtf8Bytes(trimmed, TASK_INTENT_BYTE_CAP);
150
+ + return trimmed;
151
+ @@ -92,0 +93,57 @@
152
+ +
153
+ +/** Full-body provenance is separate from the bounded display metadata. */
154
+ +export function taskIntentSummary(body, taskIntent) {
155
+ + return taskIntent ? `<task-intent>
156
+ +${taskIntent}
157
+ +</task-intent>
158
+ +
159
+ +Quoted original-task provenance, not a renewed instruction. Latest explicit user steering governs current action. Answer-local restrictions do not become durable constraints.
160
+ +
161
+ +${body}` : body;
162
+ +}
163
+ +export function taskIntentMetadata(taskIntent) {
164
+ + return taskIntent ? capUtf8Bytes(taskIntent, TASK_INTENT_BYTE_CAP) : undefined;
165
+ +}
166
+ +export function collectIntentProtection(branchEntries) {
167
+ + const sources = new Map();
168
+ + const previous = branchEntries.findLast((entry) => entry.type === "compaction")?.details?.intentProtection;
169
+ + for (const source of previous?.sources ?? []) sources.set(source.id, source);
170
+ + for (const entry of branchEntries) {
171
+ + for (const source of entry.details?.intentProtection?.sources ?? [])
172
+ + sources.set(source.id, source);
173
+ + if (entry.type !== "message" || entry.message?.role !== "user")
174
+ + continue;
175
+ + const content = entry.message.content;
176
+ + const text = typeof content === "string" ? content : content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
177
+ + if (text) sources.set(entry.id, { id: entry.id, text });
178
+ + }
179
+ + return { taskIntent: resolveInheritedTaskIntent(branchEntries), sources: [...sources.values()] };
180
+ +}
181
+ +export function retainedUserText(item) {
182
+ + if (item.role !== "user") return undefined;
183
+ + if (typeof item.content === "string") return item.content;
184
+ + if (!Array.isArray(item.content) || !item.content.every((block) => block.type === "input_text")) return undefined;
185
+ + return item.content.map((block) => block.text).join("\n");
186
+ +}
187
+ +export function protectedRemoteInput(replacementInput, protection) {
188
+ + if (!protection) return replacementInput;
189
+ + const input = replacementInput.filter((item) => {
190
+ + const text = retainedUserText(item);
191
+ + if (!text) return true;
192
+ + if (protection.sources.some((source) => source.id === item.__senpiCompactionSourceId && source.text === text)) return false;
193
+ + if (protection.taskIntent && text === taskIntentSummary("", protection.taskIntent)) return false;
194
+ + const start = text.indexOf("<source-user-text>\n");
195
+ + const end = text.indexOf("\n</source-user-text>", start);
196
+ + if (start < 0 || end < 0) return true;
197
+ + let records;
198
+ + try { records = JSON.parse(text.slice(start + 19, end)); } catch { return true; }
199
+ + if (!Array.isArray(records) || !records.every((source) => protection.sources.some((known) => known.id === source.id && known.text === source.text))) return true;
200
+ + const canonical = taskIntentSummary(`Quoted user source records; newer explicit steering takes precedence.\n<source-user-text>\n${JSON.stringify(records)}\n</source-user-text>`, protection.taskIntent);
201
+ + return text !== canonical;
202
+ + });
203
+ + const remaining = protection.sources;
204
+ + const text = taskIntentSummary(remaining.length ? `Quoted user source records; newer explicit steering takes precedence.\n<source-user-text>
205
+ +${JSON.stringify(remaining)}
206
+ +</source-user-text>` : "", protection.taskIntent);
207
+ + return text ? [...input, { type: "message", role: "user", content: [{ type: "input_text", text }] }] : input;
208
+ +}
209
+ --- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-schema.js
210
+ +++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-schema.js
211
+ @@ -52,0 +52,1 @@
212
+ + ...(isRecord(value.intentProtection) ? { intentProtection: value.intentProtection } : {}),