@hienlh/ppm 0.20.4 → 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.
Files changed (106) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/assets/skills/ppm/SKILL.md +1 -1
  3. package/assets/skills/ppm/references/cli-reference.md +4 -4
  4. package/assets/skills/ppm/references/http-api.md +1 -1
  5. package/dist/web/assets/{accounts-settings-section-BcI3ithh.js → accounts-settings-section-B-F7U-gq.js} +1 -1
  6. package/dist/web/assets/{ai-resource-editor-C3i7VkUS.js → ai-resource-editor-qRHwpktf.js} +1 -1
  7. package/dist/web/assets/{ai-settings-section-DIqSLISN.js → ai-settings-section-BePrVceb.js} +1 -1
  8. package/dist/web/assets/ai-settings-section-DSlsTrpf.js +1 -0
  9. package/dist/web/assets/{appearance-settings-section-B9n42QPH.js → appearance-settings-section-BMlkj9U2.js} +1 -1
  10. package/dist/web/assets/chat-tab-DmqV3MlC.js +15 -0
  11. package/dist/web/assets/{code-editor-iTD62P14.js → code-editor-C4CriX9c.js} +2 -2
  12. package/dist/web/assets/{conflict-editor-idh_kBSa.js → conflict-editor-LfQoRcK-.js} +1 -1
  13. package/dist/web/assets/{csv-preview-B2PR7z0o.js → csv-preview-BEYKn68B.js} +1 -1
  14. package/dist/web/assets/{database-viewer-DSv_pvEq.js → database-viewer-BL9R-ciJ.js} +1 -1
  15. package/dist/web/assets/{diff-viewer-6X5S1lw3.js → diff-viewer-D91SI_zb.js} +1 -1
  16. package/dist/web/assets/{explorer-body-CrtXpmZM.js → explorer-body-ColVUjgk.js} +1 -1
  17. package/dist/web/assets/{explorer-window-content-D11FZUgq.js → explorer-window-content-VSMJfZC2.js} +1 -1
  18. package/dist/web/assets/{extension-manager-section-Cz8fvK1u.js → extension-manager-section-B-BRHGu1.js} +1 -1
  19. package/dist/web/assets/{extension-webview-DzZeXQsO.js → extension-webview-DufzanN9.js} +1 -1
  20. package/dist/web/assets/{files-settings-section-pc4jOun8.js → files-settings-section-Do5YbTUK.js} +1 -1
  21. package/dist/web/assets/{general-settings-section-DIZIA54q.js → general-settings-section-B-n_PYzs.js} +1 -1
  22. package/dist/web/assets/{git-log-panel-BkcIfZIE.js → git-log-panel-BQzOHgxG.js} +1 -1
  23. package/dist/web/assets/{glide-data-grid-nA5LsoJK.js → glide-data-grid-BUS44oLM.js} +1 -1
  24. package/dist/web/assets/{group-chat-tab-DyzfaPol.js → group-chat-tab-CkHXqorZ.js} +1 -1
  25. package/dist/web/assets/{index-CuYtJ5xF.css → index-B9Wbjl0o.css} +1 -1
  26. package/dist/web/assets/{index-DJM59ogE.js → index-Cx1meAwb.js} +4 -4
  27. package/dist/web/assets/{jira-watcher-section-UEtpcxM9.js → jira-watcher-section-BQ4hVqWo.js} +1 -1
  28. package/dist/web/assets/{keybindings-store-BV0MMt1c.js → keybindings-store-yYx88hvw.js} +1 -1
  29. package/dist/web/assets/{keyboard-shortcuts-section-sY56ThaI.js → keyboard-shortcuts-section-BZUYdXGx.js} +1 -1
  30. package/dist/web/assets/{markdown-renderer-Bkrk39pO.js → markdown-renderer-BA3C2L-a.js} +1 -1
  31. package/dist/web/assets/{markdown-renderer-Bthd7hxl.js → markdown-renderer-CMFkajd7.js} +1 -1
  32. package/dist/web/assets/{mobile-explorer-sheet--NKsnNJn.js → mobile-explorer-sheet-gvO7a80O.js} +1 -1
  33. package/dist/web/assets/{notification-store-Bv4fnyvR.js → notification-store-CCh6a8RD.js} +1 -1
  34. package/dist/web/assets/{notifications-settings-section-BGSLkMuE.js → notifications-settings-section-JQ3l-Qp-.js} +1 -1
  35. package/dist/web/assets/{pdf-preview-DE0wVfyM.js → pdf-preview-B-JAU8JV.js} +1 -1
  36. package/dist/web/assets/{postgres-viewer-C0S8dCbA.js → postgres-viewer-B--Y59n5.js} +1 -1
  37. package/dist/web/assets/{ppmbot-settings-section-B2AZ8whe.js → ppmbot-settings-section-D1SCOc0_.js} +1 -1
  38. package/dist/web/assets/{process-table-BmT34-_s.js → process-table-D9z7cEmb.js} +1 -1
  39. package/dist/web/assets/{proxy-settings-section-BYwWFWvn.js → proxy-settings-section-y7VwH0Hf.js} +1 -1
  40. package/dist/web/assets/{query-audit-section-Dj1-npew.js → query-audit-section-Ca_gRQ_s.js} +1 -1
  41. package/dist/web/assets/{remote-desktop-mobile-sheet-bFIYIQIj.js → remote-desktop-mobile-sheet-CdO2aMXl.js} +2 -2
  42. package/dist/web/assets/{remote-desktop-mobile-view-BbrHPmPU.js → remote-desktop-mobile-view-CGQ8vSf1.js} +1 -1
  43. package/dist/web/assets/{remote-desktop-readiness-gate-Dme8r7Jp.js → remote-desktop-readiness-gate-BmC3b05e.js} +1 -1
  44. package/dist/web/assets/{remote-desktop-window-content-BdbszoSx.js → remote-desktop-window-content-Cdaoltdz.js} +1 -1
  45. package/dist/web/assets/{schedules-settings-section-rczhjkeM.js → schedules-settings-section--0s10CfA.js} +1 -1
  46. package/dist/web/assets/{settings-body-CwaQg_uc.js → settings-body-DEMqFm5z.js} +2 -2
  47. package/dist/web/assets/{settings-tab-C8l9-_9E.js → settings-tab-Dh1FpBny.js} +1 -1
  48. package/dist/web/assets/{settings-window-content-EA1489gi.js → settings-window-content-3KrQ6imZ.js} +1 -1
  49. package/dist/web/assets/{sql-query-editor-BQsKZVtz.js → sql-query-editor-Clo90z8-.js} +1 -1
  50. package/dist/web/assets/{sqlite-viewer-Di8s3Z1m.js → sqlite-viewer-C08QQR_L.js} +1 -1
  51. package/dist/web/assets/{system-monitor-body-Dlknm4Uo.js → system-monitor-body-30-TFH3a.js} +2 -2
  52. package/dist/web/assets/{system-monitor-tab-LZRO6_DP.js → system-monitor-tab-ChIXfOWg.js} +1 -1
  53. package/dist/web/assets/{system-monitor-window-content-BFDfhqDd.js → system-monitor-window-content-C1Cd_pT7.js} +1 -1
  54. package/dist/web/assets/{tab-host-window-content-Bh7064Yw.js → tab-host-window-content-C6eG_Rf1.js} +1 -1
  55. package/dist/web/assets/{team-member-sheet-CU68IqU0.js → team-member-sheet-BhatQbL1.js} +2 -2
  56. package/dist/web/assets/{team-member-window-content-BYtro89j.js → team-member-window-content-BN3luct-.js} +1 -1
  57. package/dist/web/assets/{terminal-tab-qfSPxRtZ.js → terminal-tab-afylLiLY.js} +1 -1
  58. package/dist/web/assets/tool-cards-Ct1QdT8c.js +9 -0
  59. package/dist/web/assets/{use-accounts-data-pAQw6nCa.js → use-accounts-data-C0bvcMhi.js} +1 -1
  60. package/dist/web/assets/{use-monaco-theme-DT5Q8kuf.js → use-monaco-theme-cgPbRDUm.js} +1 -1
  61. package/dist/web/assets/{use-remote-desktop-display-choice-CYQsrOMC.js → use-remote-desktop-display-choice-DSqF1Rgx.js} +1 -1
  62. package/dist/web/assets/use-websocket-BQssVSIR.js +1 -0
  63. package/dist/web/assets/{video-preview-B2dk48oQ.js → video-preview-BTzstVhM.js} +1 -1
  64. package/dist/web/index.html +2 -2
  65. package/dist/web/sw.js +1 -1
  66. package/docs/architecture/ai-chat-and-providers.md +88 -0
  67. package/package.json +1 -1
  68. package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
  69. package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
  70. package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
  71. package/packages/vscode-compat/node_modules/.bin/tsserver.exe +0 -0
  72. package/schemas/ppm-config.schema.json +18 -1
  73. package/src/providers/claude-agent-sdk.ts +21 -13
  74. package/src/providers/cli-provider-base.ts +3 -1
  75. package/src/providers/codex-app-server/codex-event-mapper.ts +4 -2
  76. package/src/providers/codex-app-server/codex-history.ts +5 -3
  77. package/src/providers/codex-app-server/codex-provider.ts +16 -2
  78. package/src/providers/codex-app-server/codex-rollout-header.ts +2 -1
  79. package/src/providers/cursor-cli/cursor-history.ts +4 -2
  80. package/src/server/routes/settings.ts +15 -1
  81. package/src/server/ws/chat.ts +3 -3
  82. package/src/services/chat.service.ts +95 -2
  83. package/src/services/codex-native-memory.ts +66 -0
  84. package/src/services/edge-forwarder.ts +191 -191
  85. package/src/services/jsonl-transcript-parser.ts +2 -1
  86. package/src/services/provider-shared-context.ts +152 -0
  87. package/src/services/windows-process-tree.ts +351 -351
  88. package/src/shared/provider-context.ts +13 -0
  89. package/src/types/api.ts +1 -1
  90. package/src/types/chat.ts +7 -1
  91. package/src/types/config.ts +29 -0
  92. package/src/web/components/chat/chat-history-bar.tsx +6 -3
  93. package/src/web/components/chat/chat-tab.tsx +7 -0
  94. package/src/web/components/chat/message-list.tsx +1 -1
  95. package/src/web/components/chat/tool-cards.tsx +10 -0
  96. package/src/web/components/settings/ai-settings-section.tsx +34 -5
  97. package/src/web/components/settings/codex-context-settings.tsx +100 -0
  98. package/src/web/hooks/use-chat.ts +71 -14
  99. package/src/web/hooks/use-websocket.ts +14 -7
  100. package/src/web/lib/api-settings.ts +3 -0
  101. package/src/web/lib/ws-client.ts +55 -4
  102. package/.release-node-shim.sh +0 -10
  103. package/dist/web/assets/ai-settings-section-COdsN_5h.js +0 -1
  104. package/dist/web/assets/chat-tab-D9sk38e8.js +0 -15
  105. package/dist/web/assets/tool-cards-CY9J4ux4.js +0 -9
  106. package/dist/web/assets/use-websocket-mkNA1M8U.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.20.4",
