cc-viewer 1.8.13 → 1.8.15

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 (49) hide show
  1. package/README.md +6 -5
  2. package/cli.js +19 -0
  3. package/dist/assets/{App-R6odqLoE.js → App-DxDo_tCQ.js} +2 -2
  4. package/dist/assets/{MdxEditorPanel-CTqsETwc.js → MdxEditorPanel-Ddpu5ELR.js} +1 -1
  5. package/dist/assets/Mobile-DA0yMMgI.js +1 -0
  6. package/dist/assets/{ProxyStatsModal-CM2H8JYh.js → ProxyStatsModal-DKKfSpXx.js} +1 -1
  7. package/dist/assets/index-BCpkzaVk.js +2 -0
  8. package/dist/assets/index-Nvk8r3tO.css +1 -0
  9. package/dist/assets/{seqResourceLoaders-Xx6a8E03.css → seqResourceLoaders-B_FSQh3U.css} +1 -1
  10. package/dist/assets/seqResourceLoaders-BkzxH2jr.js +2 -0
  11. package/dist/index.html +2 -2
  12. package/node_modules/@ccv/core/src/context-rules.js +5 -0
  13. package/package.json +1 -1
  14. package/server/i18n.js +2 -2
  15. package/server/interceptor.js +176 -21
  16. package/server/lib/builtin-model-prompts.js +12 -1
  17. package/server/lib/create_system_prompt.js +94 -1
  18. package/server/lib/ensure-hooks.js +34 -3
  19. package/server/lib/interceptor-core.js +144 -0
  20. package/server/lib/launch-config.js +83 -3
  21. package/server/lib/model-system-prompts.js +6 -1
  22. package/server/lib/proxy/proxy-retry.js +57 -2
  23. package/server/lib/resume-watchdog.js +108 -0
  24. package/server/lib/session-id.js +33 -0
  25. package/server/lib/shell-hook-inspect.js +48 -0
  26. package/server/lib/system-prompt-files.js +14 -0
  27. package/server/lib/system-prompt-live.js +622 -0
  28. package/server/lib/task-bridge.js +10 -3
  29. package/server/lib/task-state.js +32 -0
  30. package/server/lib/v2/identity.js +3 -24
  31. package/server/lib/v2/session-select.js +11 -8
  32. package/server/lib/v2/v2-writer.js +13 -5
  33. package/server/proxy.js +32 -6
  34. package/server/routes/events.js +9 -3
  35. package/server/routes/preferences.js +6 -1
  36. package/server/routes/workspaces.js +3 -2
  37. package/server/server.js +35 -5
  38. package/server/system-prompt-templates/presets/GLM-5.2.md +2 -0
  39. package/server/system-prompt-templates/presets/GLM-5.3.md +2 -0
  40. package/server/system-prompt-templates/presets/{Qwen-3.7-Max.md → Qwen-3.md} +3 -1
  41. package/server/system-prompt-templates/presets/deepseek-v4-flash.md +2 -0
  42. package/server/system-prompt-templates/presets/deepseek-v4-pro.md +2 -0
  43. package/server/system-prompt-templates/presets/index.json +7 -7
  44. package/server/system-prompt-templates/presets/kimi-k2.7-code.md +3 -0
  45. package/server/system-prompt-templates/presets/kimi-k3.md +2 -0
  46. package/dist/assets/Mobile-o_QrQ_eI.js +0 -1
  47. package/dist/assets/index--yjDHxJD.js +0 -2
  48. package/dist/assets/index-D1yGak8I.css +0 -1
  49. package/dist/assets/seqResourceLoaders-BgeRQt8C.js +0 -2
