@hienlh/ppm 0.20.5 → 0.20.6
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 +10 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{accounts-settings-section-Cmmozwer.js → accounts-settings-section-B-F7U-gq.js} +1 -1
- package/dist/web/assets/{ai-resource-editor-BlrWV3xU.js → ai-resource-editor-qRHwpktf.js} +1 -1
- package/dist/web/assets/{ai-settings-section-dDsgddSt.js → ai-settings-section-BePrVceb.js} +1 -1
- package/dist/web/assets/ai-settings-section-DSlsTrpf.js +1 -0
- package/dist/web/assets/{appearance-settings-section-DRxziHFt.js → appearance-settings-section-BMlkj9U2.js} +1 -1
- package/dist/web/assets/chat-tab-DmqV3MlC.js +15 -0
- package/dist/web/assets/{code-editor-DYkmDRRF.js → code-editor-C4CriX9c.js} +2 -2
- package/dist/web/assets/{conflict-editor-D9_sr59D.js → conflict-editor-LfQoRcK-.js} +1 -1
- package/dist/web/assets/{csv-preview-BMC8jnpF.js → csv-preview-BEYKn68B.js} +1 -1
- package/dist/web/assets/{database-viewer-Box3z5-Q.js → database-viewer-BL9R-ciJ.js} +1 -1
- package/dist/web/assets/{diff-viewer-B4PlZ1Dt.js → diff-viewer-D91SI_zb.js} +1 -1
- package/dist/web/assets/{explorer-body-SvWDS84f.js → explorer-body-ColVUjgk.js} +1 -1
- package/dist/web/assets/{explorer-window-content-DBHBTA7B.js → explorer-window-content-VSMJfZC2.js} +1 -1
- package/dist/web/assets/{extension-manager-section-BSt5I_H8.js → extension-manager-section-B-BRHGu1.js} +1 -1
- package/dist/web/assets/{extension-webview-DBGMUU97.js → extension-webview-DufzanN9.js} +1 -1
- package/dist/web/assets/{files-settings-section-0dwYLZou.js → files-settings-section-Do5YbTUK.js} +1 -1
- package/dist/web/assets/{general-settings-section-BgLy_6yj.js → general-settings-section-B-n_PYzs.js} +1 -1
- package/dist/web/assets/{git-log-panel-BkgmcWk1.js → git-log-panel-BQzOHgxG.js} +1 -1
- package/dist/web/assets/{glide-data-grid-CZVu-zCw.js → glide-data-grid-BUS44oLM.js} +1 -1
- package/dist/web/assets/{group-chat-tab-Cy9zIynj.js → group-chat-tab-CkHXqorZ.js} +1 -1
- package/dist/web/assets/{index-CuYtJ5xF.css → index-B9Wbjl0o.css} +1 -1
- package/dist/web/assets/{index-DYGLdCpe.js → index-Cx1meAwb.js} +4 -4
- package/dist/web/assets/{jira-watcher-section-DRQx632s.js → jira-watcher-section-BQ4hVqWo.js} +1 -1
- package/dist/web/assets/{keybindings-store-C2HZ1mT4.js → keybindings-store-yYx88hvw.js} +1 -1
- package/dist/web/assets/{keyboard-shortcuts-section-WiZSZ-Wr.js → keyboard-shortcuts-section-BZUYdXGx.js} +1 -1
- package/dist/web/assets/{markdown-renderer-BFYBVcZP.js → markdown-renderer-BA3C2L-a.js} +1 -1
- package/dist/web/assets/{markdown-renderer-CklcWnd6.js → markdown-renderer-CMFkajd7.js} +1 -1
- package/dist/web/assets/{mobile-explorer-sheet-D1OXw7op.js → mobile-explorer-sheet-gvO7a80O.js} +1 -1
- package/dist/web/assets/{notification-store-DyKshGRs.js → notification-store-CCh6a8RD.js} +1 -1
- package/dist/web/assets/{notifications-settings-section-BEQEjvan.js → notifications-settings-section-JQ3l-Qp-.js} +1 -1
- package/dist/web/assets/{pdf-preview-DmQazeeV.js → pdf-preview-B-JAU8JV.js} +1 -1
- package/dist/web/assets/{postgres-viewer-Cd_wXjkT.js → postgres-viewer-B--Y59n5.js} +1 -1
- package/dist/web/assets/{ppmbot-settings-section-PzCLPm8a.js → ppmbot-settings-section-D1SCOc0_.js} +1 -1
- package/dist/web/assets/{process-table-C2TdkJck.js → process-table-D9z7cEmb.js} +1 -1
- package/dist/web/assets/{proxy-settings-section-DsZ6KcHD.js → proxy-settings-section-y7VwH0Hf.js} +1 -1
- package/dist/web/assets/{query-audit-section-2eX9-6mO.js → query-audit-section-Ca_gRQ_s.js} +1 -1
- package/dist/web/assets/{remote-desktop-mobile-sheet-CMXSUcAN.js → remote-desktop-mobile-sheet-CdO2aMXl.js} +2 -2
- package/dist/web/assets/{remote-desktop-mobile-view-Bw6p5ikf.js → remote-desktop-mobile-view-CGQ8vSf1.js} +1 -1
- package/dist/web/assets/{remote-desktop-readiness-gate-W0lAhHq9.js → remote-desktop-readiness-gate-BmC3b05e.js} +1 -1
- package/dist/web/assets/{remote-desktop-window-content-P888YEqm.js → remote-desktop-window-content-Cdaoltdz.js} +1 -1
- package/dist/web/assets/{schedules-settings-section-C8YDAxlo.js → schedules-settings-section--0s10CfA.js} +1 -1
- package/dist/web/assets/{settings-body-B5v3_Tnb.js → settings-body-DEMqFm5z.js} +2 -2
- package/dist/web/assets/{settings-tab-CauShulf.js → settings-tab-Dh1FpBny.js} +1 -1
- package/dist/web/assets/{settings-window-content-BIosR7RY.js → settings-window-content-3KrQ6imZ.js} +1 -1
- package/dist/web/assets/{sql-query-editor-DMihDPnw.js → sql-query-editor-Clo90z8-.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-jCIGt2GP.js → sqlite-viewer-C08QQR_L.js} +1 -1
- package/dist/web/assets/{system-monitor-body-IUamfS5w.js → system-monitor-body-30-TFH3a.js} +2 -2
- package/dist/web/assets/{system-monitor-tab-DaWeif4k.js → system-monitor-tab-ChIXfOWg.js} +1 -1
- package/dist/web/assets/{system-monitor-window-content-Bq3BS7Mg.js → system-monitor-window-content-C1Cd_pT7.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-vfYLLIKU.js → tab-host-window-content-C6eG_Rf1.js} +1 -1
- package/dist/web/assets/{team-member-sheet-Be7OSGMT.js → team-member-sheet-BhatQbL1.js} +2 -2
- package/dist/web/assets/{team-member-window-content-D5cYZzBq.js → team-member-window-content-BN3luct-.js} +1 -1
- package/dist/web/assets/{terminal-tab-C89DQllY.js → terminal-tab-afylLiLY.js} +1 -1
- package/dist/web/assets/{tool-cards-CWotDH_2.js → tool-cards-Ct1QdT8c.js} +2 -2
- package/dist/web/assets/{use-accounts-data-lmmmG4ep.js → use-accounts-data-C0bvcMhi.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-0ziS4xlp.js → use-monaco-theme-cgPbRDUm.js} +1 -1
- package/dist/web/assets/{use-remote-desktop-display-choice-qBZ8ej7J.js → use-remote-desktop-display-choice-DSqF1Rgx.js} +1 -1
- package/dist/web/assets/use-websocket-BQssVSIR.js +1 -0
- package/dist/web/assets/{video-preview-B8xALVkh.js → video-preview-BTzstVhM.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/docs/architecture/ai-chat-and-providers.md +88 -0
- package/package.json +1 -1
- package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.exe +0 -0
- package/schemas/ppm-config.schema.json +18 -1
- package/src/providers/claude-agent-sdk.ts +21 -13
- package/src/providers/cli-provider-base.ts +3 -1
- package/src/providers/codex-app-server/codex-history.ts +5 -3
- package/src/providers/codex-app-server/codex-provider.ts +16 -2
- package/src/providers/codex-app-server/codex-rollout-header.ts +2 -1
- package/src/providers/cursor-cli/cursor-history.ts +4 -2
- package/src/server/routes/settings.ts +15 -1
- package/src/server/ws/chat.ts +2 -2
- package/src/services/chat.service.ts +95 -2
- package/src/services/codex-native-memory.ts +66 -0
- package/src/services/edge-forwarder.ts +191 -191
- package/src/services/jsonl-transcript-parser.ts +2 -1
- package/src/services/provider-shared-context.ts +152 -0
- package/src/services/windows-process-tree.ts +351 -351
- package/src/shared/provider-context.ts +13 -0
- package/src/types/chat.ts +6 -0
- package/src/types/config.ts +29 -0
- package/src/web/components/chat/chat-history-bar.tsx +6 -3
- package/src/web/components/chat/chat-tab.tsx +7 -0
- package/src/web/components/settings/ai-settings-section.tsx +34 -5
- package/src/web/components/settings/codex-context-settings.tsx +100 -0
- package/src/web/hooks/use-chat.ts +71 -14
- package/src/web/hooks/use-websocket.ts +14 -7
- package/src/web/lib/api-settings.ts +3 -0
- package/src/web/lib/ws-client.ts +55 -4
- package/.release-bin/zip +0 -15
- package/.release-node-shim.sh +0 -11
- package/dist/web/assets/ai-settings-section-PaEh_o0w.js +0 -1
- package/dist/web/assets/chat-tab-MNuc18Os.js +0 -15
- package/dist/web/assets/use-websocket-DAvfbjxh.js +0 -1
|
@@ -2,7 +2,87 @@
|
|
|
2
2
|
|
|
3
3
|
> Part of the [PPM system architecture](../system-architecture.md).
|
|
4
4
|
|
|
5
|
+
## Codex context settings
|
|
6
|
+
|
|
7
|
+
**AI Settings → Codex** exposes **Context window (tokens)** and **Auto-compact
|
|
8
|
+
threshold (tokens)**. These map to Codex's documented `model_context_window` and
|
|
9
|
+
`model_auto_compact_token_limit` keys under `ai.providers.codex`.
|
|
10
|
+
|
|
11
|
+
Both controls offer presets, Codex default and Custom entry. Presets are numeric
|
|
12
|
+
shortcuts, not advertised model capabilities. Existing non-preset values appear
|
|
13
|
+
as Custom. Both values are optional positive safe integers. The compact threshold cannot
|
|
14
|
+
exceed an explicitly configured window. The form saves the pair together;
|
|
15
|
+
`PUT /api/settings/ai` also validates merged partial updates before persisting.
|
|
16
|
+
Clearing a field sends `null`, which removes the PPM override. An omitted field
|
|
17
|
+
in a partial update preserves the existing setting.
|
|
18
|
+
|
|
19
|
+
PPM passes configured values in the `config` map of app-server `thread/start`
|
|
20
|
+
and `thread/resume`, including account rotation. It does not edit account
|
|
21
|
+
`config.toml` files. Unset values inherit native Codex configuration and model
|
|
22
|
+
defaults. Changes apply on the next provider connection/resumption; an already
|
|
23
|
+
running session keeps its current limits. Restart PPM and resume an existing
|
|
24
|
+
session to apply changed limits to it.
|
|
25
|
+
|
|
26
|
+
Increasing these values does not increase the model/account's supported limit.
|
|
27
|
+
Codex can reserve part of the configured window, so runtime token usage may
|
|
28
|
+
report a smaller effective window. Local compacted rollouts inspected on
|
|
29
|
+
2026-09-15 reported 258,400 tokens (272,000 × 95%); this is runtime evidence for
|
|
30
|
+
this installation, not a hardcoded application default.
|
|
31
|
+
|
|
32
|
+
References: [Codex configuration](https://learn.chatgpt.com/docs/config-file/config-reference)
|
|
33
|
+
and [App Server](https://learn.chatgpt.com/docs/app-server).
|
|
34
|
+
|
|
5
35
|
## Provider Layer (AI Adapters)
|
|
36
|
+
|
|
37
|
+
### Shared instructions and memory
|
|
38
|
+
|
|
39
|
+
**Settings → AI → Share rules and memory between providers** controls
|
|
40
|
+
`ai.share_provider_context`. It defaults to `true`, including existing configs.
|
|
41
|
+
PPM checks sources for each project-chat message and sends a snapshot only when
|
|
42
|
+
it differs from the last delivered snapshot for that provider/session. Live
|
|
43
|
+
follow-ups use the same check. Compaction, failed delivery, explicit resume,
|
|
44
|
+
server restart, or eviction from the bounded 512-session cache causes a fresh
|
|
45
|
+
snapshot on the next message. Slash commands retain their native parser behavior.
|
|
46
|
+
Disabling stops future injections, but does not
|
|
47
|
+
remove context already present in an existing conversation. Start a new session
|
|
48
|
+
for a clean context. The isolated HTTP API proxy is outside this project-chat flow.
|
|
49
|
+
|
|
50
|
+
Sources include project `AGENTS.md`, `CLAUDE.md`, `CLAUDE.local.md`, provider
|
|
51
|
+
`.{providerId}/rules/` and `.{providerId}/memory/` directories,
|
|
52
|
+
user Claude rules, Codex account `AGENTS.md`, and Claude auto-memory for the exact
|
|
53
|
+
project path. PPM does not import account credentials, runtime settings/hooks,
|
|
54
|
+
conversation transcripts, or another project's auto-memory.
|
|
55
|
+
|
|
56
|
+
PPM reads existing native memory directly, including knowledge created before
|
|
57
|
+
PPM was installed. It does not create or maintain an intermediate memory store.
|
|
58
|
+
Claude's native `MEMORY.md` index is prioritized and its topic files remain
|
|
59
|
+
readable at their original paths. `CLAUDE_CONFIG_DIR` is respected. Codex native
|
|
60
|
+
memory is read from its memory database with exact project attribution from its
|
|
61
|
+
thread database; databases are opened read-only. New memory stays in the native
|
|
62
|
+
provider's storage. Conversation transcripts are not treated as memory.
|
|
63
|
+
|
|
64
|
+
Snapshots omit rules already loaded by the receiving provider (Codex AGENTS and
|
|
65
|
+
rules, Claude instructions/rules/auto-memory, Cursor rules). Codex's native memory
|
|
66
|
+
is supplied only to other providers. Unknown providers retain conventional sources.
|
|
67
|
+
Memory directories contribute their index and a directory reference, with topic
|
|
68
|
+
files read on demand. A compact revision tracks file metadata changes, including
|
|
69
|
+
omitted topics, within the bounded scan.
|
|
70
|
+
|
|
71
|
+
Snapshots are bounded to 48 content files, 4 KB excerpts per file and 12,000 content
|
|
72
|
+
characters, with limited directory traversal and a 1,500-character directory list.
|
|
73
|
+
Large files retain their original paths and a truncation notice so agents can
|
|
74
|
+
read the remainder on demand. Unreadable files are skipped. Rule scopes/frontmatter
|
|
75
|
+
remain part of the supplied instructions.
|
|
76
|
+
|
|
77
|
+
New registered providers automatically receive the common context and contribute
|
|
78
|
+
conventional `.providerId/rules` and `.providerId/memory` directories. For custom
|
|
79
|
+
paths, implement `AIProvider.getSharedContextSources(projectPath)`. Implement
|
|
80
|
+
`supportsSharedContext = true` to consume `SendMessageOpts.sharedContext` separately
|
|
81
|
+
from raw user text; `CliProvider` already does this. Other implementations receive
|
|
82
|
+
a prefixed prompt automatically. Keep titles based on raw user text and use
|
|
83
|
+
`stripSharedContext` when importing native transcripts. Claude, Codex and Cursor
|
|
84
|
+
already implement this separation.
|
|
85
|
+
|
|
6
86
|
**Component:** Provider interface + implementations
|
|
7
87
|
|
|
8
88
|
**Responsibilities:**
|
|
@@ -263,6 +343,14 @@ if (entry.phase !== "idle" && entry.abort) {
|
|
|
263
343
|
|
|
264
344
|
### WebSocket Reconnection Sync
|
|
265
345
|
|
|
346
|
+
Chat sockets receive a server heartbeat every 15 seconds. The client reconnects
|
|
347
|
+
after 45 seconds without any incoming frame, including sockets that still report
|
|
348
|
+
OPEN, and checks for a stale connection when the tab becomes visible. Replay
|
|
349
|
+
finishes before queued live frames are applied. If a turn becomes idle without a
|
|
350
|
+
usable finalized answer, the client reloads history; healthy completions retain
|
|
351
|
+
their richer live metadata. Recovery responses are discarded after new activity
|
|
352
|
+
or a session switch, and older initial loads cannot overwrite applied recovery.
|
|
353
|
+
|
|
266
354
|
```
|
|
267
355
|
FE WebSocket closes (network issue, tab closes)
|
|
268
356
|
↓
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
"type": "object",
|
|
37
37
|
"properties": {
|
|
38
38
|
"default_provider": { "type": "string", "default": "claude" },
|
|
39
|
+
"share_provider_context": {
|
|
40
|
+
"type": "boolean",
|
|
41
|
+
"default": true,
|
|
42
|
+
"description": "Share project rules and memory between AI providers"
|
|
43
|
+
},
|
|
39
44
|
"providers": {
|
|
40
45
|
"type": "object",
|
|
41
46
|
"additionalProperties": {
|
|
@@ -51,9 +56,21 @@
|
|
|
51
56
|
"properties": {
|
|
52
57
|
"type": {
|
|
53
58
|
"type": "string",
|
|
54
|
-
"enum": ["agent-sdk", "mock"]
|
|
59
|
+
"enum": ["agent-sdk", "cli", "mock"]
|
|
55
60
|
},
|
|
56
61
|
"api_key_env": { "type": "string" },
|
|
62
|
+
"model_context_window": {
|
|
63
|
+
"type": ["integer", "null"],
|
|
64
|
+
"minimum": 1,
|
|
65
|
+
"maximum": 9007199254740991,
|
|
66
|
+
"description": "Codex context window in tokens; null or omitted inherits Codex configuration"
|
|
67
|
+
},
|
|
68
|
+
"model_auto_compact_token_limit": {
|
|
69
|
+
"type": ["integer", "null"],
|
|
70
|
+
"minimum": 1,
|
|
71
|
+
"maximum": 9007199254740991,
|
|
72
|
+
"description": "Codex automatic compaction threshold in tokens; cannot exceed an explicit context window; null or omitted inherits Codex configuration"
|
|
73
|
+
},
|
|
57
74
|
"model": {
|
|
58
75
|
"type": "string",
|
|
59
76
|
"description": "Model ID (e.g. claude-opus-5, claude-fable-5, claude-sonnet-5)",
|
|
@@ -18,10 +18,11 @@ import type {
|
|
|
18
18
|
ModelOption,
|
|
19
19
|
} from "./provider.interface.ts";
|
|
20
20
|
import { configService } from "../services/config.service.ts";
|
|
21
|
+
import { withSharedContext, stripSharedContext } from "../shared/provider-context.ts";
|
|
21
22
|
import { mcpConfigService } from "../services/mcp-config.service.ts";
|
|
22
23
|
import { listInheritedClaudeMcpServers } from "../services/claude-code-mcp.service.ts";
|
|
23
24
|
import { updateFromSdkEvent } from "../services/claude-usage.service.ts";
|
|
24
|
-
import { getSessionProjectPath, setSessionMetadata, getSessionTitles, insertTurnUsage, getSessionAccount } from "../services/db.service.ts";
|
|
25
|
+
import { getSessionProjectPath, setSessionMetadata, getSessionTitle, setSessionTitle, getSessionTitles, insertTurnUsage, getSessionAccount } from "../services/db.service.ts";
|
|
25
26
|
import { SUBSCRIPTION_PROMPT_CACHE_TTL_MS, API_KEY_PROMPT_CACHE_TTL_MS } from "../services/subprocess-retention.ts";
|
|
26
27
|
import { buildTurnUsage, formatTurnUsageLog } from "../shared/turn-usage.ts";
|
|
27
28
|
import { accountSelector } from "../services/account-selector.service.ts";
|
|
@@ -213,6 +214,7 @@ interface PendingApproval {
|
|
|
213
214
|
* Uses canUseTool callback for tool approvals and AskUserQuestion.
|
|
214
215
|
*/
|
|
215
216
|
export class ClaudeAgentSdkProvider implements AIProvider {
|
|
217
|
+
readonly supportsSharedContext = true;
|
|
216
218
|
id = "claude";
|
|
217
219
|
name = "Claude";
|
|
218
220
|
|
|
@@ -571,7 +573,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
571
573
|
const sessions: SessionInfo[] = sdkSessions.map((s) => ({
|
|
572
574
|
id: s.sessionId,
|
|
573
575
|
providerId: this.id,
|
|
574
|
-
title: dbTitles[s.sessionId] ?? s.customTitle ?? s.summary ?? s.firstPrompt ?? "Chat",
|
|
576
|
+
title: dbTitles[s.sessionId] ?? s.customTitle ?? s.summary ?? (stripSharedContext(s.firstPrompt ?? "") || "Chat"),
|
|
575
577
|
createdAt: new Date(s.lastModified).toISOString(),
|
|
576
578
|
updatedAt: new Date(s.lastModified).toISOString(),
|
|
577
579
|
}));
|
|
@@ -619,7 +621,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
619
621
|
return {
|
|
620
622
|
id: info.sessionId,
|
|
621
623
|
providerId: this.id,
|
|
622
|
-
title: dbTitles[info.sessionId] ?? info.customTitle ?? info.summary ?? info.firstPrompt ?? "Chat",
|
|
624
|
+
title: dbTitles[info.sessionId] ?? info.customTitle ?? info.summary ?? (stripSharedContext(info.firstPrompt ?? "") || "Chat"),
|
|
623
625
|
createdAt: new Date(info.lastModified).toISOString(),
|
|
624
626
|
updatedAt: new Date(info.lastModified).toISOString(),
|
|
625
627
|
};
|
|
@@ -725,13 +727,14 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
725
727
|
* Push a follow-up message into an existing streaming session's generator.
|
|
726
728
|
* Called by WS handler for follow-up messages (Phase 2).
|
|
727
729
|
*/
|
|
728
|
-
pushMessage(sessionId: string, content: string, opts?:
|
|
730
|
+
pushMessage(sessionId: string, content: string, opts?: import("./provider.interface.ts").SendMessageOpts): void {
|
|
729
731
|
const ss = this.streamingSessions.get(sessionId);
|
|
730
732
|
if (!ss) {
|
|
731
733
|
console.warn(`[sdk] pushMessage: no streaming session for ${sessionId}`);
|
|
732
734
|
return;
|
|
733
735
|
}
|
|
734
|
-
const
|
|
736
|
+
const modelInput = withSharedContext(content, opts?.sharedContext);
|
|
737
|
+
const msgContent = buildMessageParam(modelInput, opts?.images);
|
|
735
738
|
ss.controller.push({
|
|
736
739
|
type: 'user',
|
|
737
740
|
message: msgContent,
|
|
@@ -740,7 +743,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
740
743
|
priority: opts?.priority ?? 'next',
|
|
741
744
|
});
|
|
742
745
|
// Track latest message for retry paths (fixes stale firstMsg bug)
|
|
743
|
-
ss.lastUserContent =
|
|
746
|
+
ss.lastUserContent = modelInput;
|
|
744
747
|
ss.lastUserImages = opts?.images;
|
|
745
748
|
console.log(`[sdk] pushMessage: session=${sessionId} priority=${opts?.priority ?? 'next'}`);
|
|
746
749
|
}
|
|
@@ -794,6 +797,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
794
797
|
message: string,
|
|
795
798
|
opts?: import("./provider.interface.ts").SendMessageOpts & { forkSession?: boolean; priority?: 'now' | 'next' | 'later'; images?: Array<{ data: string; mediaType: string }> },
|
|
796
799
|
): AsyncIterable<ChatEvent> {
|
|
800
|
+
const modelInput = withSharedContext(message, opts?.sharedContext);
|
|
797
801
|
// SDK requires valid UUID session IDs. Short/random IDs can leak from
|
|
798
802
|
// tab derivation or URL parsing — migrate to a real UUID early.
|
|
799
803
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -826,7 +830,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
826
830
|
// Follow-up: push into existing streaming session, yield nothing
|
|
827
831
|
const existingStream = this.streamingSessions.get(sessionId);
|
|
828
832
|
if (existingStream) {
|
|
829
|
-
const msgContent = buildMessageParam(
|
|
833
|
+
const msgContent = buildMessageParam(modelInput, opts?.images);
|
|
830
834
|
existingStream.controller.push({
|
|
831
835
|
type: 'user',
|
|
832
836
|
message: msgContent,
|
|
@@ -835,7 +839,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
835
839
|
priority: opts?.priority ?? 'next',
|
|
836
840
|
});
|
|
837
841
|
// Track latest message for retry paths (fixes stale firstMsg bug)
|
|
838
|
-
existingStream.lastUserContent =
|
|
842
|
+
existingStream.lastUserContent = modelInput;
|
|
839
843
|
existingStream.lastUserImages = opts?.images;
|
|
840
844
|
console.log(`[sdk] sendMessage follow-up: session=${sessionId} pushed to generator`);
|
|
841
845
|
return; // Events flow through first-message's consumer loop
|
|
@@ -853,6 +857,11 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
853
857
|
const count = this.messageCount.get(sessionId) ?? 0;
|
|
854
858
|
const isFirstMessage = count === 0;
|
|
855
859
|
this.messageCount.set(sessionId, count + 1);
|
|
860
|
+
// Native firstPrompt may truncate before the shared-context block ends.
|
|
861
|
+
// Keep the opening user title available after restart without replacing a rename.
|
|
862
|
+
if (isFirstMessage && opts?.sharedContext && getSessionTitle(sessionId) === null) {
|
|
863
|
+
setSessionTitle(sessionId, meta.title);
|
|
864
|
+
}
|
|
856
865
|
|
|
857
866
|
// Check if this session should fork from another
|
|
858
867
|
const forkSourceId = this.forkSources.get(sessionId);
|
|
@@ -1145,7 +1154,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1145
1154
|
// model a bare path instead — the round trip the caller passed them in to avoid.
|
|
1146
1155
|
const firstMsg = {
|
|
1147
1156
|
type: 'user' as const,
|
|
1148
|
-
message: buildMessageParam(
|
|
1157
|
+
message: buildMessageParam(modelInput, opts?.images),
|
|
1149
1158
|
parent_tool_use_id: null,
|
|
1150
1159
|
session_id: sessionId,
|
|
1151
1160
|
};
|
|
@@ -1165,7 +1174,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1165
1174
|
// Also returns the raw content/images for re-populating the new streaming session.
|
|
1166
1175
|
const buildRetryMsg = () => {
|
|
1167
1176
|
const ss = this.streamingSessions.get(sessionId);
|
|
1168
|
-
const content = ss?.lastUserContent ??
|
|
1177
|
+
const content = ss?.lastUserContent ?? modelInput;
|
|
1169
1178
|
const images = ss?.lastUserImages;
|
|
1170
1179
|
const retryContent = turnProgressed ? "Continue from where you left off." : content;
|
|
1171
1180
|
return {
|
|
@@ -1184,7 +1193,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1184
1193
|
// On crash retry, use buildRetryMsg to get the latest user message (not the stale firstMsg)
|
|
1185
1194
|
const initRetry = crashRetryCount > 0 ? buildRetryMsg() : null;
|
|
1186
1195
|
initialCtrl.push(initRetry?.msg ?? firstMsg);
|
|
1187
|
-
const initContent = initRetry?.lastUserContent ??
|
|
1196
|
+
const initContent = initRetry?.lastUserContent ?? modelInput;
|
|
1188
1197
|
const initImages = initRetry?.lastUserImages ?? opts?.images;
|
|
1189
1198
|
|
|
1190
1199
|
const initialQuery = query({
|
|
@@ -2219,7 +2228,7 @@ function findMissingSessions(
|
|
|
2219
2228
|
if (head[end] === '"') break;
|
|
2220
2229
|
end++;
|
|
2221
2230
|
}
|
|
2222
|
-
const raw = head.slice(start, end).replace(/\\n/g, "
|
|
2231
|
+
const raw = stripSharedContext(head.slice(start, end).replace(/\\n/g, "\n").replace(/\\"/g, '"').trim()).replace(/\n/g, " ");
|
|
2223
2232
|
if (raw.length > 0) {
|
|
2224
2233
|
title = raw.length > 120 ? raw.slice(0, 120) + "…" : raw;
|
|
2225
2234
|
}
|
|
@@ -2236,4 +2245,3 @@ function findMissingSessions(
|
|
|
2236
2245
|
}
|
|
2237
2246
|
return results;
|
|
2238
2247
|
}
|
|
2239
|
-
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
10
10
|
import { parseNdjsonLines } from "../utils/ndjson-line-parser.ts";
|
|
11
11
|
import { configService } from "../services/config.service.ts";
|
|
12
|
+
import { withSharedContext } from "../shared/provider-context.ts";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Abstract base class for CLI-spawning AI providers.
|
|
@@ -16,6 +17,7 @@ import { configService } from "../services/config.service.ts";
|
|
|
16
17
|
* Subclasses only implement event mapping + arg building.
|
|
17
18
|
*/
|
|
18
19
|
export abstract class CliProvider implements AIProvider {
|
|
20
|
+
readonly supportsSharedContext = true;
|
|
19
21
|
abstract readonly id: string;
|
|
20
22
|
abstract readonly name: string;
|
|
21
23
|
abstract readonly cliCommand: string;
|
|
@@ -118,7 +120,7 @@ export abstract class CliProvider implements AIProvider {
|
|
|
118
120
|
const config = this.getProviderConfig();
|
|
119
121
|
const args = this.buildArgs({
|
|
120
122
|
sessionId: isResume ? sessionId : undefined,
|
|
121
|
-
message,
|
|
123
|
+
message: withSharedContext(message, opts?.sharedContext),
|
|
122
124
|
model: config?.model,
|
|
123
125
|
permissionMode: opts?.permissionMode || config?.permission_mode,
|
|
124
126
|
isResume,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChatMessage, ChatEvent, SessionInfo } from "../provider.interface.ts";
|
|
2
|
+
import { stripSharedContext } from "../../shared/provider-context.ts";
|
|
2
3
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
4
|
import { join, resolve } from "node:path";
|
|
4
5
|
import { homedir } from "node:os";
|
|
@@ -135,7 +136,7 @@ export function parseRolloutJsonl(
|
|
|
135
136
|
if (rec.type === "event_msg") {
|
|
136
137
|
if (p.type === "user_message" && typeof p.message === "string") {
|
|
137
138
|
if (pendingEvents.length) flushAssistant("", ts); // tools with no final text
|
|
138
|
-
messages.push({ id: `rollout-${i++}`, role: "user", content: p.message, timestamp: ts });
|
|
139
|
+
messages.push({ id: `rollout-${i++}`, role: "user", content: stripSharedContext(p.message), timestamp: ts });
|
|
139
140
|
} else if (p.type === "agent_message" && typeof p.message === "string") {
|
|
140
141
|
flushAssistant(p.message, ts);
|
|
141
142
|
} else if (p.type === "item_completed") {
|
|
@@ -145,7 +146,7 @@ export function parseRolloutJsonl(
|
|
|
145
146
|
const mapped = mapRolloutItem(p.item);
|
|
146
147
|
if (mapped.kind === "user") {
|
|
147
148
|
if (pendingEvents.length) flushAssistant("", ts);
|
|
148
|
-
messages.push({ id: `rollout-${i++}`, role: "user", content: mapped.text, timestamp: ts });
|
|
149
|
+
messages.push({ id: `rollout-${i++}`, role: "user", content: stripSharedContext(mapped.text), timestamp: ts });
|
|
149
150
|
} else if (mapped.kind === "assistant") {
|
|
150
151
|
flushAssistant(mapped.text, ts);
|
|
151
152
|
} else if (mapped.kind === "events") {
|
|
@@ -196,7 +197,8 @@ export function parseRolloutJsonl(
|
|
|
196
197
|
if (item.type !== "message") continue;
|
|
197
198
|
const role = item.role === "assistant" ? "assistant" : item.role === "user" ? "user" : null;
|
|
198
199
|
if (!role) continue;
|
|
199
|
-
const
|
|
200
|
+
const rawContent = contentToText(item.content);
|
|
201
|
+
const content = role === "user" ? stripSharedContext(rawContent) : rawContent;
|
|
200
202
|
if (content) messages.push({ id: `rollout-${i++}`, role, content, timestamp: ts });
|
|
201
203
|
}
|
|
202
204
|
}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
UsageInfo,
|
|
11
11
|
} from "../provider.interface.ts";
|
|
12
12
|
import { configService } from "../../services/config.service.ts";
|
|
13
|
+
import { withSharedContext } from "../../shared/provider-context.ts";
|
|
13
14
|
import { setSessionMetadata, getSessionProjectPath, setSessionProvider, setSessionCodexAccount, getSessionCodexAccount, getSessionTitles, insertTurnUsage } from "../../services/db.service.ts";
|
|
14
15
|
import {
|
|
15
16
|
resolveCodexAccountForSession,
|
|
@@ -291,6 +292,7 @@ function buildUserInputResponse(questions: unknown, data: unknown): ToolRequestU
|
|
|
291
292
|
* notification stream. Token-by-token streaming is the load-bearing capability.
|
|
292
293
|
*/
|
|
293
294
|
export class CodexAppServerProvider implements AIProvider {
|
|
295
|
+
readonly supportsSharedContext = true;
|
|
294
296
|
readonly id = "codex";
|
|
295
297
|
readonly name = "Codex";
|
|
296
298
|
|
|
@@ -448,7 +450,7 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
448
450
|
// gets. Images lead: the text usually refers to them ("what is this?").
|
|
449
451
|
const input: UserInput[] = [
|
|
450
452
|
...(opts?.imagePaths ?? []).map((path) => ({ type: "localImage" as const, path })),
|
|
451
|
-
{ type: "text" as const, text: message, text_elements: [] },
|
|
453
|
+
{ type: "text" as const, text: withSharedContext(message, opts?.sharedContext), text_elements: [] },
|
|
452
454
|
];
|
|
453
455
|
const turnModel = codexModel(opts?.model);
|
|
454
456
|
const turnEffort = opts?.effort ?? this.config?.effort;
|
|
@@ -651,6 +653,7 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
651
653
|
client.notify("initialized");
|
|
652
654
|
|
|
653
655
|
const resumeBase = {
|
|
656
|
+
...this.contextConfigOverrides(),
|
|
654
657
|
cwd: live.cwd,
|
|
655
658
|
sandbox: live.permission.sandbox,
|
|
656
659
|
approvalPolicy: live.permission.approvalPolicy,
|
|
@@ -687,6 +690,17 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
687
690
|
return client.request("thread/resume", { threadId, path, ...resumeBase });
|
|
688
691
|
}
|
|
689
692
|
|
|
693
|
+
/** Read on connect/resume only; changing settings never interrupts a live turn. */
|
|
694
|
+
private contextConfigOverrides(): { config?: Record<string, number> } {
|
|
695
|
+
const providerConfig = this.config;
|
|
696
|
+
const config: Record<string, number> = {};
|
|
697
|
+
for (const key of ["model_context_window", "model_auto_compact_token_limit"] as const) {
|
|
698
|
+
const value = providerConfig?.[key];
|
|
699
|
+
if (value != null) config[key] = value;
|
|
700
|
+
}
|
|
701
|
+
return Object.keys(config).length ? { config } : {};
|
|
702
|
+
}
|
|
703
|
+
|
|
690
704
|
private async connect(sessionId: string, opts?: SendMessageOpts): Promise<LiveSession> {
|
|
691
705
|
const meta = this.sessions.get(sessionId);
|
|
692
706
|
const cwd = meta?.projectPath || getSessionProjectPath(sessionId) || process.cwd();
|
|
@@ -720,7 +734,7 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
720
734
|
await client.request("initialize", { clientInfo: CLIENT_INFO, capabilities: CAPABILITIES }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
721
735
|
client.notify("initialized");
|
|
722
736
|
|
|
723
|
-
const resumeBase = { cwd, sandbox: permission.sandbox, approvalPolicy: permission.approvalPolicy, ...(model ? { model } : {}) };
|
|
737
|
+
const resumeBase = { cwd, sandbox: permission.sandbox, approvalPolicy: permission.approvalPolicy, ...(model ? { model } : {}), ...this.contextConfigOverrides() };
|
|
724
738
|
// Only treat as a resume when a rollout for this id is attributable to THIS
|
|
725
739
|
// project (fail-closed cwd guard) — never resume another project's thread.
|
|
726
740
|
const result = found
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mapRolloutItem } from "./codex-rollout-items.ts";
|
|
2
|
+
import { stripSharedContext } from "../../shared/provider-context.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Line primitives for a codex rollout JSONL, plus the header a session list
|
|
@@ -49,7 +50,7 @@ const TITLE_MAX_CHARS = 80;
|
|
|
49
50
|
|
|
50
51
|
/** First user prompt → a one-line label. */
|
|
51
52
|
function toTitle(text: string): string | undefined {
|
|
52
|
-
const flat = text.replace(/\s+/g, " ").trim();
|
|
53
|
+
const flat = stripSharedContext(text).replace(/\s+/g, " ").trim();
|
|
53
54
|
if (!flat) return undefined;
|
|
54
55
|
return flat.length > TITLE_MAX_CHARS ? `${flat.slice(0, TITLE_MAX_CHARS - 1)}…` : flat;
|
|
55
56
|
}
|
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { existsSync, readdirSync } from "node:fs";
|
|
5
5
|
import type { ChatMessage, SessionInfo } from "../provider.interface.ts";
|
|
6
|
+
import { stripSharedContext } from "../../shared/provider-context.ts";
|
|
6
7
|
|
|
7
8
|
const DEFAULT_CHATS_DIR = join(homedir(), ".cursor", "chats");
|
|
8
9
|
|
|
@@ -38,7 +39,7 @@ export async function listCursorSessions(providerId: string, chatsDir?: string):
|
|
|
38
39
|
const hex = typeof row.value === "string" ? row.value : Buffer.from(row.value).toString("utf-8");
|
|
39
40
|
const json = Buffer.from(hex, "hex").toString("utf-8");
|
|
40
41
|
const meta = JSON.parse(json);
|
|
41
|
-
if (meta.name) title = meta.name.split("\n")[0]
|
|
42
|
+
if (meta.name) title = stripSharedContext(meta.name).split("\n")[0]?.slice(0, 80) || title;
|
|
42
43
|
if (meta.createdAt) createdAt = new Date(meta.createdAt).toISOString();
|
|
43
44
|
}
|
|
44
45
|
} catch { /* use defaults */ }
|
|
@@ -139,6 +140,7 @@ function parseDagBlobs(
|
|
|
139
140
|
if (parsed.role === "user") {
|
|
140
141
|
const match = content.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/);
|
|
141
142
|
if (match?.[1]) content = match[1];
|
|
143
|
+
content = stripSharedContext(content);
|
|
142
144
|
}
|
|
143
145
|
messages.push({
|
|
144
146
|
id: blob.id,
|
|
@@ -159,7 +161,7 @@ function parseDagBlobs(
|
|
|
159
161
|
messages.push({
|
|
160
162
|
id: blob.id,
|
|
161
163
|
role: messages.length % 2 === 0 ? "user" : "assistant",
|
|
162
|
-
content: textParts,
|
|
164
|
+
content: messages.length % 2 === 0 ? stripSharedContext(textParts) : textParts,
|
|
163
165
|
timestamp: new Date().toISOString(),
|
|
164
166
|
});
|
|
165
167
|
continue;
|
|
@@ -3,6 +3,7 @@ import { configService, FILE_CONFIG_KEYS } from "../../services/config.service.t
|
|
|
3
3
|
import { getConfigValue, setConfigValue, listPairedChats, getPairingByCode, approvePairing, revokePairing, getPPMBotMemories, getDb } from "../../services/db.service.ts";
|
|
4
4
|
import {
|
|
5
5
|
validateAIProviderConfig,
|
|
6
|
+
validateCodexContextConfig,
|
|
6
7
|
validateDefaultProvider,
|
|
7
8
|
VALID_PROVIDERS,
|
|
8
9
|
DEFAULT_CONFIG,
|
|
@@ -178,16 +179,21 @@ settingsRoutes.get("/ai", (c) => {
|
|
|
178
179
|
return c.json(ok(stripSensitiveFields(ai)));
|
|
179
180
|
});
|
|
180
181
|
|
|
181
|
-
/** PUT /settings/ai — update AI
|
|
182
|
+
/** PUT /settings/ai — update AI settings, persists to SQLite */
|
|
182
183
|
settingsRoutes.put("/ai", async (c) => {
|
|
183
184
|
try {
|
|
184
185
|
const body = await c.req.json<{
|
|
185
186
|
default_provider?: string;
|
|
187
|
+
share_provider_context?: boolean;
|
|
186
188
|
providers?: Record<string, Partial<AIProviderConfig>>;
|
|
187
189
|
}>();
|
|
188
190
|
|
|
189
191
|
const currentAi = configService.get("ai");
|
|
190
192
|
|
|
193
|
+
if ("share_provider_context" in body && typeof body.share_provider_context !== "boolean") {
|
|
194
|
+
return c.json(err("share_provider_context must be a boolean"), 400);
|
|
195
|
+
}
|
|
196
|
+
|
|
191
197
|
// Validate each provider config
|
|
192
198
|
if (body.providers) {
|
|
193
199
|
for (const [name, providerConfig] of Object.entries(body.providers)) {
|
|
@@ -201,6 +207,7 @@ settingsRoutes.put("/ai", async (c) => {
|
|
|
201
207
|
// Merge: body overrides current values (shallow merge per provider)
|
|
202
208
|
const updated = {
|
|
203
209
|
...currentAi,
|
|
210
|
+
share_provider_context: body.share_provider_context ?? currentAi.share_provider_context ?? true,
|
|
204
211
|
...(body.default_provider && { default_provider: body.default_provider }),
|
|
205
212
|
};
|
|
206
213
|
if (body.providers) {
|
|
@@ -214,6 +221,13 @@ settingsRoutes.put("/ai", async (c) => {
|
|
|
214
221
|
...currentAi.providers[name],
|
|
215
222
|
...config,
|
|
216
223
|
} as AIProviderConfig;
|
|
224
|
+
for (const key of ["model_context_window", "model_auto_compact_token_limit"] as const) {
|
|
225
|
+
if (updated.providers[name]![key] === null) delete updated.providers[name]![key];
|
|
226
|
+
}
|
|
227
|
+
const contextErrors = validateCodexContextConfig(updated.providers[name]!);
|
|
228
|
+
if (contextErrors.length) {
|
|
229
|
+
return c.json(err(`Provider "${name}": ${contextErrors.join(", ")}`), 400);
|
|
230
|
+
}
|
|
217
231
|
}
|
|
218
232
|
}
|
|
219
233
|
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -1288,7 +1288,7 @@ export const chatWebSocket = {
|
|
|
1288
1288
|
if (provider && "pushMessage" in provider && parsed.type === "message") {
|
|
1289
1289
|
const effort = getSessionEffort(sessionId) ?? undefined;
|
|
1290
1290
|
const thinkingBudget = getSessionThinking(sessionId);
|
|
1291
|
-
|
|
1291
|
+
await chatService.pushMessage(providerId, sessionId, parsed.content, {
|
|
1292
1292
|
priority: parsed.priority ?? 'next',
|
|
1293
1293
|
images: parsed.images,
|
|
1294
1294
|
imagePaths: parsed.imagePaths,
|
|
@@ -1413,7 +1413,7 @@ export const chatWebSocket = {
|
|
|
1413
1413
|
}, 0);
|
|
1414
1414
|
});
|
|
1415
1415
|
} else if (provider && "pushMessage" in provider) {
|
|
1416
|
-
|
|
1416
|
+
await chatService.pushMessage(providerId, sessionId, instruction, { priority: "next" });
|
|
1417
1417
|
entry.turnEvents = [];
|
|
1418
1418
|
entry.pendingApprovalEvent = undefined;
|
|
1419
1419
|
setPhase(sessionId, "thinking");
|