@oh-my-pi/pi-coding-agent 14.8.1 → 14.9.0
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/CHANGELOG.md +38 -0
- package/package.json +16 -7
- package/src/config/model-resolver.ts +92 -35
- package/src/config/prompt-templates.ts +1 -1
- package/src/debug/index.ts +21 -0
- package/src/debug/raw-sse-buffer.ts +229 -0
- package/src/debug/raw-sse.ts +213 -0
- package/src/edit/index.ts +9 -10
- package/src/edit/streaming.ts +6 -5
- package/src/eval/js/context-manager.ts +91 -47
- package/src/extensibility/extensions/loader.ts +9 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +99 -20
- package/src/hashline/anchors.ts +113 -0
- package/src/hashline/apply.ts +732 -0
- package/src/hashline/bigrams.json +649 -0
- package/src/hashline/constants.ts +8 -0
- package/src/hashline/diff-preview.ts +43 -0
- package/src/hashline/diff.ts +56 -0
- package/src/hashline/execute.ts +268 -0
- package/src/{edit/modes/hashline.lark → hashline/grammar.lark} +1 -1
- package/src/{edit/line-hash.ts → hashline/hash.ts} +5 -651
- package/src/hashline/index.ts +14 -0
- package/src/hashline/input.ts +110 -0
- package/src/hashline/parser.ts +220 -0
- package/src/hashline/prefixes.ts +101 -0
- package/src/hashline/recovery.ts +72 -0
- package/src/hashline/stream.ts +123 -0
- package/src/hashline/types.ts +69 -0
- package/src/hashline/utils.ts +3 -0
- package/src/index.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/lsp/render.ts +4 -0
- package/src/memories/index.ts +13 -4
- package/src/modes/components/assistant-message.ts +55 -9
- package/src/modes/components/welcome.ts +114 -38
- package/src/modes/controllers/event-controller.ts +3 -1
- package/src/modes/controllers/input-controller.ts +8 -1
- package/src/modes/interactive-mode.ts +9 -9
- package/src/modes/rpc/rpc-client.ts +53 -2
- package/src/modes/rpc/rpc-mode.ts +67 -1
- package/src/modes/rpc/rpc-types.ts +17 -2
- package/src/modes/utils/ui-helpers.ts +3 -1
- package/src/prompts/agents/reviewer.md +14 -0
- package/src/prompts/tools/hashline.md +57 -10
- package/src/sdk.ts +4 -3
- package/src/session/agent-session.ts +195 -30
- package/src/session/compaction/branch-summarization.ts +4 -2
- package/src/session/compaction/compaction.ts +22 -3
- package/src/task/executor.ts +21 -2
- package/src/task/index.ts +4 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/match-line-format.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/title-generator.ts +11 -0
- package/src/edit/modes/hashline.ts +0 -2039
|
@@ -67,7 +67,11 @@ export type RpcCommand =
|
|
|
67
67
|
| { id?: string; type: "handoff"; customInstructions?: string }
|
|
68
68
|
|
|
69
69
|
// Messages
|
|
70
|
-
| { id?: string; type: "get_messages" }
|
|
70
|
+
| { id?: string; type: "get_messages" }
|
|
71
|
+
|
|
72
|
+
// Login
|
|
73
|
+
| { id?: string; type: "get_login_providers" }
|
|
74
|
+
| { id?: string; type: "login"; providerId: string };
|
|
71
75
|
|
|
72
76
|
// ============================================================================
|
|
73
77
|
// RPC State
|
|
@@ -193,6 +197,16 @@ export type RpcResponse =
|
|
|
193
197
|
// Messages
|
|
194
198
|
| { id?: string; type: "response"; command: "get_messages"; success: true; data: { messages: AgentMessage[] } }
|
|
195
199
|
|
|
200
|
+
// Login
|
|
201
|
+
| {
|
|
202
|
+
id?: string;
|
|
203
|
+
type: "response";
|
|
204
|
+
command: "get_login_providers";
|
|
205
|
+
success: true;
|
|
206
|
+
data: { providers: Array<{ id: string; name: string; available: boolean; authenticated: boolean }> };
|
|
207
|
+
}
|
|
208
|
+
| { id?: string; type: "response"; command: "login"; success: true; data: { providerId: string } }
|
|
209
|
+
|
|
196
210
|
// Error response (any command can fail)
|
|
197
211
|
| { id?: string; type: "response"; command: string; success: false; error: string };
|
|
198
212
|
|
|
@@ -244,7 +258,8 @@ export type RpcExtensionUIRequest =
|
|
|
244
258
|
widgetPlacement?: "aboveEditor" | "belowEditor";
|
|
245
259
|
}
|
|
246
260
|
| { type: "extension_ui_request"; id: string; method: "setTitle"; title: string }
|
|
247
|
-
| { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string }
|
|
261
|
+
| { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string }
|
|
262
|
+
| { type: "extension_ui_request"; id: string; method: "open_url"; url: string; instructions?: string };
|
|
248
263
|
|
|
249
264
|
// ============================================================================
|
|
250
265
|
// Host Tool Frames (bidirectional)
|
|
@@ -226,7 +226,9 @@ export class UiHelpers {
|
|
|
226
226
|
break;
|
|
227
227
|
}
|
|
228
228
|
case "assistant": {
|
|
229
|
-
const assistantComponent = new AssistantMessageComponent(message, this.ctx.hideThinkingBlock)
|
|
229
|
+
const assistantComponent = new AssistantMessageComponent(message, this.ctx.hideThinkingBlock, () =>
|
|
230
|
+
this.ctx.ui.requestRender(),
|
|
231
|
+
);
|
|
230
232
|
this.ctx.chatContainer.addChild(assistantComponent);
|
|
231
233
|
break;
|
|
232
234
|
}
|
|
@@ -77,6 +77,20 @@ Report issue only when ALL conditions hold:
|
|
|
77
77
|
- **Proportionate rigor**: Fix doesn't demand rigor absent elsewhere in codebase
|
|
78
78
|
</criteria>
|
|
79
79
|
|
|
80
|
+
<cross-boundary>
|
|
81
|
+
For every new type, variant, or value introduced by the patch that crosses a function or module boundary
|
|
82
|
+
(event, message, command, frame, enum variant, queue item, IPC payload):
|
|
83
|
+
1. Locate the **dispatch point** — the switch, router, filter chain, handler registry, or loop body
|
|
84
|
+
that receives and routes values of that kind on the **consuming** side.
|
|
85
|
+
2. Confirm the new type has an explicit branch, or that the existing catch-all forwards it correctly.
|
|
86
|
+
3. If the new type falls through to a silent drop, no-op, or discard (e.g. an unmatched `if`/`switch`
|
|
87
|
+
that simply returns without processing), report it as a defect.
|
|
88
|
+
|
|
89
|
+
The dispatch point is frequently **outside the diff**. You **MUST** read it before concluding
|
|
90
|
+
the producing side is correct. Tracing only the emitting code while skipping the consuming
|
|
91
|
+
routing logic is the single most common source of missed integration bugs in reviews.
|
|
92
|
+
</cross-boundary>
|
|
93
|
+
|
|
80
94
|
<priority>
|
|
81
95
|
|Level|Criteria|Example|
|
|
82
96
|
|---|---|---|
|
|
@@ -10,17 +10,18 @@ Purely textual format. The tool has NO awareness of language, indentation, brack
|
|
|
10
10
|
@PATH header: subsequent ops apply to PATH
|
|
11
11
|
+ ANCHOR insert lines AFTER the anchored line (or EOF); payload follows as `{{hsep}}TEXT` lines
|
|
12
12
|
< ANCHOR insert lines BEFORE the anchored line (or BOF); payload follows as `{{hsep}}TEXT` lines
|
|
13
|
-
- A..B delete the line range (inclusive)
|
|
14
|
-
= A..B replace the range with payload `{{hsep}}TEXT` lines, or with one blank line if no payload follows
|
|
13
|
+
- A..B delete the line range (inclusive).
|
|
14
|
+
= A..B replace the range with payload `{{hsep}}TEXT` lines, or with one blank line if no payload follows.
|
|
15
15
|
</ops>
|
|
16
16
|
|
|
17
17
|
<rules>
|
|
18
18
|
- Every line of inserted/replacement content **MUST** be emitted as a payload line starting with `{{hsep}}`.
|
|
19
19
|
- `{{hsep}}` is syntax, not content. The inserted text begins after the first `{{hsep}}`; use a bare `{{hsep}}` to insert a blank line.
|
|
20
20
|
- `< A` inserts before line A; `+ A` inserts after line A. `< BOF` / `+ BOF` both prepend; `< EOF` / `+ EOF` both append.
|
|
21
|
-
- `= A..B` replaces the inclusive range with the following payload lines. `= A
|
|
22
|
-
- `- A..B` deletes the inclusive range;
|
|
23
|
-
-
|
|
21
|
+
- `= A..B` replaces the inclusive range with the following payload lines. `= A..B` with no payload blanks the range to a single empty line.
|
|
22
|
+
- `- A..B` deletes the inclusive range; `A..A` for one line.
|
|
23
|
+
- **Choose a self-contained syntactic unit first.** If the change touches part of a multiline call, destructuring assignment, control-flow header, wrapper, or other construct, widen the range to include the whole construct before optimizing for size.
|
|
24
|
+
- Only after the range is self-contained, pick the smallest op for the change: pure addition → `+`/`<`; pure deletion → `-`; `= A..B` ONLY when content inside `A..B` is actually being modified or removed.
|
|
24
25
|
</rules>
|
|
25
26
|
|
|
26
27
|
<brace-shapes>
|
|
@@ -35,7 +36,7 @@ When your edit involves brace boundaries (`{` / `}`), prefer these shapes:
|
|
|
35
36
|
- **Do not replay the line past your range.** For `= A..B`, never end the payload with content that already exists at B+1. Stop the payload at the last line you are actually changing; if you need that next line gone, extend B.
|
|
36
37
|
- **Do not duplicate chunks inside one payload.** When emitting a long `=` payload, never paste the same multi-line block twice. If you catch yourself re-emitting an earlier run of lines, stop and rewrite the op.
|
|
37
38
|
- **Anchor only inside the visible region.** If the read output around your `=`/`-` end anchor is truncated (you cannot see the line at B+1), issue a fresh `read` before editing — anchoring blind drops or duplicates the boundary line.
|
|
38
|
-
- **Prefer
|
|
39
|
+
- **Prefer the narrowest self-contained edit.** Once your range cleanly contains the construct you are changing, a `+`/`<` insert plus a small `-` delete is almost always clearer and safer than a single wide `= A..B` that re-emits unchanged context.
|
|
39
40
|
</common-failures>
|
|
40
41
|
|
|
41
42
|
<case file="a.ts">
|
|
@@ -47,10 +48,22 @@ When your edit involves brace boundaries (`{` / `}`), prefer these shapes:
|
|
|
47
48
|
{{hline 6 "}"}}
|
|
48
49
|
</case>
|
|
49
50
|
|
|
51
|
+
<case file="b.ts">
|
|
52
|
+
{{hline 1 "const {"}}
|
|
53
|
+
{{hline 2 "\tevents,"}}
|
|
54
|
+
{{hline 3 "\tresponse,"}}
|
|
55
|
+
{{hline 4 "\trequestId,"}}
|
|
56
|
+
{{hline 5 "} = await getStreamResponse("}}
|
|
57
|
+
{{hline 6 "\trequest,"}}
|
|
58
|
+
{{hline 7 "\tsignal,"}}
|
|
59
|
+
{{hline 8 ");"}}
|
|
60
|
+
{{hline 9 "await notify(requestId);"}}
|
|
61
|
+
</case>
|
|
62
|
+
|
|
50
63
|
<examples>
|
|
51
64
|
# Replace one line (preserve the leading tab from the original)
|
|
52
65
|
@a.ts
|
|
53
|
-
= {{hrefr 5}}
|
|
66
|
+
= {{hrefr 5}}..{{hrefr 5}}
|
|
54
67
|
{{hsep}} return clean.trim().toUpperCase();
|
|
55
68
|
|
|
56
69
|
# Replace a contiguous range with multiple lines
|
|
@@ -59,6 +72,19 @@ When your edit involves brace boundaries (`{` / `}`), prefer these shapes:
|
|
|
59
72
|
{{hsep}} const clean = (name || DEF).trim();
|
|
60
73
|
{{hsep}} return clean.length === 0 ? DEF : clean.toUpperCase();
|
|
61
74
|
|
|
75
|
+
# Replace a full multiline destructuring/call statement
|
|
76
|
+
@b.ts
|
|
77
|
+
= {{hrefr 1}}..{{hrefr 8}}
|
|
78
|
+
{{hsep}}const {
|
|
79
|
+
{{hsep}} events,
|
|
80
|
+
{{hsep}} response,
|
|
81
|
+
{{hsep}} requestId,
|
|
82
|
+
{{hsep}}} = await getStreamResponse(
|
|
83
|
+
{{hsep}} request,
|
|
84
|
+
{{hsep}} signal,
|
|
85
|
+
{{hsep}} onEvent,
|
|
86
|
+
{{hsep}});
|
|
87
|
+
|
|
62
88
|
# Insert BEFORE a line
|
|
63
89
|
@a.ts
|
|
64
90
|
< {{hrefr 5}}
|
|
@@ -80,11 +106,11 @@ When your edit involves brace boundaries (`{` / `}`), prefer these shapes:
|
|
|
80
106
|
|
|
81
107
|
# Delete a single line
|
|
82
108
|
@a.ts
|
|
83
|
-
- {{hrefr 2}}
|
|
109
|
+
- {{hrefr 2}}..{{hrefr 2}}
|
|
84
110
|
|
|
85
111
|
# Blank a line in place (no payload required)
|
|
86
112
|
@a.ts
|
|
87
|
-
= {{hrefr 2}}
|
|
113
|
+
= {{hrefr 2}}..{{hrefr 2}}
|
|
88
114
|
</examples>
|
|
89
115
|
|
|
90
116
|
<anti-pattern>
|
|
@@ -103,7 +129,28 @@ When your edit involves brace boundaries (`{` / `}`), prefer these shapes:
|
|
|
103
129
|
+ {{hrefr 1}}
|
|
104
130
|
{{hsep}}const DEBUG = false;
|
|
105
131
|
|
|
106
|
-
|
|
132
|
+
# WRONG — continuation-fragment payload from the middle of a larger statement.
|
|
133
|
+
@b.ts
|
|
134
|
+
= {{hrefr 5}}..{{hrefr 7}}
|
|
135
|
+
{{hsep}}} = await getStreamResponse(
|
|
136
|
+
{{hsep}} request,
|
|
137
|
+
{{hsep}} signal,
|
|
138
|
+
{{hsep}} onEvent,
|
|
139
|
+
|
|
140
|
+
# RIGHT — widen to the full statement so the payload starts at a self-contained boundary.
|
|
141
|
+
@b.ts
|
|
142
|
+
= {{hrefr 1}}..{{hrefr 8}}
|
|
143
|
+
{{hsep}}const {
|
|
144
|
+
{{hsep}} events,
|
|
145
|
+
{{hsep}} response,
|
|
146
|
+
{{hsep}} requestId,
|
|
147
|
+
{{hsep}}} = await getStreamResponse(
|
|
148
|
+
{{hsep}} request,
|
|
149
|
+
{{hsep}} signal,
|
|
150
|
+
{{hsep}} onEvent,
|
|
151
|
+
{{hsep}});
|
|
152
|
+
|
|
153
|
+
If your replacement payload would render with even one unchanged line in the diff, or if the first or last payload line is only a continuation fragment from a larger construct (`} =`, `);`, `,`, `.method(`), you have the wrong op or range. Stop and widen to a self-contained boundary before minimizing the edit.
|
|
107
154
|
</anti-pattern>
|
|
108
155
|
|
|
109
156
|
<critical>
|
package/src/sdk.ts
CHANGED
|
@@ -1675,9 +1675,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1675
1675
|
preferWebsockets: preferOpenAICodexWebsockets,
|
|
1676
1676
|
getToolContext: tc => toolContextStore.getContext(tc),
|
|
1677
1677
|
getApiKey: async provider => {
|
|
1678
|
-
//
|
|
1679
|
-
//
|
|
1680
|
-
const key = await modelRegistry.getApiKeyForProvider(provider,
|
|
1678
|
+
// Read agent.sessionId at call time so credential selection stays aligned
|
|
1679
|
+
// with metadataResolver after /new, fork, resume, or branch switches.
|
|
1680
|
+
const key = await modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
|
|
1681
1681
|
if (!key) {
|
|
1682
1682
|
throw new Error(`No API key found for provider "${provider}"`);
|
|
1683
1683
|
}
|
|
@@ -1757,6 +1757,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1757
1757
|
asyncJobManager,
|
|
1758
1758
|
agentId: resolvedAgentId,
|
|
1759
1759
|
agentRegistry,
|
|
1760
|
+
providerSessionId: options.providerSessionId,
|
|
1760
1761
|
});
|
|
1761
1762
|
hasSession = true;
|
|
1762
1763
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* Modes use this class and add their own I/O layer on top.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import * as crypto from "node:crypto";
|
|
16
17
|
import * as fs from "node:fs";
|
|
17
18
|
import * as path from "node:path";
|
|
18
19
|
|
|
@@ -67,6 +68,7 @@ import {
|
|
|
67
68
|
} from "../config/model-resolver";
|
|
68
69
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
69
70
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
71
|
+
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
70
72
|
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
71
73
|
import {
|
|
72
74
|
disposeKernelSessionsByOwner,
|
|
@@ -148,7 +150,9 @@ import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
|
148
150
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
149
151
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
150
152
|
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
153
|
+
import type { AuthStorage } from "./auth-storage";
|
|
151
154
|
import {
|
|
155
|
+
type CompactionPreparation,
|
|
152
156
|
type CompactionResult,
|
|
153
157
|
calculateContextTokens,
|
|
154
158
|
calculatePromptTokens,
|
|
@@ -157,6 +161,7 @@ import {
|
|
|
157
161
|
estimateTokens,
|
|
158
162
|
generateBranchSummary,
|
|
159
163
|
prepareCompaction,
|
|
164
|
+
type SummaryOptions,
|
|
160
165
|
shouldCompact,
|
|
161
166
|
} from "./compaction";
|
|
162
167
|
import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "./compaction/pruning";
|
|
@@ -249,6 +254,10 @@ export interface AgentSessionConfig {
|
|
|
249
254
|
onPayload?: SimpleStreamOptions["onPayload"];
|
|
250
255
|
/** Provider response hook used by the active session request path */
|
|
251
256
|
onResponse?: SimpleStreamOptions["onResponse"];
|
|
257
|
+
/** Raw SSE hook used by the active session request path */
|
|
258
|
+
onSseEvent?: SimpleStreamOptions["onSseEvent"];
|
|
259
|
+
/** Per-session raw SSE diagnostic buffer */
|
|
260
|
+
rawSseDebugBuffer?: RawSseDebugBuffer;
|
|
252
261
|
/** Current session message-to-LLM conversion pipeline */
|
|
253
262
|
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
254
263
|
/** System prompt builder that can consider tool availability. Returns ordered provider-facing blocks. */
|
|
@@ -280,6 +289,13 @@ export interface AgentSessionConfig {
|
|
|
280
289
|
agentId?: string;
|
|
281
290
|
/** Shared agent registry (for forwarding IRC observations to the main session UI). */
|
|
282
291
|
agentRegistry?: AgentRegistry;
|
|
292
|
+
/**
|
|
293
|
+
* Override the provider-facing session ID for all API requests from this session.
|
|
294
|
+
* When absent, `sessionManager.getSessionId()` is used. Needed when benchmark or
|
|
295
|
+
* SDK callers issue probes / prewarming with an explicit `--provider-session-id`
|
|
296
|
+
* so that credential sticky selection is consistent with the session's streaming calls.
|
|
297
|
+
*/
|
|
298
|
+
providerSessionId?: string;
|
|
283
299
|
}
|
|
284
300
|
|
|
285
301
|
/** Options for AgentSession.prompt() */
|
|
@@ -400,6 +416,56 @@ function todoClearKey(phaseName: string, taskContent: string): string {
|
|
|
400
416
|
return `${phaseName}\u0000${taskContent}`;
|
|
401
417
|
}
|
|
402
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Build the per-request `metadata` payload for the Anthropic provider, shaped
|
|
421
|
+
* like real Claude Code's `getAPIMetadata` output (`{ session_id, account_uuid,
|
|
422
|
+
* device_id }`) so the backend buckets requests under one session and attributes
|
|
423
|
+
* them to the authenticated OAuth account when available. Resolved at request
|
|
424
|
+
* time so token refreshes and login/logout transitions don't strand a stale
|
|
425
|
+
* account UUID in memory. `account_uuid` and `device_id` are omitted for
|
|
426
|
+
* non-Anthropic providers to avoid leaking the user's Claude identity to
|
|
427
|
+
* third-party APIs (including Anthropic-format-compatible proxies such as
|
|
428
|
+
* cloudflare-ai-gateway or gitlab-duo).
|
|
429
|
+
*
|
|
430
|
+
* `provider` is the target provider string (e.g. `"anthropic"`) and gates the
|
|
431
|
+
* `account_uuid` and `device_id` lookups — only `"anthropic"` requests carry them.
|
|
432
|
+
*
|
|
433
|
+
* `sessionId` is forwarded to the auth-storage session-sticky lookup so that
|
|
434
|
+
* multi-credential setups attribute to the same OAuth account used for the
|
|
435
|
+
* actual API request rather than always picking the first credential.
|
|
436
|
+
*
|
|
437
|
+
* `authStorage` is treated as optional so test fixtures that stub `modelRegistry`
|
|
438
|
+
* without a real storage layer still work; the resolver simply skips the lookup
|
|
439
|
+
* and emits `{ session_id }` alone, matching the no-OAuth-credential path.
|
|
440
|
+
*/
|
|
441
|
+
function buildSessionMetadata(
|
|
442
|
+
sessionId: string,
|
|
443
|
+
provider: string,
|
|
444
|
+
authStorage: AuthStorage | undefined,
|
|
445
|
+
): Record<string, unknown> {
|
|
446
|
+
const userId: Record<string, string> = { session_id: sessionId };
|
|
447
|
+
// Only look up account_uuid when the request is going to Anthropic. Injecting
|
|
448
|
+
// a Claude OAuth account_uuid into requests bound for other providers (including
|
|
449
|
+
// Anthropic-format-compatible proxies like cloudflare-ai-gateway or gitlab-duo)
|
|
450
|
+
// would leak the user's Anthropic identity to unrelated third-party APIs.
|
|
451
|
+
if (provider === "anthropic") {
|
|
452
|
+
const accountUuid = authStorage?.getOAuthAccountId("anthropic", sessionId);
|
|
453
|
+
if (typeof accountUuid === "string" && accountUuid.length > 0) {
|
|
454
|
+
userId.account_uuid = accountUuid;
|
|
455
|
+
// Derive device_id from account_uuid so the payload matches the real CC
|
|
456
|
+
// getAPIMetadata shape without hardware fingerprinting. A SHA-256 of a
|
|
457
|
+
// namespaced account UUID produces a stable 64-hex value that is
|
|
458
|
+
// indistinguishable from a randomly generated device ID on the wire, is
|
|
459
|
+
// deterministic per account (survives reinstalls), and is auditable: it
|
|
460
|
+
// is derived solely from the OAuth UUID the user already consented to
|
|
461
|
+
// share with Anthropic. Omitted when no OAuth credential is available
|
|
462
|
+
// (API-key callers) to avoid sending a hash of an empty string.
|
|
463
|
+
userId.device_id = crypto.createHash("sha256").update(`omp-device-id-v1:${accountUuid}`).digest("hex");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return { user_id: JSON.stringify(userId) };
|
|
467
|
+
}
|
|
468
|
+
|
|
403
469
|
const noOpUIContext: ExtensionUIContext = {
|
|
404
470
|
select: async (_title, _options, _dialogOptions) => undefined,
|
|
405
471
|
confirm: async (_title, _message, _dialogOptions) => false,
|
|
@@ -503,6 +569,7 @@ export class AgentSession {
|
|
|
503
569
|
// Agent identity + registry for IRC relay forwarding to the main session UI.
|
|
504
570
|
#agentId: string | undefined;
|
|
505
571
|
#agentRegistry: AgentRegistry | undefined;
|
|
572
|
+
#providerSessionId: string | undefined;
|
|
506
573
|
// Extension system
|
|
507
574
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
508
575
|
#turnIndex = 0;
|
|
@@ -525,6 +592,7 @@ export class AgentSession {
|
|
|
525
592
|
#transformContext: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
526
593
|
#onPayload: SimpleStreamOptions["onPayload"] | undefined;
|
|
527
594
|
#onResponse: SimpleStreamOptions["onResponse"] | undefined;
|
|
595
|
+
#onSseEvent: SimpleStreamOptions["onSseEvent"] | undefined;
|
|
528
596
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
529
597
|
#rebuildSystemPrompt:
|
|
530
598
|
| ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>)
|
|
@@ -576,6 +644,7 @@ export class AgentSession {
|
|
|
576
644
|
#promptGeneration = 0;
|
|
577
645
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
578
646
|
#hindsightSessionState: HindsightSessionState | undefined = undefined;
|
|
647
|
+
readonly rawSseDebugBuffer: RawSseDebugBuffer;
|
|
579
648
|
|
|
580
649
|
#startPowerAssertion(): void {
|
|
581
650
|
if (process.platform !== "darwin") {
|
|
@@ -622,7 +691,19 @@ export class AgentSession {
|
|
|
622
691
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
623
692
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
624
693
|
this.#onPayload = config.onPayload;
|
|
625
|
-
this
|
|
694
|
+
this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
|
|
695
|
+
const configuredOnResponse = config.onResponse;
|
|
696
|
+
this.#onResponse = async (response, model) => {
|
|
697
|
+
this.rawSseDebugBuffer.recordResponse(response, model);
|
|
698
|
+
await configuredOnResponse?.(response, model);
|
|
699
|
+
};
|
|
700
|
+
const configuredOnSseEvent = config.onSseEvent;
|
|
701
|
+
this.#onSseEvent = (event, model) => {
|
|
702
|
+
this.rawSseDebugBuffer.recordEvent(event, model);
|
|
703
|
+
configuredOnSseEvent?.(event, model);
|
|
704
|
+
};
|
|
705
|
+
this.agent.setProviderResponseInterceptor(this.#onResponse);
|
|
706
|
+
this.agent.setRawSseEventInterceptor(this.#onSseEvent);
|
|
626
707
|
this.#convertToLlm = config.convertToLlm ?? convertToLlm;
|
|
627
708
|
this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
|
|
628
709
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
@@ -652,6 +733,7 @@ export class AgentSession {
|
|
|
652
733
|
this.#obfuscator = config.obfuscator;
|
|
653
734
|
this.#agentId = config.agentId;
|
|
654
735
|
this.#agentRegistry = config.agentRegistry;
|
|
736
|
+
this.#providerSessionId = config.providerSessionId;
|
|
655
737
|
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
656
738
|
const event: AgentEvent = {
|
|
657
739
|
type: "message_update",
|
|
@@ -662,6 +744,7 @@ export class AgentSession {
|
|
|
662
744
|
this.#maybeAbortStreamingEdit(event);
|
|
663
745
|
});
|
|
664
746
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
747
|
+
this.#syncAgentSessionId();
|
|
665
748
|
this.#syncTodoPhasesFromBranch();
|
|
666
749
|
|
|
667
750
|
// Always subscribe to agent events for internal handling
|
|
@@ -1987,7 +2070,24 @@ export class AgentSession {
|
|
|
1987
2070
|
this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
|
|
1988
2071
|
}
|
|
1989
2072
|
|
|
1990
|
-
/**
|
|
2073
|
+
/**
|
|
2074
|
+
* Set agent.sessionId from the session manager and install a dynamic
|
|
2075
|
+
* metadata resolver so every API request carries `metadata.user_id` shaped
|
|
2076
|
+
* like real Claude Code's `getAPIMetadata` output: `{ session_id,
|
|
2077
|
+
* account_uuid }` (the latter only when an Anthropic OAuth credential with
|
|
2078
|
+
* a known account UUID is loaded). Resolving live keeps the value in sync
|
|
2079
|
+
* with auth-state changes (login/logout, token refresh that surfaces a new
|
|
2080
|
+
* account uuid) without needing to re-call `#syncAgentSessionId()` on every
|
|
2081
|
+
* such event.
|
|
2082
|
+
*/
|
|
2083
|
+
#syncAgentSessionId(sessionId?: string): void {
|
|
2084
|
+
const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
|
|
2085
|
+
this.agent.sessionId = sid;
|
|
2086
|
+
this.agent.setMetadataResolver((provider: string) =>
|
|
2087
|
+
buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
1991
2091
|
#rekeyHindsightMemoryForCurrentSessionId(): void {
|
|
1992
2092
|
if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
|
|
1993
2093
|
const sid = this.agent.sessionId;
|
|
@@ -2692,13 +2792,22 @@ export class AgentSession {
|
|
|
2692
2792
|
}
|
|
2693
2793
|
|
|
2694
2794
|
/** Apply session-level stream hooks to a direct side request. */
|
|
2695
|
-
prepareSimpleStreamOptions(options: SimpleStreamOptions): SimpleStreamOptions {
|
|
2795
|
+
prepareSimpleStreamOptions(options: SimpleStreamOptions, provider = "anthropic"): SimpleStreamOptions {
|
|
2696
2796
|
const sessionOnPayload = this.#onPayload;
|
|
2697
2797
|
const sessionOnResponse = this.#onResponse;
|
|
2698
|
-
|
|
2798
|
+
const sessionMetadata = this.agent.metadataForProvider(provider);
|
|
2799
|
+
const sessionOnSseEvent = this.#onSseEvent;
|
|
2800
|
+
if (!sessionOnPayload && !sessionOnResponse && !sessionMetadata && !sessionOnSseEvent) return options;
|
|
2699
2801
|
|
|
2700
2802
|
const preparedOptions: SimpleStreamOptions = { ...options };
|
|
2701
2803
|
|
|
2804
|
+
// Stamp session metadata (e.g. user_id={session_id}) onto direct-call requests so
|
|
2805
|
+
// they share the same session bucket as Agent.prompt-routed requests on Anthropic
|
|
2806
|
+
// OAuth. Caller-provided metadata wins so explicit overrides are respected.
|
|
2807
|
+
if (sessionMetadata && !options.metadata) {
|
|
2808
|
+
preparedOptions.metadata = sessionMetadata;
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2702
2811
|
if (sessionOnPayload) {
|
|
2703
2812
|
if (!options.onPayload) {
|
|
2704
2813
|
preparedOptions.onPayload = sessionOnPayload;
|
|
@@ -2725,6 +2834,18 @@ export class AgentSession {
|
|
|
2725
2834
|
}
|
|
2726
2835
|
}
|
|
2727
2836
|
|
|
2837
|
+
if (sessionOnSseEvent) {
|
|
2838
|
+
if (!options.onSseEvent) {
|
|
2839
|
+
preparedOptions.onSseEvent = sessionOnSseEvent;
|
|
2840
|
+
} else {
|
|
2841
|
+
const requestOnSseEvent = options.onSseEvent;
|
|
2842
|
+
preparedOptions.onSseEvent = (event, model) => {
|
|
2843
|
+
sessionOnSseEvent(event, model);
|
|
2844
|
+
requestOnSseEvent(event, model);
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2728
2849
|
return preparedOptions;
|
|
2729
2850
|
}
|
|
2730
2851
|
|
|
@@ -2750,7 +2871,7 @@ export class AgentSession {
|
|
|
2750
2871
|
|
|
2751
2872
|
/** Current session ID */
|
|
2752
2873
|
get sessionId(): string {
|
|
2753
|
-
return this.sessionManager.getSessionId();
|
|
2874
|
+
return this.#providerSessionId ?? this.sessionManager.getSessionId();
|
|
2754
2875
|
}
|
|
2755
2876
|
|
|
2756
2877
|
/** Current session display name, if set */
|
|
@@ -3810,7 +3931,7 @@ export class AgentSession {
|
|
|
3810
3931
|
}
|
|
3811
3932
|
await this.sessionManager.newSession(options);
|
|
3812
3933
|
this.setTodoPhases([]);
|
|
3813
|
-
this
|
|
3934
|
+
this.#syncAgentSessionId();
|
|
3814
3935
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
3815
3936
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
3816
3937
|
this.#steeringMessages = [];
|
|
@@ -3905,7 +4026,7 @@ export class AgentSession {
|
|
|
3905
4026
|
}
|
|
3906
4027
|
|
|
3907
4028
|
// Update agent session ID
|
|
3908
|
-
this
|
|
4029
|
+
this.#syncAgentSessionId();
|
|
3909
4030
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
3910
4031
|
|
|
3911
4032
|
// Emit session_switch event with reason "fork" to hooks
|
|
@@ -4286,14 +4407,7 @@ export class AgentSession {
|
|
|
4286
4407
|
}
|
|
4287
4408
|
|
|
4288
4409
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
4289
|
-
const compactionModel = this.model;
|
|
4290
|
-
const apiKey = await this.#modelRegistry.getApiKey(compactionModel, this.sessionId);
|
|
4291
|
-
if (!apiKey) {
|
|
4292
|
-
throw new Error(`No API key for ${compactionModel.provider}`);
|
|
4293
|
-
}
|
|
4294
|
-
|
|
4295
4410
|
const pathEntries = this.sessionManager.getBranch();
|
|
4296
|
-
|
|
4297
4411
|
const preparation = prepareCompaction(pathEntries, compactionSettings);
|
|
4298
4412
|
if (!preparation) {
|
|
4299
4413
|
// Check why we can't compact
|
|
@@ -4363,10 +4477,8 @@ export class AgentSession {
|
|
|
4363
4477
|
preserveData ??= hookCompaction.preserveData;
|
|
4364
4478
|
} else {
|
|
4365
4479
|
// Generate compaction result
|
|
4366
|
-
const result = await
|
|
4480
|
+
const result = await this.#compactWithFallbackModel(
|
|
4367
4481
|
preparation,
|
|
4368
|
-
compactionModel,
|
|
4369
|
-
apiKey,
|
|
4370
4482
|
customInstructions,
|
|
4371
4483
|
compactionAbortController.signal,
|
|
4372
4484
|
{
|
|
@@ -4616,7 +4728,7 @@ export class AgentSession {
|
|
|
4616
4728
|
this.#asyncJobManager?.cancelAll();
|
|
4617
4729
|
await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
|
|
4618
4730
|
this.agent.reset();
|
|
4619
|
-
this
|
|
4731
|
+
this.#syncAgentSessionId();
|
|
4620
4732
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
4621
4733
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
4622
4734
|
this.#steeringMessages = [];
|
|
@@ -5286,6 +5398,50 @@ export class AgentSession {
|
|
|
5286
5398
|
|
|
5287
5399
|
return candidates;
|
|
5288
5400
|
}
|
|
5401
|
+
#isCompactionAuthFailure(error: unknown): boolean {
|
|
5402
|
+
if (!(error instanceof Error)) return false;
|
|
5403
|
+
return /auth_unavailable|no auth available/i.test(error.message);
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5406
|
+
#buildCompactionAuthError(): Error {
|
|
5407
|
+
const currentModel = this.model;
|
|
5408
|
+
if (!currentModel) {
|
|
5409
|
+
return new Error(
|
|
5410
|
+
"Compaction requires a model with usable credentials, but no authenticated compaction model is available.",
|
|
5411
|
+
);
|
|
5412
|
+
}
|
|
5413
|
+
return new Error(
|
|
5414
|
+
`Compaction requires usable credentials for ${currentModel.provider}/${currentModel.id}. ` +
|
|
5415
|
+
`Configure ${currentModel.provider} credentials or assign an authenticated fallback role such as modelRoles.smol.`,
|
|
5416
|
+
);
|
|
5417
|
+
}
|
|
5418
|
+
|
|
5419
|
+
async #compactWithFallbackModel(
|
|
5420
|
+
preparation: CompactionPreparation,
|
|
5421
|
+
customInstructions: string | undefined,
|
|
5422
|
+
signal: AbortSignal,
|
|
5423
|
+
options?: SummaryOptions,
|
|
5424
|
+
): Promise<CompactionResult> {
|
|
5425
|
+
const candidates = this.#getCompactionModelCandidates(this.#modelRegistry.getAvailable());
|
|
5426
|
+
|
|
5427
|
+
for (const candidate of candidates) {
|
|
5428
|
+
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
5429
|
+
if (!apiKey) continue;
|
|
5430
|
+
|
|
5431
|
+
try {
|
|
5432
|
+
return await compact(preparation, candidate, apiKey, customInstructions, signal, {
|
|
5433
|
+
...options,
|
|
5434
|
+
metadata: this.agent.metadataForProvider(candidate.provider),
|
|
5435
|
+
});
|
|
5436
|
+
} catch (error) {
|
|
5437
|
+
if (!this.#isCompactionAuthFailure(error)) {
|
|
5438
|
+
throw error;
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
throw this.#buildCompactionAuthError();
|
|
5444
|
+
}
|
|
5289
5445
|
|
|
5290
5446
|
/**
|
|
5291
5447
|
* Internal: Run auto-compaction with events.
|
|
@@ -5487,6 +5643,7 @@ export class AgentSession {
|
|
|
5487
5643
|
promptOverride: hookPrompt,
|
|
5488
5644
|
extraContext: hookContext,
|
|
5489
5645
|
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
5646
|
+
metadata: this.agent.metadataForProvider(candidate.provider),
|
|
5490
5647
|
initiatorOverride: "agent",
|
|
5491
5648
|
});
|
|
5492
5649
|
break;
|
|
@@ -5496,6 +5653,10 @@ export class AgentSession {
|
|
|
5496
5653
|
}
|
|
5497
5654
|
|
|
5498
5655
|
const message = error instanceof Error ? error.message : String(error);
|
|
5656
|
+
if (this.#isCompactionAuthFailure(error)) {
|
|
5657
|
+
lastError = this.#buildCompactionAuthError();
|
|
5658
|
+
break;
|
|
5659
|
+
}
|
|
5499
5660
|
const retryAfterMs = this.#parseRetryAfterMsFromError(message);
|
|
5500
5661
|
const shouldRetry =
|
|
5501
5662
|
retrySettings.enabled &&
|
|
@@ -6606,15 +6767,18 @@ export class AgentSession {
|
|
|
6606
6767
|
systemPrompt: this.systemPrompt,
|
|
6607
6768
|
messages: llmMessages,
|
|
6608
6769
|
};
|
|
6609
|
-
const options = this.prepareSimpleStreamOptions(
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6770
|
+
const options = this.prepareSimpleStreamOptions(
|
|
6771
|
+
{
|
|
6772
|
+
apiKey,
|
|
6773
|
+
sessionId: this.sessionId,
|
|
6774
|
+
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
6775
|
+
hideThinkingSummary: this.agent.hideThinkingSummary,
|
|
6776
|
+
serviceTier: this.serviceTier,
|
|
6777
|
+
signal: args.signal,
|
|
6778
|
+
toolChoice: "none",
|
|
6779
|
+
},
|
|
6780
|
+
model.provider,
|
|
6781
|
+
);
|
|
6618
6782
|
|
|
6619
6783
|
let replyText = "";
|
|
6620
6784
|
let assistantMessage: AssistantMessage | undefined;
|
|
@@ -6791,7 +6955,7 @@ export class AgentSession {
|
|
|
6791
6955
|
|
|
6792
6956
|
try {
|
|
6793
6957
|
await this.sessionManager.setSessionFile(sessionPath);
|
|
6794
|
-
this
|
|
6958
|
+
this.#syncAgentSessionId();
|
|
6795
6959
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6796
6960
|
|
|
6797
6961
|
const sessionContext = this.buildDisplaySessionContext();
|
|
@@ -6869,7 +7033,7 @@ export class AgentSession {
|
|
|
6869
7033
|
return true;
|
|
6870
7034
|
} catch (error) {
|
|
6871
7035
|
this.sessionManager.restoreState(previousSessionState);
|
|
6872
|
-
this
|
|
7036
|
+
this.#syncAgentSessionId(previousSessionState.sessionId);
|
|
6873
7037
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6874
7038
|
let restoreMcpError: unknown;
|
|
6875
7039
|
try {
|
|
@@ -6961,7 +7125,7 @@ export class AgentSession {
|
|
|
6961
7125
|
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
6962
7126
|
}
|
|
6963
7127
|
this.#syncTodoPhasesFromBranch();
|
|
6964
|
-
this
|
|
7128
|
+
this.#syncAgentSessionId();
|
|
6965
7129
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6966
7130
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
6967
7131
|
|
|
@@ -7082,6 +7246,7 @@ export class AgentSession {
|
|
|
7082
7246
|
signal: this.#branchSummaryAbortController.signal,
|
|
7083
7247
|
customInstructions: options.customInstructions,
|
|
7084
7248
|
reserveTokens: branchSummarySettings.reserveTokens,
|
|
7249
|
+
metadata: this.agent.metadataForProvider(model.provider),
|
|
7085
7250
|
});
|
|
7086
7251
|
this.#branchSummaryAbortController = undefined;
|
|
7087
7252
|
if (result.aborted) {
|
|
@@ -75,6 +75,8 @@ export interface GenerateBranchSummaryOptions {
|
|
|
75
75
|
customInstructions?: string;
|
|
76
76
|
/** Tokens reserved for prompt + LLM response (default 16384) */
|
|
77
77
|
reserveTokens?: number;
|
|
78
|
+
/** Optional metadata forwarded to the underlying API request (e.g. user_id for session attribution). */
|
|
79
|
+
metadata?: Record<string, unknown>;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
// ============================================================================
|
|
@@ -258,7 +260,7 @@ export async function generateBranchSummary(
|
|
|
258
260
|
entries: SessionEntry[],
|
|
259
261
|
options: GenerateBranchSummaryOptions,
|
|
260
262
|
): Promise<BranchSummaryResult> {
|
|
261
|
-
const { model, apiKey, signal, customInstructions, reserveTokens = 16384 } = options;
|
|
263
|
+
const { model, apiKey, signal, customInstructions, reserveTokens = 16384, metadata } = options;
|
|
262
264
|
|
|
263
265
|
// Token budget = context window minus reserved space for prompt + response
|
|
264
266
|
const contextWindow = model.contextWindow || 128000;
|
|
@@ -291,7 +293,7 @@ export async function generateBranchSummary(
|
|
|
291
293
|
const response = await completeSimple(
|
|
292
294
|
model,
|
|
293
295
|
{ systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
|
|
294
|
-
{ apiKey, signal, maxTokens: 2048 },
|
|
296
|
+
{ apiKey, signal, maxTokens: 2048, metadata },
|
|
295
297
|
);
|
|
296
298
|
|
|
297
299
|
// Check if aborted or errored
|