@co0ontty/wand 4.82.0 → 4.83.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -9
- package/dist/agent-dispatch.d.ts +3 -2
- package/dist/agent-dispatch.js +19 -3
- package/dist/ai-team-delivery-types.d.ts +40 -0
- package/dist/ai-team-delivery-types.js +1 -0
- package/dist/ai-team-delivery.d.ts +13 -0
- package/dist/ai-team-delivery.js +89 -0
- package/dist/ai-team-employee-binding.d.ts +16 -0
- package/dist/ai-team-employee-binding.js +77 -0
- package/dist/ai-team-prompts.js +5 -0
- package/dist/ai-team-runner.d.ts +9 -2
- package/dist/ai-team-runner.js +52 -7
- package/dist/ai-team-types.d.ts +17 -3
- package/dist/ai-team-types.js +43 -3
- package/dist/build-info.json +3 -3
- package/dist/cli.js +36 -0
- package/dist/config.d.ts +18 -10
- package/dist/config.js +63 -105
- package/dist/decision-client.d.ts +4 -0
- package/dist/decision-client.js +91 -0
- package/dist/decision-runner.d.ts +9 -0
- package/dist/decision-runner.js +36 -0
- package/dist/decision-service.d.ts +46 -0
- package/dist/decision-service.js +230 -0
- package/dist/decision-skill.d.ts +6 -0
- package/dist/decision-skill.js +70 -0
- package/dist/decision-tool.d.ts +21 -0
- package/dist/decision-tool.js +346 -0
- package/dist/decision-types.d.ts +36 -0
- package/dist/decision-types.js +123 -0
- package/dist/decision-worker.py +86 -0
- package/dist/default-employee.d.ts +4 -1
- package/dist/default-employee.js +11 -1
- package/dist/env-utils.js +3 -0
- package/dist/git-quick-commit.js +7 -1
- package/dist/message-truncator.d.ts +1 -1
- package/dist/message-truncator.js +29 -22
- package/dist/models.d.ts +1 -0
- package/dist/models.js +31 -4
- package/dist/process-manager.d.ts +39 -1
- package/dist/process-manager.js +112 -8
- package/dist/provider-catalog.d.ts +47 -0
- package/dist/provider-catalog.js +138 -0
- package/dist/provider-cli-updater.d.ts +1 -1
- package/dist/pty-foreground.d.ts +37 -0
- package/dist/pty-foreground.js +56 -0
- package/dist/pty-turn-activity.d.ts +6 -5
- package/dist/pty-turn-activity.js +6 -5
- package/dist/retention.d.ts +29 -0
- package/dist/retention.js +142 -0
- package/dist/server-ai-team-routes.d.ts +2 -1
- package/dist/server-ai-team-routes.js +27 -9
- package/dist/server-decision-routes.d.ts +10 -0
- package/dist/server-decision-routes.js +52 -0
- package/dist/server-employee-routes.js +9 -1
- package/dist/server-session-routes.d.ts +2 -2
- package/dist/server-session-routes.js +97 -1
- package/dist/server-settings-routes.js +10 -36
- package/dist/server-update-routes.js +2 -1
- package/dist/server-workspace-routes.js +8 -0
- package/dist/server.js +37 -5
- package/dist/session-completion-state.d.ts +26 -0
- package/dist/session-completion-state.js +30 -0
- package/dist/session-completion.d.ts +12 -0
- package/dist/session-completion.js +62 -0
- package/dist/session-provider.d.ts +1 -15
- package/dist/session-provider.js +4 -54
- package/dist/session-registry.d.ts +5 -0
- package/dist/session-registry.js +19 -1
- package/dist/session-transport.js +2 -0
- package/dist/storage.d.ts +12 -0
- package/dist/storage.js +119 -16
- package/dist/structured-client-protocol.js +36 -1
- package/dist/structured-provider-common.js +3 -3
- package/dist/structured-session-manager.d.ts +13 -1
- package/dist/structured-session-manager.js +196 -27
- package/dist/system-employee.js +1 -0
- package/dist/task-types.d.ts +4 -2
- package/dist/tool-preview.d.ts +8 -0
- package/dist/tool-preview.js +158 -0
- package/dist/types.d.ts +35 -2
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/scripts.js +66 -64
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +3 -0
- package/dist/web-ui/embedded-assets.js +3 -3
- package/dist/web-ui/provider-identity.d.ts +7 -13
- package/dist/web-ui/provider-identity.js +8 -71
- package/package.json +3 -2
- package/skills/wand-decision/SKILL.md +61 -0
- package/skills/wand-decision/scripts/decide.mjs +16 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
/** Keep tool payloads out of
|
|
1
|
+
/** Keep full tool payloads out of compact snapshots; retain bounded card previews. */
|
|
2
|
+
import { withToolPreview } from "./tool-preview.js";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { contentHasStructuredImage } from "./structured-content.js";
|
|
5
|
+
import { isDecisionToolCall } from "./decision-tool.js";
|
|
4
6
|
const TRUNCATION_THRESHOLD = 200;
|
|
5
7
|
const SUMMARY_LENGTH = 100;
|
|
6
8
|
/**
|
|
@@ -29,21 +31,23 @@ export const MESSAGE_FIRST_PAINT_BYTES = 1024 * 1024;
|
|
|
29
31
|
* 而不是把整段拉进首屏(窗口的目标是压住首屏载荷,吸附不能反过来把它撑大)。
|
|
30
32
|
*/
|
|
31
33
|
const MAX_BLOCK_SNAP_BACK_BYTES = 128 * 1024;
|
|
34
|
+
/** Decision calls keep their raw names; only this local defaults lookup treats them as always visible. */
|
|
35
|
+
function toolNamesForTransport(content) {
|
|
36
|
+
return new Map(content.flatMap((block) => block.type === "tool_use"
|
|
37
|
+
? [[block.id, isDecisionToolCall(block) ? "__wand_decision" : block.name]] : []));
|
|
38
|
+
}
|
|
32
39
|
/** 每个块在客户端是否「默认收起」(思考块 + 默认收起的工具卡片,与传输截断同一口径)。 */
|
|
33
40
|
export function collapsedBlockFlags(content, cardDefaults) {
|
|
34
|
-
const toolNameById =
|
|
35
|
-
for (const block of content) {
|
|
36
|
-
if (block.type === "tool_use")
|
|
37
|
-
toolNameById.set(block.id, block.name);
|
|
38
|
-
}
|
|
41
|
+
const toolNameById = toolNamesForTransport(content);
|
|
39
42
|
return content.map((block) => {
|
|
40
43
|
switch (block.type) {
|
|
41
44
|
case "thinking":
|
|
42
45
|
return cardDefaults.thinking !== true;
|
|
43
46
|
case "tool_use":
|
|
44
|
-
return isToolDefaultCollapsed(block.name, cardDefaults);
|
|
47
|
+
return !isDecisionToolCall(block) && isToolDefaultCollapsed(block.name, cardDefaults);
|
|
45
48
|
case "tool_result":
|
|
46
|
-
return
|
|
49
|
+
return block.semantic?.kind !== "decision"
|
|
50
|
+
&& isToolDefaultCollapsed(toolNameById.get(block.tool_use_id) ?? "", cardDefaults);
|
|
47
51
|
default:
|
|
48
52
|
return false;
|
|
49
53
|
}
|
|
@@ -75,7 +79,8 @@ function blockTransportBytes(block, cardDefaults, toolNameById) {
|
|
|
75
79
|
case "tool_result": {
|
|
76
80
|
const result = block;
|
|
77
81
|
const raw = getContentString(result.content);
|
|
78
|
-
const collapsed =
|
|
82
|
+
const collapsed = result.semantic?.kind !== "decision"
|
|
83
|
+
&& isToolDefaultCollapsed(toolNameById.get(result.tool_use_id) ?? "", cardDefaults);
|
|
79
84
|
const truncated = collapsed && !result.is_error &&
|
|
80
85
|
!contentHasStructuredImage(result.content) && raw.length > TRUNCATION_THRESHOLD;
|
|
81
86
|
return truncated ? SUMMARY_LENGTH + 4 : raw.length;
|
|
@@ -89,11 +94,7 @@ function blockTransportBytes(block, cardDefaults, toolNameById) {
|
|
|
89
94
|
* (预算判定只需要知道「超了」,不需要精确值)。
|
|
90
95
|
*/
|
|
91
96
|
export function contentTransportBytes(content, cardDefaults, limit = Number.POSITIVE_INFINITY) {
|
|
92
|
-
const toolNameById =
|
|
93
|
-
for (const block of content) {
|
|
94
|
-
if (block.type === "tool_use")
|
|
95
|
-
toolNameById.set(block.id, block.name);
|
|
96
|
-
}
|
|
97
|
+
const toolNameById = toolNamesForTransport(content);
|
|
97
98
|
let total = 0;
|
|
98
99
|
for (const block of content) {
|
|
99
100
|
total += blockTransportBytes(block, cardDefaults, toolNameById) + 32;
|
|
@@ -128,11 +129,7 @@ function cutStartByBudget(content, cardDefaults, visibleBudget, byteBudget) {
|
|
|
128
129
|
if (content.length === 0)
|
|
129
130
|
return 0;
|
|
130
131
|
const collapsed = collapsedBlockFlags(content, cardDefaults);
|
|
131
|
-
const toolNameById =
|
|
132
|
-
for (const block of content) {
|
|
133
|
-
if (block.type === "tool_use")
|
|
134
|
-
toolNameById.set(block.id, block.name);
|
|
135
|
-
}
|
|
132
|
+
const toolNameById = toolNamesForTransport(content);
|
|
136
133
|
let visible = 0;
|
|
137
134
|
let bytes = 0;
|
|
138
135
|
let cut = content.length;
|
|
@@ -413,11 +410,14 @@ export function compactToolMessagesForTransport(messages, knownToolNames) {
|
|
|
413
410
|
const toolNameMap = new Map(knownToolNames);
|
|
414
411
|
const imageToolIds = new Set();
|
|
415
412
|
const independentToolIds = new Set();
|
|
413
|
+
const decisionToolIds = new Set();
|
|
416
414
|
for (const turn of messages) {
|
|
417
415
|
for (const block of turn.content) {
|
|
418
416
|
if (block.type === "tool_use") {
|
|
419
417
|
toolNameMap.set(block.id, block.name);
|
|
420
|
-
if (
|
|
418
|
+
if (isDecisionToolCall(block))
|
|
419
|
+
decisionToolIds.add(block.id);
|
|
420
|
+
if (INDEPENDENT_TOOL_NAMES.has(block.name) || block.semantic || isDecisionToolCall(block) || block.__subagent
|
|
421
421
|
|| toolInputShowsImage(block.input ?? {}))
|
|
422
422
|
independentToolIds.add(block.id);
|
|
423
423
|
}
|
|
@@ -427,8 +427,11 @@ export function compactToolMessagesForTransport(messages, knownToolNames) {
|
|
|
427
427
|
}
|
|
428
428
|
}
|
|
429
429
|
return messages.map((turn) => {
|
|
430
|
-
const truncatedContent = turn.content.map((block) => {
|
|
430
|
+
const truncatedContent = turn.content.map(withToolPreview).map((block) => {
|
|
431
431
|
if (block.type === "tool_use") {
|
|
432
|
+
// 决策卡携带服务端缩略投影(题数/要点/结论),压缩时不能把它降回只有 kind。
|
|
433
|
+
if (decisionToolIds.has(block.id))
|
|
434
|
+
return { ...block, semantic: { ...block.semantic, kind: "decision" }, activity: undefined };
|
|
432
435
|
if (independentToolIds.has(block.id) || imageToolIds.has(block.id))
|
|
433
436
|
return block;
|
|
434
437
|
return {
|
|
@@ -442,6 +445,9 @@ export function compactToolMessagesForTransport(messages, knownToolNames) {
|
|
|
442
445
|
} : undefined,
|
|
443
446
|
};
|
|
444
447
|
}
|
|
448
|
+
if (block.type === "tool_result" && (block.semantic?.kind === "decision" || decisionToolIds.has(block.tool_use_id))) {
|
|
449
|
+
return { ...block, semantic: { ...block.semantic, kind: "decision" } };
|
|
450
|
+
}
|
|
445
451
|
if (block.type === "tool_result" && toolNameMap.has(block.tool_use_id)
|
|
446
452
|
&& !INDEPENDENT_TOOL_NAMES.has(toolNameMap.get(block.tool_use_id))
|
|
447
453
|
&& !independentToolIds.has(block.tool_use_id)
|
|
@@ -459,6 +465,7 @@ function getContentString(content) {
|
|
|
459
465
|
}
|
|
460
466
|
/** Existing transport behavior for clients that have not opted into compact tool cards. */
|
|
461
467
|
export function truncateMessagesForTransport(messages, cardDefaults, streamingTurnIndex = -1) {
|
|
468
|
+
const decisionIds = new Set(messages.flatMap((turn) => turn.content.flatMap((block) => block.type === "tool_use" && isDecisionToolCall(block) ? [block.id] : [])));
|
|
462
469
|
return messages.map((turn, turnIndex) => {
|
|
463
470
|
if (turnIndex === streamingTurnIndex)
|
|
464
471
|
return turn;
|
|
@@ -471,7 +478,7 @@ export function truncateMessagesForTransport(messages, cardDefaults, streamingTu
|
|
|
471
478
|
const truncatedContent = turn.content.map((block) => {
|
|
472
479
|
if (block.type !== "tool_result")
|
|
473
480
|
return block;
|
|
474
|
-
if (block.is_error)
|
|
481
|
+
if (block.is_error || block.semantic?.kind === "decision" || decisionIds.has(block.tool_use_id))
|
|
475
482
|
return block;
|
|
476
483
|
const toolName = toolNameMap.get(block.tool_use_id) ?? "";
|
|
477
484
|
if (!isToolDefaultCollapsed(toolName, cardDefaults))
|
package/dist/models.d.ts
CHANGED
package/dist/models.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
3
6
|
import { promisify } from "node:util";
|
|
4
7
|
import Anthropic from "@anthropic-ai/sdk";
|
|
5
8
|
import { resolveChildEnv } from "./env-utils.js";
|
|
@@ -550,18 +553,42 @@ function unionModelEfforts(models) {
|
|
|
550
553
|
}
|
|
551
554
|
return levels;
|
|
552
555
|
}
|
|
553
|
-
function createOfficialModelsApi(apiKey) {
|
|
554
|
-
const client = new Anthropic({ apiKey });
|
|
556
|
+
function createOfficialModelsApi(apiKey, baseURL) {
|
|
557
|
+
const client = new Anthropic({ apiKey, baseURL: baseURL || undefined });
|
|
555
558
|
return {
|
|
556
559
|
list: () => client.models.list({ limit: 100 }),
|
|
557
560
|
};
|
|
558
561
|
}
|
|
562
|
+
function resolveClaudeAuth(options, env) {
|
|
563
|
+
let apiKey = options.apiKey?.trim() || env.ANTHROPIC_API_KEY?.trim() || env.ANTHROPIC_AUTH_TOKEN?.trim();
|
|
564
|
+
let baseURL = options.baseUrl?.trim() || env.ANTHROPIC_BASE_URL?.trim();
|
|
565
|
+
if (!apiKey) {
|
|
566
|
+
try {
|
|
567
|
+
const settingsPath = path.join(homedir(), ".claude", "settings.json");
|
|
568
|
+
if (existsSync(settingsPath)) {
|
|
569
|
+
const raw = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
570
|
+
const token = raw?.env?.ANTHROPIC_AUTH_TOKEN || raw?.env?.ANTHROPIC_API_KEY;
|
|
571
|
+
if (typeof token === "string" && token.trim()) {
|
|
572
|
+
apiKey = token.trim();
|
|
573
|
+
}
|
|
574
|
+
const base = raw?.env?.ANTHROPIC_BASE_URL;
|
|
575
|
+
if (typeof base === "string" && base.trim() && !baseURL) {
|
|
576
|
+
baseURL = base.trim();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
// ignore
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return { apiKey, baseURL };
|
|
585
|
+
}
|
|
559
586
|
async function listClaudeModelsFromApi(options, env) {
|
|
560
|
-
const apiKey = options
|
|
587
|
+
const { apiKey, baseURL } = resolveClaudeAuth(options, env);
|
|
561
588
|
if (!apiKey)
|
|
562
589
|
return { ok: false };
|
|
563
590
|
try {
|
|
564
|
-
const api = options.modelsApi ?? createOfficialModelsApi(apiKey);
|
|
591
|
+
const api = options.modelsApi ?? createOfficialModelsApi(apiKey, baseURL);
|
|
565
592
|
const models = [];
|
|
566
593
|
for await (const model of api.list()) {
|
|
567
594
|
const id = normalizeClaudeModelId(model?.id);
|
|
@@ -3,6 +3,7 @@ import { WandStorage } from "./storage.js";
|
|
|
3
3
|
import { ExecutionMode, ProcessEventHandler, SessionProvider, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
|
|
4
4
|
import { type PermissionResolution } from "./claude-pty-bridge.js";
|
|
5
5
|
import { type PtyTerminalSnapshot, type PtyHistoryPage } from "./pty-terminal-state.js";
|
|
6
|
+
import { samplePtyForegrounds } from "./pty-foreground.js";
|
|
6
7
|
import { type TerminalHost } from "./terminal-host.js";
|
|
7
8
|
import { type ClaudeHistorySession, type CodexHistorySession, type OpenCodeHistorySession, type QoderHistorySession } from "./provider-history-scanner.js";
|
|
8
9
|
export type { ClaudeHistorySession, CodexHistorySession, OpenCodeHistorySession, QoderHistorySession, } from "./provider-history-scanner.js";
|
|
@@ -26,9 +27,18 @@ export declare class PtyInputDeliveryError extends Error {
|
|
|
26
27
|
* Matches the bridge's own idle-probe delay so both paths make the same call.
|
|
27
28
|
*/
|
|
28
29
|
export declare const PTY_TURN_IDLE_MS = 3000;
|
|
30
|
+
/**
|
|
31
|
+
* 采样间隔:多久向内核问一次「这个 PTY 的前台进程组还是 CLI 吗」。
|
|
32
|
+
* 只查运行中的 PTY 会话,一次 `ps` 拿全部会话,开销与间隔同量级(毫秒)。
|
|
33
|
+
*/
|
|
34
|
+
export declare const PTY_FOREGROUND_SAMPLE_MS = 1000;
|
|
29
35
|
export interface ProcessManagerOptions {
|
|
30
36
|
/** Overrides the non-Claude PTY turn quiet window (tests use a short one). */
|
|
31
37
|
ptyTurnIdleMs?: number;
|
|
38
|
+
/** Overrides the foreground-process-group sampling interval (tests use a short one). */
|
|
39
|
+
ptyForegroundSampleMs?: number;
|
|
40
|
+
/** Overrides the foreground probe itself, so tests can drive it deterministically. */
|
|
41
|
+
samplePtyForegrounds?: typeof samplePtyForegrounds;
|
|
32
42
|
}
|
|
33
43
|
export declare class ProcessManager extends EventEmitter {
|
|
34
44
|
private readonly config;
|
|
@@ -38,8 +48,16 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
38
48
|
private readonly providerHistory;
|
|
39
49
|
/** Quiet window for the non-Claude PTY turn tracker. */
|
|
40
50
|
private readonly ptyTurnIdleMs;
|
|
51
|
+
/** Foreground-process-group sampling interval. */
|
|
52
|
+
private readonly ptyForegroundSampleMs;
|
|
53
|
+
/** Foreground probe; injectable so tests can drive it without a real process table. */
|
|
54
|
+
private readonly probePtyForegrounds;
|
|
41
55
|
/** 24h archive scan timer */
|
|
42
56
|
private archiveTimer;
|
|
57
|
+
/** 前台进程组采样定时器(见 samplePtyForegrounds) */
|
|
58
|
+
private ptyForegroundTimer;
|
|
59
|
+
/** 采样是 async 的,避免慢 ps 重叠成多个进程 */
|
|
60
|
+
private ptyForegroundSampling;
|
|
43
61
|
/** Per-session debounce timers for throttled persist calls */
|
|
44
62
|
private readonly persistDebounceTimers;
|
|
45
63
|
/** Last persisted message state per session — used to skip redundant message writes */
|
|
@@ -53,9 +71,25 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
53
71
|
private disposed;
|
|
54
72
|
private readonly terminalHost;
|
|
55
73
|
constructor(config: WandConfig, storage: WandStorage, configDir?: string, terminalHost?: TerminalHost, options?: ProcessManagerOptions);
|
|
74
|
+
/**
|
|
75
|
+
* 采样每个运行中 PTY 会话的终端前台进程组,得出「CLI 是否还在前台」。
|
|
76
|
+
*
|
|
77
|
+
* 这是 ptyBusy 的实时资格判断,取代一次性启动标记:CLI 退出后又被重新拉起
|
|
78
|
+
* (自更新、手动重跑)时,前台进程组会自然回到 CLI,于是新一轮仍然能被跟踪。
|
|
79
|
+
* 采样不到(不支持的平台、ps 失败、pid 已消失)就保持上次结论,绝不因此清状态。
|
|
80
|
+
*/
|
|
81
|
+
private samplePtyForegrounds;
|
|
56
82
|
private makeRestoredRecord;
|
|
57
83
|
private bindRestoredTerminal;
|
|
58
84
|
private initializeClaudeBridge;
|
|
85
|
+
/**
|
|
86
|
+
* 这一轮的终端是否值得跟踪:这个会话有 provider CLI,且它现在就在前台
|
|
87
|
+
* (实时采样的内核事实);采样不可用时退回一次性的 providerCliActive 标记。
|
|
88
|
+
*
|
|
89
|
+
* 纯 shell 会话(没有 provider)不跟踪:用户在终端里随手敲一条命令不应该
|
|
90
|
+
* 把任务标成「运行中 / 刚完成」。
|
|
91
|
+
*/
|
|
92
|
+
private canTrackPtyTurn;
|
|
59
93
|
/**
|
|
60
94
|
* Open a non-Claude PTY turn and start its quiet-window timer. Claude's bridge
|
|
61
95
|
* owns ptyBusy, so bridged sessions are left untouched.
|
|
@@ -93,7 +127,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
93
127
|
workspaceTaskId?: string;
|
|
94
128
|
interactiveShell?: boolean;
|
|
95
129
|
systemPrompt?: string;
|
|
96
|
-
}): Promise<SessionSnapshot>;
|
|
130
|
+
} & Pick<SessionSnapshot, "employeeId" | "employeeName" | "employeeAvatar">): Promise<SessionSnapshot>;
|
|
97
131
|
list(): SessionSnapshot[];
|
|
98
132
|
/** Return lightweight snapshots for the session list (no output/messages). */
|
|
99
133
|
listSlim(): SessionSnapshot[];
|
|
@@ -123,6 +157,10 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
123
157
|
* the PTY so Claude Code switches on the fly.
|
|
124
158
|
*/
|
|
125
159
|
setSessionModel(id: string, model: string | null): SessionSnapshot;
|
|
160
|
+
/**
|
|
161
|
+
* 归档 / 取消归档一个 PTY 会话:只写标记,不杀进程;保留期清理再按 archivedAt 删除。
|
|
162
|
+
*/
|
|
163
|
+
setSessionArchived(id: string, archived: boolean): SessionSnapshot;
|
|
126
164
|
/**
|
|
127
165
|
* Set the thinking-effort level for a PTY session. Interactive Claude supports
|
|
128
166
|
* this through /effort; off maps to auto, which restores the model default.
|
package/dist/process-manager.js
CHANGED
|
@@ -22,8 +22,10 @@ import { describePtySpawnFailure } from "./ensure-node-pty-helper.js";
|
|
|
22
22
|
import { resolveSessionProvider, resolveSystemAiContext } from "./session-ai-context.js";
|
|
23
23
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
24
24
|
import { inferProviderFromCommand } from "./session-provider.js";
|
|
25
|
+
import { RETENTION_IDLE_MS } from "./retention.js";
|
|
25
26
|
import { PtyTerminalState } from "./pty-terminal-state.js";
|
|
26
27
|
import { isPtySubmitInput } from "./pty-turn-activity.js";
|
|
28
|
+
import { samplePtyForegrounds } from "./pty-foreground.js";
|
|
27
29
|
import { buildPtyShellLaunchPlan, PtyCliExitMarker } from "./pty-shell-launch.js";
|
|
28
30
|
import { InProcessTerminalHost, } from "./terminal-host.js";
|
|
29
31
|
import { ProviderHistoryScanner, } from "./provider-history-scanner.js";
|
|
@@ -480,7 +482,8 @@ function restoreTerminalState(state) {
|
|
|
480
482
|
return terminal;
|
|
481
483
|
}
|
|
482
484
|
const MAX_SESSIONS = 200;
|
|
483
|
-
|
|
485
|
+
// 与会话/任务的统一保留期一致:7 天没活动才自动归档(随后由保留期扫描在 7 天后清理)。
|
|
486
|
+
const ARCHIVE_AFTER_MS = RETENTION_IDLE_MS;
|
|
484
487
|
const CONFIRM_WINDOW_SIZE = 800;
|
|
485
488
|
/**
|
|
486
489
|
* Quiet window for the non-Claude PTY turn tracker: a submitted turn stays busy
|
|
@@ -488,6 +491,11 @@ const CONFIRM_WINDOW_SIZE = 800;
|
|
|
488
491
|
* Matches the bridge's own idle-probe delay so both paths make the same call.
|
|
489
492
|
*/
|
|
490
493
|
export const PTY_TURN_IDLE_MS = 3000;
|
|
494
|
+
/**
|
|
495
|
+
* 采样间隔:多久向内核问一次「这个 PTY 的前台进程组还是 CLI 吗」。
|
|
496
|
+
* 只查运行中的 PTY 会话,一次 `ps` 拿全部会话,开销与间隔同量级(毫秒)。
|
|
497
|
+
*/
|
|
498
|
+
export const PTY_FOREGROUND_SAMPLE_MS = 1000;
|
|
491
499
|
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
492
500
|
function getClaudeProjectDir(cwd) {
|
|
493
501
|
// Claude Code encodes the project dir by replacing every non-alphanumeric
|
|
@@ -546,8 +554,16 @@ export class ProcessManager extends EventEmitter {
|
|
|
546
554
|
providerHistory = new ProviderHistoryScanner();
|
|
547
555
|
/** Quiet window for the non-Claude PTY turn tracker. */
|
|
548
556
|
ptyTurnIdleMs;
|
|
557
|
+
/** Foreground-process-group sampling interval. */
|
|
558
|
+
ptyForegroundSampleMs;
|
|
559
|
+
/** Foreground probe; injectable so tests can drive it without a real process table. */
|
|
560
|
+
probePtyForegrounds;
|
|
549
561
|
/** 24h archive scan timer */
|
|
550
562
|
archiveTimer = null;
|
|
563
|
+
/** 前台进程组采样定时器(见 samplePtyForegrounds) */
|
|
564
|
+
ptyForegroundTimer = null;
|
|
565
|
+
/** 采样是 async 的,避免慢 ps 重叠成多个进程 */
|
|
566
|
+
ptyForegroundSampling = false;
|
|
551
567
|
/** Per-session debounce timers for throttled persist calls */
|
|
552
568
|
persistDebounceTimers = new Map();
|
|
553
569
|
/** Last persisted message state per session — used to skip redundant message writes */
|
|
@@ -565,6 +581,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
565
581
|
this.config = config;
|
|
566
582
|
this.storage = storage;
|
|
567
583
|
this.ptyTurnIdleMs = options.ptyTurnIdleMs ?? PTY_TURN_IDLE_MS;
|
|
584
|
+
this.ptyForegroundSampleMs = options.ptyForegroundSampleMs ?? PTY_FOREGROUND_SAMPLE_MS;
|
|
585
|
+
this.probePtyForegrounds = options.samplePtyForegrounds ?? samplePtyForegrounds;
|
|
568
586
|
this.terminalHost = terminalHost ?? new InProcessTerminalHost();
|
|
569
587
|
this.logger = new SessionLogger(configDir || path.join(process.env.HOME || process.cwd(), ".wand"), config.shortcutLogMaxBytes);
|
|
570
588
|
let startupCodexHistory = null;
|
|
@@ -675,6 +693,49 @@ export class ProcessManager extends EventEmitter {
|
|
|
675
693
|
}
|
|
676
694
|
}, 60 * 1000);
|
|
677
695
|
this.archiveTimer.unref?.();
|
|
696
|
+
this.ptyForegroundTimer = setInterval(() => { void this.samplePtyForegrounds(); }, this.ptyForegroundSampleMs);
|
|
697
|
+
this.ptyForegroundTimer.unref?.();
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* 采样每个运行中 PTY 会话的终端前台进程组,得出「CLI 是否还在前台」。
|
|
701
|
+
*
|
|
702
|
+
* 这是 ptyBusy 的实时资格判断,取代一次性启动标记:CLI 退出后又被重新拉起
|
|
703
|
+
* (自更新、手动重跑)时,前台进程组会自然回到 CLI,于是新一轮仍然能被跟踪。
|
|
704
|
+
* 采样不到(不支持的平台、ps 失败、pid 已消失)就保持上次结论,绝不因此清状态。
|
|
705
|
+
*/
|
|
706
|
+
async samplePtyForegrounds() {
|
|
707
|
+
if (this.disposed || this.ptyForegroundSampling)
|
|
708
|
+
return;
|
|
709
|
+
const candidates = [...this.sessions.values()].filter((record) => (record.status === "running"
|
|
710
|
+
&& record.ptyProcess !== null
|
|
711
|
+
&& !record.ptyBridge
|
|
712
|
+
&& typeof record.processId === "number"
|
|
713
|
+
&& record.processId > 0));
|
|
714
|
+
if (candidates.length === 0)
|
|
715
|
+
return;
|
|
716
|
+
this.ptyForegroundSampling = true;
|
|
717
|
+
try {
|
|
718
|
+
const samples = await this.probePtyForegrounds(candidates.map((record) => record.processId));
|
|
719
|
+
if (!samples || this.disposed)
|
|
720
|
+
return;
|
|
721
|
+
for (const record of candidates) {
|
|
722
|
+
if (this.sessions.get(record.id) !== record)
|
|
723
|
+
continue;
|
|
724
|
+
const next = samples.get(record.processId);
|
|
725
|
+
if (next === undefined || record.ptyForeground === next)
|
|
726
|
+
continue;
|
|
727
|
+
record.ptyForeground = next;
|
|
728
|
+
// CLI 退回提示符:这一轮立刻结束,不必再等静默窗口。
|
|
729
|
+
if (!next)
|
|
730
|
+
this.closePtyTurn(record);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
// 探测失败只意味着这一轮没有新信息,保留上次结论。
|
|
735
|
+
}
|
|
736
|
+
finally {
|
|
737
|
+
this.ptyForegroundSampling = false;
|
|
738
|
+
}
|
|
678
739
|
}
|
|
679
740
|
makeRestoredRecord(snapshot, provider, ptyProcess, terminal, history) {
|
|
680
741
|
return {
|
|
@@ -756,12 +817,25 @@ export class ProcessManager extends EventEmitter {
|
|
|
756
817
|
this.handleBridgeEvent(record, event);
|
|
757
818
|
});
|
|
758
819
|
}
|
|
820
|
+
/**
|
|
821
|
+
* 这一轮的终端是否值得跟踪:这个会话有 provider CLI,且它现在就在前台
|
|
822
|
+
* (实时采样的内核事实);采样不可用时退回一次性的 providerCliActive 标记。
|
|
823
|
+
*
|
|
824
|
+
* 纯 shell 会话(没有 provider)不跟踪:用户在终端里随手敲一条命令不应该
|
|
825
|
+
* 把任务标成「运行中 / 刚完成」。
|
|
826
|
+
*/
|
|
827
|
+
canTrackPtyTurn(record) {
|
|
828
|
+
if (record.ptyForeground !== undefined) {
|
|
829
|
+
return record.ptyForeground && record.provider !== undefined;
|
|
830
|
+
}
|
|
831
|
+
return record.providerCliActive;
|
|
832
|
+
}
|
|
759
833
|
/**
|
|
760
834
|
* Open a non-Claude PTY turn and start its quiet-window timer. Claude's bridge
|
|
761
835
|
* owns ptyBusy, so bridged sessions are left untouched.
|
|
762
836
|
*/
|
|
763
837
|
openPtyTurn(record) {
|
|
764
|
-
if (record.ptyBridge || !record
|
|
838
|
+
if (record.ptyBridge || !this.canTrackPtyTurn(record) || record.status !== "running")
|
|
765
839
|
return;
|
|
766
840
|
if (record.ptyBusy !== true) {
|
|
767
841
|
record.ptyBusy = true;
|
|
@@ -771,7 +845,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
771
845
|
}
|
|
772
846
|
/** Extend the quiet window because the CLI is still producing output. */
|
|
773
847
|
refreshPtyTurn(record) {
|
|
774
|
-
if (record.ptyBridge || !record
|
|
848
|
+
if (record.ptyBridge || !this.canTrackPtyTurn(record))
|
|
775
849
|
return;
|
|
776
850
|
if (record.ptyTurnTimer)
|
|
777
851
|
clearTimeout(record.ptyTurnTimer);
|
|
@@ -937,10 +1011,16 @@ export class ProcessManager extends EventEmitter {
|
|
|
937
1011
|
if (chunk && current.providerCliActive && current.autoApprovePermissions && !current.ptyBridge && current.provider === "claude") {
|
|
938
1012
|
this.autoConfirmWithRecord(current, chunk, child);
|
|
939
1013
|
}
|
|
940
|
-
// Non-Claude CLIs have no parsed turn boundary
|
|
941
|
-
// open
|
|
942
|
-
|
|
943
|
-
|
|
1014
|
+
// Non-Claude CLIs have no parsed turn boundary: output keeps an open turn
|
|
1015
|
+
// open, and output while the CLI is in the foreground opens one — the CLI may
|
|
1016
|
+
// have (re)started in this terminal long after the one-shot launch marker was
|
|
1017
|
+
// consumed (see pty-turn-activity.ts / pty-foreground.ts).
|
|
1018
|
+
if (chunk && this.canTrackPtyTurn(current)) {
|
|
1019
|
+
if (current.ptyBusy === true)
|
|
1020
|
+
this.refreshPtyTurn(current);
|
|
1021
|
+
else
|
|
1022
|
+
this.openPtyTurn(current);
|
|
1023
|
+
}
|
|
944
1024
|
if (chunk)
|
|
945
1025
|
onVisibleChunk?.(chunk);
|
|
946
1026
|
if (boundary.exitCode !== null)
|
|
@@ -963,6 +1043,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
963
1043
|
clearInterval(this.archiveTimer);
|
|
964
1044
|
this.archiveTimer = null;
|
|
965
1045
|
}
|
|
1046
|
+
if (this.ptyForegroundTimer) {
|
|
1047
|
+
clearInterval(this.ptyForegroundTimer);
|
|
1048
|
+
this.ptyForegroundTimer = null;
|
|
1049
|
+
}
|
|
966
1050
|
const pendingPersistIds = new Set(this.persistDebounceTimers.keys());
|
|
967
1051
|
for (const timer of this.persistDebounceTimers.values())
|
|
968
1052
|
clearTimeout(timer);
|
|
@@ -1061,6 +1145,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1061
1145
|
let inheritedAutomationId;
|
|
1062
1146
|
let inheritedWorkspaceId;
|
|
1063
1147
|
let inheritedWorkspaceTaskId;
|
|
1148
|
+
let inheritedRole;
|
|
1064
1149
|
if (opts?.reuseId) {
|
|
1065
1150
|
const oldRecord = this.sessions.get(id);
|
|
1066
1151
|
if (oldRecord) {
|
|
@@ -1069,6 +1154,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1069
1154
|
inheritedAutomationId = oldRecord.automationId;
|
|
1070
1155
|
inheritedWorkspaceId = oldRecord.workspaceId;
|
|
1071
1156
|
inheritedWorkspaceTaskId = oldRecord.workspaceTaskId;
|
|
1157
|
+
inheritedRole = oldRecord;
|
|
1072
1158
|
this.cleanupRecord(oldRecord);
|
|
1073
1159
|
this.sessions.delete(id);
|
|
1074
1160
|
}
|
|
@@ -1079,6 +1165,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1079
1165
|
inheritedAutomationId = stored?.automationId;
|
|
1080
1166
|
inheritedWorkspaceId = stored?.workspaceId;
|
|
1081
1167
|
inheritedWorkspaceTaskId = stored?.workspaceTaskId;
|
|
1168
|
+
inheritedRole = stored ?? undefined;
|
|
1082
1169
|
}
|
|
1083
1170
|
this.terminalHost.forget(id);
|
|
1084
1171
|
}
|
|
@@ -1095,7 +1182,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1095
1182
|
const initialThinkingEffort = normalizeThinkingEffort(opts?.thinkingEffort);
|
|
1096
1183
|
let processedCommand = this.processCommandForMode(command, effectiveMode, provider, selectedModel, initialThinkingEffort);
|
|
1097
1184
|
// 会话级系统提示:provider 有系统提示开关就走开关(PTY 与结构化行为一致),没有就并进首条输入。
|
|
1098
|
-
const systemPrompt = opts?.systemPrompt?.trim() || undefined;
|
|
1185
|
+
const systemPrompt = (opts?.systemPrompt ?? inheritedRole?.systemPrompt)?.trim() || undefined;
|
|
1099
1186
|
const initialInputText = initialInput && systemPrompt && !systemPromptFlag(provider)
|
|
1100
1187
|
? composeSystemFallback(systemPrompt, initialInput)
|
|
1101
1188
|
: initialInput;
|
|
@@ -1136,6 +1223,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
1136
1223
|
sessionSource: opts?.sessionSource ?? inheritedSessionSource ?? "interactive",
|
|
1137
1224
|
automationId: opts?.automationId ?? inheritedAutomationId,
|
|
1138
1225
|
systemPrompt: systemPrompt ?? null,
|
|
1226
|
+
employeeId: opts?.employeeId ?? inheritedRole?.employeeId,
|
|
1227
|
+
employeeName: opts?.employeeName ?? inheritedRole?.employeeName,
|
|
1228
|
+
employeeAvatar: opts?.employeeAvatar ?? inheritedRole?.employeeAvatar,
|
|
1139
1229
|
workspaceId: opts?.workspaceId ?? inheritedWorkspaceId,
|
|
1140
1230
|
workspaceTaskId: opts?.workspaceTaskId ?? inheritedWorkspaceTaskId,
|
|
1141
1231
|
provider,
|
|
@@ -1549,6 +1639,17 @@ export class ProcessManager extends EventEmitter {
|
|
|
1549
1639
|
this.emitEvent({ type: "status", sessionId: id, data: { selectedModel: normalized } });
|
|
1550
1640
|
return this.snapshot(record);
|
|
1551
1641
|
}
|
|
1642
|
+
/**
|
|
1643
|
+
* 归档 / 取消归档一个 PTY 会话:只写标记,不杀进程;保留期清理再按 archivedAt 删除。
|
|
1644
|
+
*/
|
|
1645
|
+
setSessionArchived(id, archived) {
|
|
1646
|
+
const record = this.mustGet(id);
|
|
1647
|
+
record.archived = archived;
|
|
1648
|
+
record.archivedAt = archived ? new Date().toISOString() : null;
|
|
1649
|
+
this.persist(record);
|
|
1650
|
+
this.emitEvent({ type: "status", sessionId: id, data: { archived, archivedAt: record.archivedAt } });
|
|
1651
|
+
return this.snapshot(record);
|
|
1652
|
+
}
|
|
1552
1653
|
/**
|
|
1553
1654
|
* Set the thinking-effort level for a PTY session. Interactive Claude supports
|
|
1554
1655
|
* this through /effort; off maps to auto, which restores the model default.
|
|
@@ -1972,6 +2073,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
1972
2073
|
automationId: record.automationId,
|
|
1973
2074
|
// 会话级系统提示要跟着快照回库(persist 就是走 snapshot),否则重启后 resume 会丢掉角色与规则。
|
|
1974
2075
|
systemPrompt: record.systemPrompt ?? null,
|
|
2076
|
+
employeeId: record.employeeId,
|
|
2077
|
+
employeeName: record.employeeName,
|
|
2078
|
+
employeeAvatar: record.employeeAvatar,
|
|
1975
2079
|
provider: record.provider,
|
|
1976
2080
|
providerCliActive: record.providerCliActive,
|
|
1977
2081
|
providerCliExitCode: record.providerCliExitCode,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider 的唯一真源:受支持的 CLI 列表、展示名、别名表与推断规则。
|
|
3
|
+
*
|
|
4
|
+
* 这个模块必须保持"纯数据 + 纯函数",不能 import 任何 `node:*`:
|
|
5
|
+
* 服务端(`src/session-provider.ts`)与浏览器 bundle(`src/web-ui/provider-identity.ts`)
|
|
6
|
+
* 都直接复用它,避免同一个 provider 概念散落在两处各写一份。
|
|
7
|
+
*/
|
|
8
|
+
/** 全部受支持的会话 provider;顺序即 UI / 派发选择器里的展示顺序。 */
|
|
9
|
+
export declare const SESSION_PROVIDERS: readonly ["claude", "codex", "opencode", "grok", "qoder", "pi", "gemini"];
|
|
10
|
+
export type SessionProvider = (typeof SESSION_PROVIDERS)[number];
|
|
11
|
+
/**
|
|
12
|
+
* 拥有原生思考档位(`provider:level`)的 provider。
|
|
13
|
+
* Gemini CLI 没有档位开关,所以不出现在这里:`gemini:max` 这种值不应该被当成合法档位保留。
|
|
14
|
+
*/
|
|
15
|
+
export declare const NATIVE_THINKING_EFFORT_PROVIDERS: readonly ["claude", "codex", "opencode", "grok", "qoder", "pi"];
|
|
16
|
+
/** 原生思考档位的 `provider:level` 形状;各端必须共用同一张 provider 名单。 */
|
|
17
|
+
export declare const NATIVE_THINKING_EFFORT_PATTERN: string;
|
|
18
|
+
/** 判断字符串是否为 `provider:level` 形状的原生思考档。 */
|
|
19
|
+
export declare function isNativeThinkingEffort(value: unknown): boolean;
|
|
20
|
+
/** 运行时守卫:把配置、数据库、请求体里的任意值收敛成 SessionProvider。 */
|
|
21
|
+
export declare function isSessionProvider(value: unknown): value is SessionProvider;
|
|
22
|
+
/** provider 对应的 CLI 可执行文件名(Qoder 的 CLI 叫 qodercli)。 */
|
|
23
|
+
export declare function providerCliCommand(provider: SessionProvider): string;
|
|
24
|
+
/** provider 的展示名。缺省不认识的字符串一律回落到 "AI"。 */
|
|
25
|
+
export declare const PROVIDER_LABELS: Readonly<Record<SessionProvider, string>>;
|
|
26
|
+
/**
|
|
27
|
+
* provider id 别名表:既接受用户/客户端可能写出的别名(`anthropic`、`open-code`),
|
|
28
|
+
* 也接受历史 runner 名(`claude-cli-print`、`codex-cli-exec` …),
|
|
29
|
+
* 供 `normalizeProviderId` 与 `inferProviderFromRunner` 共用。
|
|
30
|
+
*/
|
|
31
|
+
export declare const PROVIDER_ALIASES: Readonly<Record<string, SessionProvider>>;
|
|
32
|
+
/** 归一化一个已持久化的 provider id 或 runner/别名;无法识别时返回 null。 */
|
|
33
|
+
export declare function normalizeProviderId(value: unknown): SessionProvider | null;
|
|
34
|
+
/** 从 runner 名推断 provider;`pty` 等与 provider 无关的 runner 返回 undefined。 */
|
|
35
|
+
export declare function inferProviderFromRunner(runner: unknown): SessionProvider | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* 从启动命令推断 provider;识别的是命令行里真正的可执行名。
|
|
38
|
+
*
|
|
39
|
+
* - `/opt/homebrew/bin/claude --resume abc` → claude
|
|
40
|
+
* - `open-code run` / `qodercli --print` → opencode / qoder
|
|
41
|
+
* - `npx claude` / `pnpm dlx codex` → 看包管理器后面的真实 CLI
|
|
42
|
+
*
|
|
43
|
+
* 参数与父目录名不能冒充别的 provider(`claude -p codex` 仍是 claude)。
|
|
44
|
+
*/
|
|
45
|
+
export declare function inferProviderFromCommand(command: unknown): SessionProvider | undefined;
|
|
46
|
+
/** 每个 provider 在自由文本里可能出现的写法,用于把命令名收敛成可读标签。 */
|
|
47
|
+
export declare function providerDisplayName(value: unknown): string;
|