3
+ "version": "0.20.6",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -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?: { priority?: 'now' | 'next' | 'later'; images?: Array<{ data: string; mediaType: string }> }): void {
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 msgContent = buildMessageParam(content, opts?.images);
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 = content;
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(message, opts?.images);
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 = message;
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(message, opts?.images),
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 ?? message;
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 ?? message;
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, " ").replace(/\\"/g, '"').trim();
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,
@@ -126,11 +126,13 @@ export function itemToToolResult(item: Item): ChatEvent {
126
126
  const type = item.type;
127
127
  let output = "";
128
128
  let isError = false;
129
+ let exitCode: number | undefined;
129
130
 
130
131
  if (type === "commandExecution") {
131
132
  output = redactTruncate(item.aggregatedOutput ?? "");
132
133
  const exit = item.exitCode;
133
- isError = typeof exit === "number" && exit !== 0;
134
+ exitCode = typeof exit === "number" ? exit : undefined;
135
+ isError = exitCode != null && exitCode !== 0;
134
136
  } else if (type === "mcpToolCall") {
135
137
  output = redactTruncate(item.result ?? item.error ?? "");
136
138
  isError = item.error != null;
@@ -153,7 +155,7 @@ export function itemToToolResult(item: Item): ChatEvent {
153
155
  output = redactTruncate(item);
154
156
  }
155
157
 
156
- return { type: "tool_result", output, isError, toolUseId: item.id };
158
+ return { type: "tool_result", output, isError, ...(exitCode != null ? { exitCode } : {}), toolUseId: item.id };
157
159
  }
158
160
 
159
161
  /**
@@ -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 content = contentToText(item.content);
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].slice(0, 80);
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 provider settings, writes to yaml */
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
 
@@ -333,7 +333,7 @@ function bufferAndBroadcast(sessionId: string, event: unknown): void {
333
333
  for (let i = entry.turnEvents.length - 1; i >= 0; i--) {
334
334
  const buffered = entry.turnEvents[i] as any;
335
335
  if (buffered.type === "tool_use" && buffered.toolUseId === toolUseId) {
336
- buffered.result = { output: (event as any).output, isError: (event as any).isError };
336
+ buffered.result = { output: (event as any).output, isError: (event as any).isError, exitCode: (event as any).exitCode };
337
337
  break;
338
338
  }
339
339
  }
@@ -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
- (provider as any).pushMessage(sessionId, parsed.content, {
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
- (provider as any).pushMessage(sessionId, instruction, { priority: "next" });
1416
+ await chatService.pushMessage(providerId, sessionId, instruction, { priority: "next" });
1417
1417
  entry.turnEvents = [];
1418
1418
  entry.pendingApprovalEvent = undefined;
1419
1419
  setPhase(sessionId, "thinking");