@@ -34,6 +34,14 @@ function _touch(task) {
34
34
  export function applyTaskEvent(payload) {
35
35
  if (!payload || typeof payload !== 'object') return;
36
36
  const { hookEventName } = payload;
37
+ // New-prompt reset. MUST stay above the taskId guard (this event has no
38
+ // task_id) and MUST NOT move _sessionId — a foreign/malformed prompt event
39
+ // must not change the shouldResetTasks comparison base. resetTasks() nulls
40
+ // the tag anyway, so the next TaskCreated re-tags the session.
41
+ if (hookEventName === 'UserPromptSubmit') {
42
+ if (shouldResetTasksOnPrompt(payload, _sessionId)) resetTasks();
43
+ return;
44
+ }
37
45
  const taskId = payload.taskId != null ? String(payload.taskId) : null;
38
46
  // Validate BEFORE touching the session tag: a malformed/unknown event must
39
47
  // not move the shouldResetTasks comparison base.
@@ -139,6 +147,30 @@ export function shouldResetTasks(payload, currentSessionId) {
139
147
  return false;
140
148
  }
141
149
 
150
+ /**
151
+ * New-prompt reset gate (pure, unit-tested). Claude Code fires
152
+ * UserPromptSubmit on every user prompt (queued-message drains included) with
153
+ * NO task_id; the previous turn's checklist is stale by definition, so the
154
+ * shared list resets and the model's next TaskUpdate rebuilds whatever it is
155
+ * still working on (stub semantics).
156
+ * Conservative guards: a teammate/subagent process inherits CCVIEWER_PORT and
157
+ * POSTs to the same /api/task-event, so only a prompt bearing the session we
158
+ * are already tracking may wipe the shared list. (agent_id is NOT reliably
159
+ * present on this event — the session check is the load-bearing one.)
160
+ * Known trade-off: while the tag is null (no main-agent task event seen yet,
161
+ * e.g. right after a reset — teammate TaskCreated carries agentId and does not
162
+ * re-tag), ANY prompt passes the gate. A teammate prompt in that window still
163
+ * clears the list; it self-heals on the next TaskUpdate. See task-state.test.js
164
+ * ("null-tag window") which locks this behavior deliberately.
165
+ */
166
+ export function shouldResetTasksOnPrompt(payload, currentSessionId) {
167
+ const { agentId, sessionId } = payload || {};
168
+ if (agentId) return false;
169
+ if (!sessionId) return false;
170
+ if (currentSessionId && sessionId !== currentSessionId) return false;
171
+ return true;
172
+ }
173
+
142
174
  /** Full-snapshot view for SSE broadcast; insertion order preserved. */
143
175
  export function getTaskSnapshot() {
144
176
  return { sessionId: _sessionId, tasks: [..._tasks.values()] };
@@ -13,30 +13,9 @@
13
13
  // prefix (server/lib/interceptor-core.js TEAMMATE_PROMPT_PREFIX_LEN).
14
14
  export const SPAWN_PROMPT_PREFIX_LEN = 60;
15
15
 
16
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17
-
18
- /**
19
- * Parse a raw metadata.user_id string into { sessionId, encoding } or null.
20
- * Encodings (spec §8):
21
- * - 'json': '{"device_id":…,"account_uuid":…,"session_id":"<uuid>"}'
22
- * - 'delimited': 'user_<hash>_account_<acct?>_session_<uuid>'
23
- */
24
- export function parseUserId(userIdRaw) {
25
- if (typeof userIdRaw !== 'string' || userIdRaw === '') return null;
26
- try {
27
- const obj = JSON.parse(userIdRaw);
28
- if (obj && typeof obj.session_id === 'string' && obj.session_id !== '') {
29
- return { sessionId: obj.session_id, encoding: 'json' };
30
- }
31
- return null; // valid JSON but no session_id — treat as unparseable
32
- } catch { /* not JSON → try the delimited form */ }
33
- const idx = userIdRaw.lastIndexOf('_session_');
34
- if (idx >= 0) {
35
- const tail = userIdRaw.slice(idx + '_session_'.length);
36
- if (UUID_RE.test(tail)) return { sessionId: tail, encoding: 'delimited' };
37
- }
38
- return null;
39
- }
16
+ // parseUserId lives in the shared leaf session-id.js (so lib/proxy can use it
17
+ // without an R3 cross-subsystem edge); re-exported here for existing consumers.
18
+ export { parseUserId } from '../session-id.js';
40
19
 
41
20
  const REMINDER_OPEN = '<system-reminder>';
42
21
  const REMINDER_CLOSE = '</system-reminder>';
@@ -142,8 +142,10 @@ export function sessionHasMainTurn(dir) {
142
142
  * COMPLETED_TURN_SCAN_BUDGET — wide enough that a heavy multi-agent first turn
143
143
  * whose done lands megabytes past the head is still found. Stronger than
144
144
  * `sessionHasMainTurn`: a session with only an in-flight first main request (req
145
- * written, response still streaming) returns false, so cold-load keeps showing
146
- * the previous conversation until the current one has renderable content.
145
+ * written, response still streaming) returns false. This no longer decides the
146
+ * cold-load source on its own getLiveLogSource ORs `sessionHasMainTurn` onto
147
+ * it to also accept an in-flight first main req on the v3 wire, whose conv
148
+ * prefix is renderable at request initiation (2026-09-13 refresh-blank fix).
147
149
  * @param {string} dir - absolute session dir
148
150
  * @returns {boolean}
149
151
  */
@@ -243,11 +245,12 @@ export function isDiscardableSession(dir, meta) {
243
245
  * the identity UUID (meta.sessionId) alongside the dir so a `-c` continuation
244
246
  * can adopt this session's folder while preserving its identity.
245
247
  *
246
- * `excludeDir` skips one absolute session dir entirely: getLiveLogSource passes
247
- * its current in-flight session here, because this picker's weaker
248
- * has-a-main-req gate would otherwise re-select exactly the dir the caller's
249
- * completed-turn gate just rejected (it IS the newest once its first main req
250
- * is written), nullifying the fallback.
248
+ * `excludeDir` skips one absolute session dir entirely. getLiveLogSource passes
249
+ * its current dir: the caller's activated gate and this picker now share the
250
+ * same has-a-main-req predicate, so exclusion is no longer selection logic
251
+ * it closes the TOCTOU race where a main req line lands between the caller's
252
+ * gate check and this picker's scan (the dir would otherwise be re-selected
253
+ * as the newest, nullifying the fallback).
251
254
  *
252
255
  * `skipForeignLive` (multi-window isolation, 2026-07-17) drops candidates
253
256
  * whose `owner.lock` is held by ANOTHER live process — a parallel ccv window's
@@ -294,7 +297,7 @@ export function latestMainSession(projectDir, { excludeDir = '', skipForeignLive
294
297
  * Absolute dir of the newest readable, non-teammate session that HAS a main
295
298
  * turn, or '' if there is none. Thin wrapper over {@link latestMainSession}.
296
299
  * @param {string} projectDir - absolute LOG_DIR/<project>
297
- * @param {{excludeDir?: string}} [opts] - see {@link latestMainSession}
300
+ * @param {{excludeDir?: string, skipForeignLive?: boolean}} [opts] - see {@link latestMainSession}
298
301
  * @returns {string} absolute session dir, or ''
299
302
  */
300
303
  export function latestMainSessionDir(projectDir, opts) {
@@ -437,17 +437,25 @@ export class V2Writer {
437
437
  // count_tokens/heartbeat probes wear main-agent shapes but aren't the user's
438
438
  // turn (same gate as the resume-switch above); adopted (-c) sessions skip —
439
439
  // their pendings are resumeExpected, reserved for the SessionStart-hook bind.
440
- // First-main-only via the per-session flag: probes may create the session state
441
- // before the real turn arrives. The live-sessions-dir gate keeps the offline
442
- // converter (staging dirs) from consuming LIVE pendings. Fully caught: a lost
443
- // bind degrades to the no-record resume path, never to a dropped log entry.
440
+ // The latch only locks on a SUCCESSFUL consume: a session's first main-shaped
441
+ // request can be a small-model side call (title/compression) whose system text
442
+ // the live layer already rewrote to that model's persona (interceptor.js:1124-
443
+ // 1151 rewrites requestEntry.body BEFORE this ingest) it matches no pending,
444
+ // and latching there would permanently deny the real main-model turns that
445
+ // follow (observed: a session whose 8 later PRO-persona requests never rebound,
446
+ // leaving every later `-c` on the F2 no-record path). Unmatched requests retry;
447
+ // a genuinely injection-less session still consumes its EMPTY pending on some
448
+ // later request (the fallback just records "no injection", so deferring it is
449
+ // harmless). The live-sessions-dir gate keeps the offline converter (staging
450
+ // dirs) from consuming LIVE pendings. Fully caught: a lost bind degrades to
451
+ // the no-record resume path, never to a dropped log entry.
444
452
  if (!adoptTarget && entry.mainAgent && !entry.isCountTokens && !entry.isHeartbeat
445
453
  && this._sessionsDirName === 'sessions'
446
454
  && s && !s.sysPromptBindDone) {
447
- s.sysPromptBindDone = true;
448
455
  try {
449
456
  const pend = consumePendingForWireByKey(project, systemTextOfBody(entry.body), this._logDir);
450
457
  if (pend) {
458
+ s.sysPromptBindDone = true;
451
459
  writeSnapshotByKey(project, sid, { entries: pend.entries, model: pend.model, boundVia: 'wire' }, { logDir: this._logDir });
452
460
  }
453
461
  } catch (err) { reportSwallowed('v2-write.sys-prompt-bind', err); }
package/server/proxy.js CHANGED
@@ -9,6 +9,8 @@ import { extractApiErrorMessage, formatProxyRequestError } from './lib/proxy/pro
9
9
  import { getProxyDispatcher } from './lib/proxy/proxy-env.js';
10
10
  import { getClaudeConfigDir } from '../findcc.js';
11
11
  import { isAnthropicApiPath, classifyProxyRole } from './lib/interceptor-core.js';
12
+ import { parseAgentId, findHeader } from './lib/v2/agent-id.js';
13
+ import { liveSystemPromptEnabled } from './lib/system-prompt-live.js';
12
14
  import { executeRequest, extractModel } from './lib/proxy/proxy-retry.js';
13
15
  import { buildRecord, appendRecord, dailyFilePath, todayStr, emitProxyStatsUpdate } from './lib/proxy/proxy-stats.js';
14
16
  import { reportSwallowed } from '@ccv/core/error-report';
@@ -134,13 +136,29 @@ export function startProxy() {
134
136
  const _reqPath = (() => { try { return new URL(req.url || '/', 'http://x').pathname; } catch { return req.url || ''; } })();
135
137
  const _isUtility = /\/messages\/count_tokens$/.test(_reqPath) || /^\/api\/eval\/sdk-/.test(_reqPath);
136
138
  let roleProfile;
137
- if (body.length > 0 && isAnthropicApiPath(_reqPath) && !_isUtility && interceptor.hasExplicitRoleAssignments()) {
139
+ let _role; // live system 改写的 role 门(仅 main 生效);未分类 → undefined → main 语义
140
+ const _needClassify = body.length > 0 && isAnthropicApiPath(_reqPath) && !_isUtility;
141
+ // 角色分类在两种情况下都必须做:显式角色分配(选路)或 live system 已启用
142
+ // (否则默认配置下 subagent/teammate 会以 role=undefined 穿过 proxy 侧门被注入主 system)。
143
+ if (_needClassify && (interceptor.hasExplicitRoleAssignments() || liveSystemPromptEnabled())) {
138
144
  let parsedBody = null;
139
145
  try { parsedBody = JSON.parse(body.toString('utf8')); } catch { /* 非 JSON body → 保持 main 语义 */ }
140
146
  if (parsedBody && typeof parsedBody === 'object') {
141
147
  try {
142
- const role = classifyProxyRole(parsedBody, {});
143
- roleProfile = interceptor.getEffectiveRoleProfile(role);
148
+ // Header 硬判据优先(与 interceptor fetch hook 同构,review P0):
149
+ // x-claude-code-agent-id 是 SDK 命名队友(name@…)/ 匿名子代理(hex)
150
+ // 的判别信号,不依赖 system 文本 —— body 正则对「SDK 身份行 + 无
151
+ // billing 标记」的形态失效,会把 header-only 子代理误判 'main' 而注入
152
+ // 主 persona。named→teammate、anon→subagent,覆盖 body 分类。
153
+ // Header hard-signal first (mirrors the interceptor fetch hook): the
154
+ // agent-id header marks sub-agent streams without relying on body markers.
155
+ const _agent = parseAgentId(findHeader(req.headers, 'x-claude-code-agent-id'));
156
+ _role = _agent
157
+ ? (_agent.named ? 'teammate' : 'subagent')
158
+ : classifyProxyRole(parsedBody, {});
159
+ if (interceptor.hasExplicitRoleAssignments()) {
160
+ roleProfile = interceptor.getEffectiveRoleProfile(_role);
161
+ }
144
162
  } catch (err) { reportSwallowed('proxy.role-classify', err); }
145
163
  }
146
164
  }
@@ -166,7 +184,7 @@ export function startProxy() {
166
184
  // 模型替换由重试引擎用 resolveProfileModel 纯函数完成(interceptor 对 trace 请求会再跑一次,幂等 no-op)。
167
185
  // 非大模型 API 请求走原逻辑(同样用 x-cc-viewer-trace 让 interceptor 记录,但不经重试引擎)。
168
186
  if (isAnthropicApiPath(fullUrl)) {
169
- await handleLlmApiRequest(req, res, fullUrl, fetchOptions, body, proxyDispatcher, roleProfile);
187
+ await handleLlmApiRequest(req, res, fullUrl, fetchOptions, body, proxyDispatcher, roleProfile, { role: _role, isUtility: _isUtility });
170
188
  return;
171
189
  }
172
190
 
@@ -256,7 +274,7 @@ export function startProxy() {
256
274
  // 每请求取最新值,UI 改 retry-config.json 后下一个请求即生效,无需重启。
257
275
  const _retryConfigGetter = () => interceptor._retryConfigState;
258
276
 
259
- async function handleLlmApiRequest(req, res, fullUrl, fetchOptions, body, proxyDispatcher, roleProfile) {
277
+ async function handleLlmApiRequest(req, res, fullUrl, fetchOptions, body, proxyDispatcher, roleProfile, liveCtx = {}) {
260
278
  const statsEnabled = process.env.CCV_PROXY_STATS !== 'off';
261
279
  const retryConfig = _retryConfigGetter(); // live binding:每请求取最新重试配置
262
280
  // 角色解析后的有效 profile(上游/模型替换/统计归属)。undefined = 未分类 → 沿用 main 活跃 profile。
@@ -299,7 +317,15 @@ async function handleLlmApiRequest(req, res, fullUrl, fetchOptions, body, proxyD
299
317
  url: fullUrl,
300
318
  fetchOptions: retryFetchOptions,
301
319
  retryConfig,
302
- ctx: { dispatcher: proxyDispatcher, profile, signal: clientAbort.signal },
320
+ ctx: {
321
+ dispatcher: proxyDispatcher,
322
+ profile,
323
+ signal: clientAbort.signal,
324
+ // live system 改写上下文(system-prompt-live.js):role 门 + 项目键。
325
+ role: liveCtx.role,
326
+ isUtility: liveCtx.isUtility === true,
327
+ projectKey: interceptor._projectName || '',
328
+ },
303
329
  });
304
330
 
305
331
  const { response, attempts, retryCodes, durationMs, finalStatus, succeeded, upstreamStatus } = result;
@@ -6,7 +6,7 @@ import { LOG_DIR } from '../../findcc.js';
6
6
  import { streamRawEntriesAsync } from '../lib/log-stream.js';
7
7
  import { migrationStatus } from '../lib/v2/migrate-prompt.js';
8
8
  import { reportSwallowed } from '@ccv/core/error-report';
9
- import { sseHead, sseWrite, needsDrain, wireEnd, awaitWireDrain } from '../lib/wire-compress.js';
9
+ import { sseHead, sseWrite, needsDrain, wireEnd, awaitWireDrain, isWireV3Enabled } from '../lib/wire-compress.js';
10
10
  import { readV2ColdBundle } from '../lib/v2/meta-rows.js';
11
11
  import { readV2SingleEntry } from '../lib/v2/adapter.js';
12
12
  import { enrichRawIfNeeded } from '../lib/enrich-plan-input.js';
@@ -248,7 +248,11 @@ async function events(req, res, parsedUrl, isLocal, deps) {
248
248
 
249
249
  // S6b: the cold-load source is the current v2 session dir when the v2
250
250
  // writer is active (adapter stream), else the v1 file.
251
- const coldLoadResult = v3Cold ? null : await streamRawEntriesAsync(getLiveLogSource(), async (raw) => {
251
+ // serveInFlight follows the wire: the legacy entry stream ships the
252
+ // in-flight placeholder, which the client batch gate still blocks — the
253
+ // previous conversation is the better cold load there (interceptor.js
254
+ // getLiveLogSource header comment).
255
+ const coldLoadResult = v3Cold ? null : await streamRawEntriesAsync(getLiveLogSource({ serveInFlight: !!deps.wireV3 }), async (raw) => {
252
256
  // 直接发送原始 JSON 字符串,不做 parse/reconstruct/stringify
253
257
  // ExitPlanMode V2 空 input 的条目按需补全 plan / planFilePath,其它原样透传
254
258
  if (res.destroyed || !res.writable) return;
@@ -438,7 +442,9 @@ async function requests(req, res) {
438
442
  try {
439
443
  sseWrite(res, '[');
440
444
  let first = true;
441
- await streamRawEntriesAsync(getLiveLogSource(), (raw) => {
445
+ // No deps bag on this route — read the wire flag from the same env source
446
+ // server.js uses (isWireV3Enabled), keeping serveInFlight wire-consistent.
447
+ await streamRawEntriesAsync(getLiveLogSource({ serveInFlight: isWireV3Enabled(process.env.CCV_WIRE_V3) }), (raw) => {
442
448
  if (!first) sseWrite(res, ',');
443
449
  sseWrite(res, enrichRawIfNeeded(raw));
444
450
  first = false;
@@ -11,6 +11,7 @@ import { reportSwallowed } from '@ccv/core/error-report';
11
11
  import { setLang } from '../i18n.js';
12
12
  import { reconcileVoicePackPrefs as vpReconcile } from '../lib/voice-pack-manager.js';
13
13
  import { readClaudeProjectModel } from '../lib/context-watcher.js';
14
+ import { inspectShellHook } from '../lib/shell-hook-inspect.js';
14
15
  import { sendEventToClients } from '../lib/log-watcher.js';
15
16
  import { listPlatforms } from '../lib/im/im-config.js';
16
17
  import { mutatePrefs, applyPrefsPatch, readPrefsRaw } from '../lib/prefs-store.js';
@@ -274,7 +275,11 @@ function claudeSettingsGet(req, res, parsedUrl, isLocal, deps) {
274
275
  // 错显 200K,详见 src/utils/helpers.js resolveCalibrationTokens)。
275
276
  const projectCwd = process.env.CCV_PROJECT_DIR || process.cwd();
276
277
  const claudeSettings = deps.claudeSettings;
277
- res.end(JSON.stringify({ env, model: claudeSettings.model || null, showThinkingSummaries: claudeSettings.showThinkingSummaries || false, claudeAvailable: process.env.CCV_CLAUDE_MISSING !== '1', claudeProjectModel: readClaudeProjectModel(projectCwd) }));
278
+ // L1: shell hook 状态(只读;不传模板构造器 stale false,stale 精确判定由 cli.js
279
+ // 侧 refreshShellHookState 负责,这里回答「装没装」足够面板提示用)。失败退化 null。
280
+ let shellHook = null;
281
+ try { shellHook = inspectShellHook(null); } catch (err) { reportSwallowed('prefs.shellHook', err); }
282
+ res.end(JSON.stringify({ env, model: claudeSettings.model || null, showThinkingSummaries: claudeSettings.showThinkingSummaries || false, claudeAvailable: process.env.CCV_CLAUDE_MISSING !== '1', claudeProjectModel: readClaudeProjectModel(projectCwd), shellHook }));
278
283
  }
279
284
 
280
285
  function claudeSettingsPost(req, res, parsedUrl, isLocal, deps) {
@@ -99,8 +99,9 @@ function workspacesLaunch(req, res, parsedUrl, isLocal, deps) {
99
99
  // 流式分段广播以刷新会话区域,避免全量加载 OOM
100
100
  // S6b: the live source is the v2 session dir when the v2 writer is
101
101
  // active (a fresh workspace has no session yet → empty stream, the live
102
- // feed picks up from the first request).
103
- const wsReloadSource = getLiveLogSource();
102
+ // feed picks up from the first request). Legacy-shaped reload frames —
103
+ // serveInFlight follows the wire flag, same as /events.
104
+ const wsReloadSource = getLiveLogSource({ serveInFlight: !!deps.wireV3 });
104
105
  const wsReloadTotal = await countLogEntries(wsReloadSource);
105
106
  deps.clients.forEach(client => {
106
107
  try { sseWrite(client, `event: load_start\ndata: ${JSON.stringify({ total: wsReloadTotal, incremental: false })}\n\n`); } catch {}
package/server/server.js CHANGED
@@ -66,18 +66,32 @@ function execWithStdin(cmd, args, input, options) {
66
66
  const child = spawn(cmd, args, { ...options, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
67
67
  let stdout = '';
68
68
  let stderr = '';
69
+ let settled = false;
70
+ const settle = (fn, val) => { if (!settled) { settled = true; fn(val); } };
69
71
  child.stdout.on('data', d => { stdout += d; });
70
72
  child.stderr.on('data', d => { stderr += d; });
71
- child.on('error', reject);
73
+ child.on('error', (err) => settle(reject, err));
72
74
  child.on('close', code => {
73
75
  // git check-ignore exits 1 when no files are ignored — treat as success
74
- resolve(stdout);
76
+ settle(resolve, stdout);
75
77
  });
78
+ // A child that exits before consuming its stdin (git check-ignore outside a
79
+ // repo, a killed-on-timeout child, a missing binary) closes the read end of
80
+ // the stdin pipe; a subsequent write then throws `write EPIPE`. Without an
81
+ // 'error' listener on child.stdin that EPIPE escapes as an uncaughtException
82
+ // (the recurring server.test.js teardown flake) — swallow it into the
83
+ // promise's settle path instead.
84
+ child.stdin.on('error', () => settle(resolve, stdout));
76
85
  if (options?.timeout) {
77
- setTimeout(() => { try { child.kill(); } catch {} reject(new Error('timeout')); }, options.timeout);
86
+ setTimeout(() => { try { child.kill(); } catch {} settle(reject, new Error('timeout')); }, options.timeout);
87
+ }
88
+ try {
89
+ child.stdin.write(input);
90
+ child.stdin.end();
91
+ } catch {
92
+ // Synchronous EPIPE/ERR_STREAM_DESTROYED when the child already exited.
93
+ settle(resolve, stdout);
78
94
  }
79
- child.stdin.write(input);
80
- child.stdin.end();
81
95
  });
82
96
  }
83
97
  import { _initPromise, _projectName, _logDir, _v2Writer, streamingState, resetStreamingState, PROFILE_PATH, RETRY_CONFIG_PATH, _retryConfigState, setLivePort, getImLiveText, resetImLiveText, markSessionStart } from './interceptor.js';
@@ -602,6 +616,8 @@ const deps = {
602
616
  onSessionStartNotify: markSessionStart,
603
617
  // Task checklist (task-bridge.js → /api/task-event): apply to the shared
604
618
  // in-memory reducer, then debounce-broadcast a full snapshot over SSE.
619
+ // UserPromptSubmit events reset the list inside the reducer, so the same
620
+ // path broadcasts the empty snapshot — no separate reset route/hook.
605
621
  onTaskEvent: (payload) => {
606
622
  try {
607
623
  applyTaskEvent(payload);
@@ -1237,6 +1253,20 @@ export async function startViewer() {
1237
1253
  try { await imCore.stopAll(); } catch { /* no in-process instances */ }
1238
1254
  imProcMgr.reconcileImProcesses().catch((e) => console.error('[CC Viewer] IM reconcile failed:', e?.message || e));
1239
1255
  }
1256
+ // L2: 裸续接检测(transcript 在写但请求未经 ccv → 提示注入丢失)。仅 CLI 模式
1257
+ // (面板场景);检测失败只退化为不提示,绝不影响启动。
1258
+ if (isCliMode) {
1259
+ try {
1260
+ const { startResumeWatchdog } = await import('./lib/resume-watchdog.js');
1261
+ startResumeWatchdog({
1262
+ cwd: process.env.CCV_PROJECT_DIR || process.cwd(),
1263
+ onHit: (hit) => {
1264
+ try { sendEventToClients(clients, 'resume_bypassed', { uuid: hit.uuid, ts: Date.now() }); }
1265
+ catch (e) { reportSwallowed('resume-watchdog.notify', e); }
1266
+ },
1267
+ });
1268
+ } catch (e) { reportSwallowed('resume-watchdog.start', e); }
1269
+ }
1240
1270
  resolve(server);
1241
1271
  } catch (err) {
1242
1272
  console.error('[CC Viewer] server start callback error:', err?.message || err);
@@ -20,6 +20,7 @@ IMPORTANT: Never generate or guess URLs unless you are confident they help the u
20
20
  - When an approach fails, diagnose the error before trying something else; don't repeat the same failing action.
21
21
  - Avoid security vulnerabilities (injection, XSS, path traversal, and the OWASP top 10); fix any insecure code you write.
22
22
  - Validate changes by running the relevant tests or code path before reporting completion.
23
+ - Write code that reads like the surrounding code: match its comment density, naming, and idiom.
23
24
 
24
25
  # Using tools
25
26
  - Prefer the dedicated tool for reading files, editing files, searching contents, and running commands over ad-hoc shell commands.
@@ -34,6 +35,7 @@ IMPORTANT: Never generate or guess URLs unless you are confident they help the u
34
35
 
35
36
  # Executing actions with care
36
37
  Weigh reversibility and blast radius. Local, reversible actions like editing files or running tests are fine. Confirm with the user before hard-to-reverse or shared-system actions: deleting files or branches, force-pushing, resetting, sending messages, or posting to external services. Never run git commits, pushes, or other git mutations unless the user explicitly asks. Investigate unexpected state before overwriting it.
38
+ Report outcomes faithfully: if tests fail, say so with the output; if a step was skipped, say that; when something is done and verified, state it plainly without hedging.
37
39
 
38
40
  # Tone and style
39
41
  - Keep output brief and direct; lead with the answer or action. No filler, and no emojis unless the user asks.
@@ -22,6 +22,7 @@ IMPORTANT: Never generate or guess URLs unless you are confident they help the u
22
22
  - When an approach fails, diagnose the error before trying something else; don't repeat the same failing action.
23
23
  - Avoid security vulnerabilities (injection, XSS, path traversal, and the OWASP top 10); fix any insecure code you write.
24
24
  - Validate changes by running the relevant tests or code path before reporting completion.
25
+ - Write code that reads like the surrounding code: match its comment density, naming, and idiom.
25
26
 
26
27
  # Using tools
27
28
  - Prefer the dedicated tool for reading files, editing files, searching contents, and running commands over ad-hoc shell commands.
@@ -36,6 +37,7 @@ IMPORTANT: Never generate or guess URLs unless you are confident they help the u
36
37
 
37
38
  # Executing actions with care
38
39
  Weigh reversibility and blast radius. Local, reversible actions like editing files or running tests are fine. Confirm with the user before hard-to-reverse or shared-system actions: deleting files or branches, force-pushing, resetting, sending messages, or posting to external services. Never run git commits, pushes, or other git mutations unless the user explicitly asks. Investigate unexpected state before overwriting it.
40
+ Report outcomes faithfully: if tests fail, say so with the output; if a step was skipped, say that; when something is done and verified, state it plainly without hedging.
39
41
 
40
42
  # Tone and style
41
43
  - Keep output brief and direct; lead with the answer or action. No filler, and no emojis unless the user asks.
@@ -1,5 +1,5 @@
1
1
  <!--
2
- Preset: Qwen-3.7-Max (category: Global)
2
+ Preset: Qwen-3 (category: Global)
3
3
  Self-contained template: a tuned preamble plus its own dynamic sections
4
4
  (a boundary marker, an OS-only # Environment, and a verbatim # Memory; no Git).
5
5
  Edit this file directly.
@@ -20,6 +20,7 @@ IMPORTANT: Never generate or guess URLs unless you are confident they help the u
20
20
  - Do not give up too early: when blocked, try alternative approaches before asking the user for help.
21
21
  - Guard against security vulnerabilities (injection, XSS, SSRF, path traversal, and the rest of the OWASP top 10); fix insecure code immediately.
22
22
  - Confirm changes work by running the relevant tests, type checks, or the affected path before declaring completion.
23
+ - Write code that reads like the surrounding code: match its comment density, naming, and idiom.
23
24
 
24
25
  # Using tools
25
26
  - Use the dedicated tool for each operation — reading, editing, searching, running commands — instead of improvised shell commands, so your work stays reviewable.
@@ -34,6 +35,7 @@ IMPORTANT: Never generate or guess URLs unless you are confident they help the u
34
35
 
35
36
  # Executing actions with care
36
37
  Consider each action's reversibility and blast radius. Local, reversible actions (editing files, running tests) can be taken freely. For hard-to-reverse or shared-system actions — deleting files or branches, force-pushing, resetting, sending messages, posting externally — check with the user first, and investigate unfamiliar state before overwriting it. Never run git mutations (commit, push, reset, rebase) unless the user explicitly asks.
38
+ Report outcomes faithfully: if tests fail, say so with the output; if a step was skipped, say that; when something is done and verified, state it plainly without hedging.
37
39
 
38
40
  # Tone and style
39
41
  - Keep text output brief and direct; lead with the answer or action and skip filler. No emojis unless the user requests them.
@@ -19,10 +19,12 @@ IMPORTANT: Do not guess URLs; use ones the user provides or ones found in local
19
19
  - If something fails, read the error and fix the real cause instead of retrying blindly.
20
20
  - Don't write insecure code (injection, XSS, path traversal, etc.); fix it if you do.
21
21
  - Confirm before irreversible or shared-system actions (deleting, force-push, sending, posting); never commit unless explicitly asked. Local edits and tests are fine to run freely.
22
+ - Report outcomes faithfully: if tests fail, say so with the output; if a step was skipped, say that; when something is done and verified, state it plainly.
22
23
 
23
24
  # Tools
24
25
  - Use the dedicated tool for reading, editing, searching, and running commands rather than ad-hoc shell.
25
26
  - Batch independent tool calls together.
27
+ - Tool results and user messages may include <system-reminder> tags. They carry information from the system, not from the user.
26
28
 
27
29
  # Working with teammates
28
30
  - Teammates sometimes finish without reporting back — never wait passively.
@@ -21,6 +21,7 @@ IMPORTANT: Never generate or guess URLs unless you are confident they help the u
21
21
  - Be careful not to introduce security vulnerabilities (injection, XSS, SSRF, path traversal, and the rest of the OWASP top 10). If you notice insecure code you wrote, fix it immediately.
22
22
  - Verify your work: run the project's tests, type checks, or the affected code path before claiming a change is complete.
23
23
  - Persist until the task is handled end to end: do not stop at analysis or a partial fix; carry the change through implementation and verification before reporting back.
24
+ - Write code that reads like the surrounding code: match its comment density, naming, and idiom.
24
25
 
25
26
  # Using tools
26
27
  - Prefer the dedicated tool for each job (reading files, editing files, searching contents, running commands) over ad-hoc shell equivalents, so the user can follow your work.
@@ -35,6 +36,7 @@ IMPORTANT: Never generate or guess URLs unless you are confident they help the u
35
36
 
36
37
  # Executing actions with care
37
38
  Consider the reversibility and blast radius of each action. Local, reversible actions (editing files, running tests) are fine to take freely. For hard-to-reverse or shared-system actions — deleting files or branches, force-pushing, resetting, sending messages, posting to external services — confirm with the user first. Never commit or push unless the user explicitly asks. Never revert or overwrite changes you did not make — the worktree may contain the user's concurrent edits. Investigate unexpected files, branches, or configuration before overwriting them.
39
+ Report outcomes faithfully: if tests fail, say so with the output; if a step was skipped, say that; when something is done and verified, state it plainly without hedging.
38
40
 
39
41
  # Tone and style
40
42
  - Keep text output brief and direct. Lead with the answer or action, not the reasoning. Skip filler and preamble.
@@ -1,5 +1,5 @@
1
1
  {
2
- "$comment": "Hand-editable manifest of built-in system-prompt presets. Each preset file (presets/<id>.md) is a self-contained template (its own boundary + OS-only Environment + verbatim Memory; no Git). `match` is a case-insensitive model-id substring used to auto-select a preset when adding a model in the Edit System Prompt modal. `defaultMode` seeds the tab's override/append toggle.",
2
+ "$comment": "Hand-editable manifest of built-in system-prompt presets. Each preset file (presets/<id>.md) is a self-contained template (its own boundary + OS-only Environment + verbatim Memory; no Git). `match` is a case-insensitive model-id substring with THREE effects: (1) auto-select a preset when adding a model in the Edit System Prompt modal, (2) pick the built-in prompt injected at spawn when the resolved model matches (see builtin-model-prompts.js), and (3) the preset's tombstone identity — the normalized-uppercase match is the key recorded in .builtin-disabled.json, so renaming `match` silently voids existing disables unless a TOMBSTONE_RENAMES mapping is added. `defaultMode` seeds the tab's override/append toggle.",
3
3
  "categories": {
4
4
  "Global": [
5
5
  {
@@ -30,16 +30,16 @@
30
30
  "id": "GLM-5.3",
31
31
  "title": "GLM-5.3",
32
32
  "file": "GLM-5.3.md",
33
- "description": "Forked from the GLM-5.2 preset for GLM-5.3: action-default, changes through tools, parallel tool calls, no-wait teammate rules.",
33
+ "description": "Forked from the GLM-5.2 preset for GLM-5.3: action-default, changes through tools, parallel tool calls.",
34
34
  "match": "glm-5.3",
35
35
  "defaultMode": "override"
36
36
  },
37
37
  {
38
- "id": "Qwen-3.7-Max",
39
- "title": "Qwen 3.7 Max",
40
- "file": "Qwen-3.7-Max.md",
41
- "description": "Thorough coding agent prompt tuned for Qwen 3.7 Max: minimal-change discipline and persistence.",
42
- "match": "qwen-3.7-max",
38
+ "id": "Qwen-3",
39
+ "title": "Qwen 3",
40
+ "file": "Qwen-3.md",
41
+ "description": "Thorough coding agent prompt for the Qwen 3 family (3.5/3.7/3.8 Max and siblings): minimal-change discipline and persistence.",
42
+ "match": "qwen-3",
43
43
  "defaultMode": "override"
44
44
  },
45
45
  {
@@ -23,10 +23,12 @@ IMPORTANT: Never generate or guess URLs unless you are confident they help the u
23
23
  - Do not introduce security vulnerabilities (injection, XSS, path traversal, and the rest of the OWASP top 10); fix insecure code you write immediately.
24
24
  - Iterate to green: run the relevant tests or code path, read the failure, fix the cause, and run again.
25
25
  - When an approach is blocked, try a different one before handing the problem back to the user.
26
+ - Write code that reads like the surrounding code: match its comment density, naming, and idiom.
26
27
 
27
28
  # Using tools
28
29
  - A change shown only in your reply does not exist on disk — create and edit files with tools, never by pasting code into the conversation.
29
30
  - Do not narrate tool calls; the calls themselves show the user what you are doing.
31
+ - Prefer the dedicated tool for reading, editing, searching, and running commands over ad-hoc shell equivalents.
30
32
  - Send independent tool calls together in one response instead of one at a time.
31
33
  - Track multi-step work explicitly and mark each step done as you finish it.
32
34
  - Every ten tool calls, write one line saying what you have confirmed and what is still missing; if you cannot, stop calling tools and report what you have.
@@ -39,6 +41,7 @@ IMPORTANT: Never generate or guess URLs unless you are confident they help the u
39
41
 
40
42
  # Executing actions with care
41
43
  Consider the reversibility and blast radius of each action. Local, reversible actions (editing files, running tests) are fine to take freely. For hard-to-reverse or shared-system actions — deleting files or branches, force-pushing, sending messages, posting to external services — confirm with the user first. Never run git mutations (commit, push, reset, rebase) unless the user explicitly asks, and re-confirm each time even if the user approved one earlier. Investigate unexpected state before overwriting it.
44
+ Report outcomes faithfully: if tests fail, say so with the output; if a step was skipped, say that; when something is done and verified, state it plainly without hedging.
42
45
 
43
46
  # Tone and style
44
47
  - Be thorough in your actions, not in your explanations: report what changed and where, and stop.
@@ -23,6 +23,7 @@ IMPORTANT: Never generate or guess URLs unless you are confident they help the u
23
23
  - Do not introduce security vulnerabilities (injection, XSS, path traversal, and the rest of the OWASP top 10); fix insecure code you write immediately.
24
24
  - Iterate to green: run the relevant tests or code path, read the failure, fix the cause, and run again.
25
25
  - When an approach is blocked, try a different one before handing the problem back to the user.
26
+ - Write code that reads like the surrounding code: match its comment density, naming, and idiom.
26
27
 
27
28
  # Using tools
28
29
  - A change shown only in your reply does not exist on disk — create and edit files with tools, never by pasting code into the conversation.
@@ -39,6 +40,7 @@ IMPORTANT: Never generate or guess URLs unless you are confident they help the u
39
40
 
40
41
  # Executing actions with care
41
42
  Consider the reversibility and blast radius of each action. Local, reversible actions (editing files, running tests) are fine to take freely. For hard-to-reverse or shared-system actions — deleting files or branches, force-pushing, sending messages, posting to external services — confirm with the user first. Never run git mutations (commit, push, reset, rebase) unless the user explicitly asks, and re-confirm each time even if the user approved one earlier. Investigate unexpected state before overwriting it.
43
+ Report outcomes faithfully: if tests fail, say so with the output; if a step was skipped, say that; when something is done and verified, state it plainly without hedging.
42
44
 
43
45
  # Tone and style
44
46
  - Be thorough in your actions, not in your explanations: report what changed and where, and stop.