lark-remote 0.5.13 → 0.5.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/agent-kinds.d.ts +27 -0
- package/dist/agent-kinds.js +38 -0
- package/dist/bridge/approval-actions.d.ts +95 -0
- package/dist/bridge/approval-actions.js +104 -0
- package/dist/bridge/final-usage.d.ts +8 -17
- package/dist/bridge/final-usage.js +1 -18
- package/dist/bridge/index.d.ts +46 -39
- package/dist/bridge/index.js +201 -229
- package/dist/bridge/queue-manager.d.ts +25 -1
- package/dist/bridge/queue-manager.js +179 -160
- package/dist/bridge/run-approval.d.ts +2 -1
- package/dist/bridge/run-usage.d.ts +16 -30
- package/dist/bridge/run-usage.js +41 -50
- package/dist/bridge/session-writeback.d.ts +3 -2
- package/dist/card/approval-render.d.ts +3 -2
- package/dist/card/approval-render.js +3 -2
- package/dist/{router → card}/card-helpers.d.ts +7 -7
- package/dist/{router → card}/card-helpers.js +8 -8
- package/dist/card/card-session.d.ts +4 -2
- package/dist/card/card-session.js +8 -6
- package/dist/card/card-shared.d.ts +1 -1
- package/dist/card/card-shared.js +1 -1
- package/dist/card/final-answer-overflow.d.ts +0 -6
- package/dist/card/final-answer-overflow.js +1 -1
- package/dist/card/run-card-session.d.ts +2 -2
- package/dist/card/run-card-session.js +2 -2
- package/dist/card/run-renderer.js +7 -22
- package/dist/card/run-state.d.ts +28 -50
- package/dist/card/run-state.js +23 -0
- package/dist/card/usage-format.d.ts +30 -0
- package/dist/{router → card}/usage-format.js +3 -10
- package/dist/clone.js +6 -1
- package/dist/config/index.d.ts +16 -20
- package/dist/config/index.js +0 -16
- package/dist/config/pi-config.js +6 -4
- package/dist/connector/index.d.ts +2 -4
- package/dist/connector/index.js +7 -11
- package/dist/{runner/dsh → dsh}/auth.js +1 -1
- package/dist/dsh/protocol.d.ts +9 -0
- package/dist/dsh/protocol.js +61 -0
- package/dist/index.js +10 -5
- package/dist/model-catalog.d.ts +14 -0
- package/dist/model-catalog.js +25 -0
- package/dist/platform/path.d.ts +9 -0
- package/dist/platform/path.js +18 -0
- package/dist/router/active-run-usage.d.ts +4 -0
- package/dist/router/active-run-usage.js +12 -0
- package/dist/router/commands/ls.js +1 -1
- package/dist/router/commands/order.js +1 -1
- package/dist/router/commands/resume.d.ts +5 -4
- package/dist/router/commands/resume.js +4 -4
- package/dist/router/commands/ws.js +1 -1
- package/dist/router/config/claude.js +2 -1
- package/dist/router/config/common/render.d.ts +5 -1
- package/dist/router/config/common/render.js +6 -13
- package/dist/router/config/dsh.d.ts +1 -1
- package/dist/router/config/index.d.ts +1 -1
- package/dist/router/config/types.d.ts +11 -3
- package/dist/router/index.d.ts +41 -11
- package/dist/router/index.js +165 -226
- package/dist/runner/agent-choices-common.d.ts +1 -1
- package/dist/runner/claude/runner.d.ts +20 -2
- package/dist/runner/claude/runner.js +2 -15
- package/dist/runner/codex/app-server/runner.d.ts +3 -1
- package/dist/runner/codex/app-server/translator.d.ts +11 -0
- package/dist/runner/codex/app-server/translator.js +56 -149
- package/dist/runner/common/acp/base-acp-runner.d.ts +3 -1
- package/dist/runner/common/connection-based-runner.d.ts +3 -1
- package/dist/runner/common/jsonrpc/transport.js +17 -7
- package/dist/runner/dsh/client.d.ts +1 -4
- package/dist/runner/dsh/client.js +2 -55
- package/dist/runner/dsh/daemon.js +9 -2
- package/dist/runner/dsh/exit-cancel.js +1 -1
- package/dist/runner/dsh/mux-stream.js +21 -2
- package/dist/runner/dsh/runner.d.ts +21 -1
- package/dist/runner/dsh/runner.js +76 -73
- package/dist/runner/dsh/translator.d.ts +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +22 -1
- package/dist/runner/kimi/acp/runner.js +83 -60
- package/dist/runner/opencode/acp/error-monitor.d.ts +11 -0
- package/dist/runner/opencode/acp/error-monitor.js +31 -2
- package/dist/runner/opencode/acp/runner.d.ts +3 -1
- package/dist/runner/pi/rpc/runner.d.ts +2 -1
- package/dist/runner/probe.d.ts +1 -1
- package/dist/runner/probe.js +10 -9
- package/dist/runner/registry.d.ts +2 -1
- package/dist/runner/types.d.ts +20 -154
- package/dist/runner/types.js +4 -24
- package/dist/session/claude/session-index.d.ts +11 -4
- package/dist/session/claude/session-index.js +30 -8
- package/dist/session/claude/session-reader.d.ts +1 -1
- package/dist/session/claude/sessions.d.ts +2 -2
- package/dist/session/claude/sessions.js +41 -26
- package/dist/session/codex/rollout-reader.d.ts +1 -1
- package/dist/session/codex/rollout-reader.js +14 -13
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +4 -2
- package/dist/session/common/jsonl.js +7 -1
- package/dist/session/common/pagination.d.ts +2 -0
- package/dist/session/common/pagination.js +1 -1
- package/dist/session/common/recency.d.ts +6 -0
- package/dist/session/common/recency.js +7 -5
- package/dist/session/common/two-pass.d.ts +1 -1
- package/dist/session/common/usage-accumulator.d.ts +1 -1
- package/dist/session/dsh/sessions.d.ts +1 -1
- package/dist/session/dsh/sessions.js +37 -12
- package/dist/session/kimi/sessions.d.ts +1 -1
- package/dist/session/kimi/sessions.js +35 -8
- package/dist/session/opencode/sessions.d.ts +1 -1
- package/dist/session/opencode/sessions.js +51 -47
- package/dist/session/pi/sessions.d.ts +1 -1
- package/dist/session/pi/sessions.js +20 -7
- package/dist/session/registry.d.ts +2 -1
- package/dist/session/session-store.d.ts +1 -2
- package/dist/session/session-store.js +14 -7
- package/dist/session-contract.d.ts +178 -0
- package/dist/session-contract.js +9 -0
- package/package.json +1 -1
- package/dist/router/usage-format.d.ts +0 -60
- /package/dist/{runner/dsh → dsh}/auth.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/home.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/home.js +0 -0
- /package/dist/{runner/dsh → dsh}/types.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/types.js +0 -0
package/dist/runner/probe.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* with no child process at all.
|
|
19
19
|
*/
|
|
20
20
|
import { resolveExecutable } from '../platform/command.js';
|
|
21
|
+
import { TtlCache } from '../common/ttl-cache.js';
|
|
21
22
|
/** Map from AgentKind to its CLI binary name. */
|
|
22
23
|
const BINARY_MAP = {
|
|
23
24
|
claude: 'claude',
|
|
@@ -30,8 +31,11 @@ const BINARY_MAP = {
|
|
|
30
31
|
dsh: '',
|
|
31
32
|
};
|
|
32
33
|
const CACHE_TTL_MS = 5 * 60_000;
|
|
33
|
-
/**
|
|
34
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Module-level cache, survives across calls within the process. 键集就是
|
|
36
|
+
* AgentKind(有界),因此不设条目上限;共享 `TtlCache` 与 session readers 同语义。
|
|
37
|
+
*/
|
|
38
|
+
const cache = new TtlCache(CACHE_TTL_MS);
|
|
35
39
|
/**
|
|
36
40
|
* Probe a single agent's availability (pure PATH lookup; dsh has no CLI
|
|
37
41
|
* binary and is always unavailable here).
|
|
@@ -49,10 +53,10 @@ async function probeOne(kind) {
|
|
|
49
53
|
*/
|
|
50
54
|
export async function probeAgentAvailability(kind) {
|
|
51
55
|
const cached = cache.get(kind);
|
|
52
|
-
if (cached
|
|
53
|
-
return cached
|
|
56
|
+
if (cached !== undefined)
|
|
57
|
+
return cached;
|
|
54
58
|
const ok = await probeOne(kind);
|
|
55
|
-
cache.set(kind,
|
|
59
|
+
cache.set(kind, ok);
|
|
56
60
|
return ok;
|
|
57
61
|
}
|
|
58
62
|
/**
|
|
@@ -76,10 +80,7 @@ export async function probeAllAgents() {
|
|
|
76
80
|
* Used by synchronous code paths (e.g. buildConfigCard) that cannot await.
|
|
77
81
|
*/
|
|
78
82
|
export function getCachedAvailability(kind) {
|
|
79
|
-
|
|
80
|
-
if (!cached || Date.now() - cached.ts >= CACHE_TTL_MS)
|
|
81
|
-
return undefined;
|
|
82
|
-
return cached.ok;
|
|
83
|
+
return cache.get(kind);
|
|
83
84
|
}
|
|
84
85
|
/** Clear all cache entries. Exported for test use only (cache auto-expires via TTL). */
|
|
85
86
|
export function _clearCacheForTest() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AgentKind
|
|
1
|
+
import type { AgentKind } from '../agent-kinds.js';
|
|
2
|
+
import type { Runner } from './index.js';
|
|
2
3
|
/**
|
|
3
4
|
* Registry of agent factories, keyed by `AgentKind`. `index.ts` registers one
|
|
4
5
|
* factory per available agent at startup; `Bridge.getRunner(cwd)` looks
|
package/dist/runner/types.d.ts
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 本文件包含所有 Agent 相关的类型定义,从 runner/index.ts 提取。
|
|
5
5
|
* 所有下游模块应从本文件 import 类型,而非直接依赖实现。
|
|
6
|
+
* (例外:`AgentKind` 住在 `src/agent-kinds.ts`,见文件底部说明。)
|
|
6
7
|
*/
|
|
8
|
+
import type { AgentKind } from '../agent-kinds.js';
|
|
9
|
+
import type { AgentSessionReader } from '../session-contract.js';
|
|
7
10
|
export interface SystemInitEvent {
|
|
8
11
|
type: 'system';
|
|
9
12
|
subtype: 'init';
|
|
@@ -552,18 +555,7 @@ export interface Runner {
|
|
|
552
555
|
*/
|
|
553
556
|
unregisterExitHandlers?(): void;
|
|
554
557
|
}
|
|
555
|
-
|
|
556
|
-
* Agent 名单单源(运行时数组 + 类型同源,二者不可能漂移)。
|
|
557
|
-
*
|
|
558
|
-
* 历史上这份名单被手抄成多份(router 的 VALID_AGENTS、bridge 的 resume.compact
|
|
559
|
-
* 校验、各 agent 的 config builder…),漂移过:dsh 加入后 bridge 的
|
|
560
|
-
* resume.compact 仍只有 5 个,带 dsh 的 resume 卡片会静默退回 defaultAgent。
|
|
561
|
-
* 新增 agent 只改这里。
|
|
562
|
-
*/
|
|
563
|
-
export declare const AGENT_KINDS: readonly ["claude", "codex", "opencode", "pi", "kimi", "dsh"];
|
|
564
|
-
export type AgentKind = (typeof AGENT_KINDS)[number];
|
|
565
|
-
/** 用户可见 agent 显示名单源:启动注册与无 registry 的回退路径共用。 */
|
|
566
|
-
export declare const AGENT_DISPLAY_NAMES: Record<AgentKind, string>;
|
|
558
|
+
export { AGENT_DISPLAY_NAMES } from '../agent-kinds.js';
|
|
567
559
|
/**
|
|
568
560
|
* Compact 能力声明(运行时门控的能力清单,编译期强制完整)。
|
|
569
561
|
*
|
|
@@ -579,147 +571,6 @@ export declare const AGENT_DISPLAY_NAMES: Record<AgentKind, string>;
|
|
|
579
571
|
export declare const COMPACT_CAPABILITY: Readonly<Record<AgentKind, boolean>>;
|
|
580
572
|
/** 该 agent 是否声明支持 compact(渲染门控与执行门控共用的唯一判据)。 */
|
|
581
573
|
export declare function supportsCompact(kind: AgentKind): boolean;
|
|
582
|
-
/** Unified session descriptor returned by every AgentSessionReader. */
|
|
583
|
-
export interface AgentSession {
|
|
584
|
-
sessionId: string;
|
|
585
|
-
summary: string;
|
|
586
|
-
mtime: number;
|
|
587
|
-
/** DSH:session 创建时固定的 preset;其他 agent 无此概念(undefined)。 */
|
|
588
|
-
agentPreset?: string;
|
|
589
|
-
/**
|
|
590
|
-
* 服务端投影已有的会话标题(DSH `projections.values.title`)。
|
|
591
|
-
*
|
|
592
|
-
* 与 `summary` 的区别:`summary` 是「首条用户输入」的兜底口径(claude /
|
|
593
|
-
* codex / pi / kimi 无标题字段),`title` 是 agent 自己起的标题。列表卡
|
|
594
|
-
* 用它决定标签文案(标题 / 会话摘要),缺省时回退 `summary`。
|
|
595
|
-
*/
|
|
596
|
-
title?: string;
|
|
597
|
-
/**
|
|
598
|
-
* 最后一轮用户输入(服务端投影,单行预览)。
|
|
599
|
-
*
|
|
600
|
-
* 只给「列表响应里已顺带返回」的 reader 用(DSH 的 `turnOutline` 末条),
|
|
601
|
-
* 目的是让列表卡不必为每行做一次全量内容读取。
|
|
602
|
-
*/
|
|
603
|
-
lastUserMessage?: string;
|
|
604
|
-
/** 最后一轮助手输出(服务端投影已截断,单行预览)。 */
|
|
605
|
-
lastAssistantMessage?: string;
|
|
606
|
-
}
|
|
607
|
-
/** A single renderable event from session history. */
|
|
608
|
-
export interface AgentSessionContentEvent {
|
|
609
|
-
type: string;
|
|
610
|
-
/** 正文(展开后的面板内容)。 */
|
|
611
|
-
content: string;
|
|
612
|
-
/**
|
|
613
|
-
* 面板标题(可选,覆盖 reader 的默认标签)。
|
|
614
|
-
*
|
|
615
|
-
* 折叠面板默认只显示标题:工具调用这种"标题即全部信息"的事件必须把
|
|
616
|
-
* `bash — npm test` 这类摘要放进标题,否则折叠态只剩 `🔧 工具调用`。
|
|
617
|
-
*/
|
|
618
|
-
title?: string;
|
|
619
|
-
timestamp?: string;
|
|
620
|
-
}
|
|
621
|
-
export interface AgentSessionUsage {
|
|
622
|
-
inputTokens: number;
|
|
623
|
-
outputTokens: number;
|
|
624
|
-
contextLength: number;
|
|
625
|
-
/** Agent 上报的当前模型 context window 上限(codex token_count.info.model_context_window)。
|
|
626
|
-
* 仅 codex 提供;缺省时卡片只显示绝对量、不显示百分比。 */
|
|
627
|
-
contextLimit?: number;
|
|
628
|
-
/** Claude auto-compact 次数(数 compact_boundary 事件);codex/opencode 无此概念,留 undefined。 */
|
|
629
|
-
compactCount?: number;
|
|
630
|
-
/** 压缩前上下文水位(codex compact 后从会话 jsonl 读取,仅会话以压缩收尾时有值)。 */
|
|
631
|
-
compactPreContextLength?: number;
|
|
632
|
-
/** 从缓存读取的 token 数(节省的费用)。 */
|
|
633
|
-
cacheReadTokens?: number;
|
|
634
|
-
/** 新建缓存的 token 数。 */
|
|
635
|
-
cacheCreationTokens?: number;
|
|
636
|
-
/** Agent 声明的总量;展示用 max(total, 分项和)。 */
|
|
637
|
-
totalTokens?: number;
|
|
638
|
-
/** 会话累计 total token(所有 run 之和,含本次)。用于 Run 卡片"累计"展示。 */
|
|
639
|
-
cumulativeTotalTokens?: number;
|
|
640
|
-
/** 会话累计 input token(所有 run 之和,含本次)。用于 Run 卡片"累计"展示。 */
|
|
641
|
-
cumulativeInputTokens?: number;
|
|
642
|
-
/** 会话累计 output token(所有 run 之和,含本次)。 */
|
|
643
|
-
cumulativeOutputTokens?: number;
|
|
644
|
-
/** 会话累计 cache read token(所有 run 之和)。用于 Run 卡片展示累计缓存命中。 */
|
|
645
|
-
cumulativeCacheReadTokens?: number;
|
|
646
|
-
/** 会话累计 cache creation token(所有 run 之和)。 */
|
|
647
|
-
cumulativeCacheCreationTokens?: number;
|
|
648
|
-
}
|
|
649
|
-
/** Unified session content payload returned by every AgentSessionReader. */
|
|
650
|
-
export interface SessionContent {
|
|
651
|
-
events: AgentSessionContentEvent[];
|
|
652
|
-
usage?: AgentSessionUsage;
|
|
653
|
-
aiTitle?: string;
|
|
654
|
-
recap?: string;
|
|
655
|
-
displayTitle?: string;
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* Lightweight session summary used by `/resume` list prefetch.
|
|
659
|
-
*
|
|
660
|
-
* Carries only the display fields (title / recap / usage) — **no `events`
|
|
661
|
-
* array**. Readers that can extract these without constructing the full event
|
|
662
|
-
* list (e.g. codex, which would otherwise JSON.parse every response_item and
|
|
663
|
-
* build an `events[]` that the prefetch immediately discards) implement the
|
|
664
|
-
* optional `readSessionSummary` on `AgentSessionReader`.
|
|
665
|
-
*/
|
|
666
|
-
export interface SessionSummary {
|
|
667
|
-
aiTitle?: string;
|
|
668
|
-
recap?: string;
|
|
669
|
-
displayTitle?: string;
|
|
670
|
-
usage?: AgentSessionUsage;
|
|
671
|
-
}
|
|
672
|
-
/**
|
|
673
|
-
* Reads an agent's session history. Each agent implements its own (Claude reads
|
|
674
|
-
* `~/.claude/projects/`, Codex calls `thread/list`, OpenCode calls `GET /session`).
|
|
675
|
-
*
|
|
676
|
-
* Q-C decision: `projectsDir` and other agent-specific path config live on the
|
|
677
|
-
* concrete reader's constructor, NOT on these method opts. The router/bridge
|
|
678
|
-
* depend only on this interface and never pass path config.
|
|
679
|
-
*/
|
|
680
|
-
export interface AgentSessionReader {
|
|
681
|
-
/**
|
|
682
|
-
* List sessions for a cwd, newest first by mtime.
|
|
683
|
-
*
|
|
684
|
-
* `sessions` is the page `[offset, offset+limit)` of the full set ordered
|
|
685
|
-
* by mtime desc, with same-mtime ties broken by a deterministic secondary
|
|
686
|
-
* key (sessionId/file path) so ordering is stable across calls. When
|
|
687
|
-
* `limit` is omitted, codex/pi/kimi return their default-sized page
|
|
688
|
-
* (default 20); claude/opencode return the full remaining set from
|
|
689
|
-
* `offset`. `total` is ALWAYS the size of the full cwd-matched set before
|
|
690
|
-
* pagination — never the truncated page length. A negative `offset` is
|
|
691
|
-
* clamped to 0 (first page).
|
|
692
|
-
*/
|
|
693
|
-
listSessions(cwd: string, opts?: {
|
|
694
|
-
limit?: number;
|
|
695
|
-
offset?: number;
|
|
696
|
-
}): {
|
|
697
|
-
sessions: AgentSession[];
|
|
698
|
-
total: number;
|
|
699
|
-
};
|
|
700
|
-
getNewestSession(cwd: string): AgentSession | null;
|
|
701
|
-
readSessionContent(sessionId: string, cwd: string, opts?: {
|
|
702
|
-
maxEvents?: number;
|
|
703
|
-
}): SessionContent;
|
|
704
|
-
/**
|
|
705
|
-
* Optional lightweight summary for list-prefetch.
|
|
706
|
-
*
|
|
707
|
-
* Returns only the display fields (aiTitle/recap/displayTitle/usage) without
|
|
708
|
-
* constructing the full `events` array. When absent, callers fall back to
|
|
709
|
-
* `readSessionContent` and take the needed fields.
|
|
710
|
-
*/
|
|
711
|
-
readSessionSummary?(sessionId: string, cwd: string): SessionSummary;
|
|
712
|
-
/**
|
|
713
|
-
* Whether a session is still active (process may still be running).
|
|
714
|
-
*
|
|
715
|
-
* Dormant API: no production caller reads this today — the router decides
|
|
716
|
-
* "is an agent running" via the in-memory `bridge.isBusyFor(cwd)` (see
|
|
717
|
-
* `cmdPs`), not by probing session files. Retained on the reader contract
|
|
718
|
-
* because all 5 readers implement it and it is the natural hook if a
|
|
719
|
-
* file-based liveness check is ever needed; not wired into the bridge.
|
|
720
|
-
*/
|
|
721
|
-
isSessionActive(sessionId: string, cwd: string): boolean;
|
|
722
|
-
}
|
|
723
574
|
/**
|
|
724
575
|
* AgentRunner extends Runner with agent identity and a session reader.
|
|
725
576
|
* Bridge/Router depend on this interface so adding a new agent is
|
|
@@ -732,6 +583,22 @@ export interface AgentRunner extends Runner {
|
|
|
732
583
|
getStatusInfo(): AgentStatusInfo;
|
|
733
584
|
/** Whether this runner provides live usage data (vs. file-based). */
|
|
734
585
|
getUsageAuthority?(): 'live' | 'jsonl';
|
|
586
|
+
/**
|
|
587
|
+
* 回传审批决策(codex thread/permissions、ACP elicitation、DSH approval
|
|
588
|
+
* resolve)。未实现的 agent 由协调器按「无人应答」超时收尾。
|
|
589
|
+
*/
|
|
590
|
+
respondApproval?(requestId: number | string, response: unknown): Promise<void>;
|
|
591
|
+
/**
|
|
592
|
+
* 停止 agent 侧的一个后台任务(DSH 的 background job)。只有支持后台任务的
|
|
593
|
+
* agent 实现;调用方按能力存在与否门控。
|
|
594
|
+
*/
|
|
595
|
+
killJob?(sessionId: string, jobId: string): Promise<boolean>;
|
|
596
|
+
/**
|
|
597
|
+
* 热更审批模式/权限预设(config.save 后推给在跑的 runner,不必重启会话)。
|
|
598
|
+
* 各家 settings 词表不同(codex: approvalPolicy/sandbox;kimi: permissionMode;
|
|
599
|
+
* opencode: mode;dsh: permissionPreset),故为开放载荷。
|
|
600
|
+
*/
|
|
601
|
+
updateApprovalMode?(settings: Record<string, unknown>): Promise<void>;
|
|
735
602
|
}
|
|
736
603
|
/** Agent self-describing status info for /status display. */
|
|
737
604
|
export interface AgentStatusInfo {
|
|
@@ -746,4 +613,3 @@ export interface AgentStatusInfo {
|
|
|
746
613
|
/** Agent-specific extra fields (e.g. codex.sandbox). */
|
|
747
614
|
extras?: Record<string, string>;
|
|
748
615
|
}
|
|
749
|
-
export {};
|
package/dist/runner/types.js
CHANGED
|
@@ -1,30 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runner 模块类型定义
|
|
3
|
-
*
|
|
4
|
-
* 本文件包含所有 Agent 相关的类型定义,从 runner/index.ts 提取。
|
|
5
|
-
* 所有下游模块应从本文件 import 类型,而非直接依赖实现。
|
|
6
|
-
*/
|
|
7
1
|
// =============================================================================
|
|
8
2
|
// Multi-Agent Adapter Types
|
|
9
3
|
// =============================================================================
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* 校验、各 agent 的 config builder…),漂移过:dsh 加入后 bridge 的
|
|
15
|
-
* resume.compact 仍只有 5 个,带 dsh 的 resume 卡片会静默退回 defaultAgent。
|
|
16
|
-
* 新增 agent 只改这里。
|
|
17
|
-
*/
|
|
18
|
-
export const AGENT_KINDS = ['claude', 'codex', 'opencode', 'pi', 'kimi', 'dsh'];
|
|
19
|
-
/** 用户可见 agent 显示名单源:启动注册与无 registry 的回退路径共用。 */
|
|
20
|
-
export const AGENT_DISPLAY_NAMES = {
|
|
21
|
-
claude: 'Claude',
|
|
22
|
-
codex: 'Codex',
|
|
23
|
-
opencode: 'Opencode',
|
|
24
|
-
pi: 'Pi',
|
|
25
|
-
kimi: 'Kimi',
|
|
26
|
-
dsh: 'DSH',
|
|
27
|
-
};
|
|
4
|
+
// Agent 名单与显示名住在 `src/agent-kinds.ts`(中立层,config/session/card 都
|
|
5
|
+
// 要用);本模块只 import `AgentKind` 类型来签名 runner 契约。保留旧路径的
|
|
6
|
+
// re-export 兼容调用方,但值仍只由 agent-kinds.ts 定义。
|
|
7
|
+
export { AGENT_DISPLAY_NAMES } from '../agent-kinds.js';
|
|
28
8
|
/**
|
|
29
9
|
* Compact 能力声明(运行时门控的能力清单,编译期强制完整)。
|
|
30
10
|
*
|
|
@@ -74,15 +74,22 @@ export declare class SessionIndex {
|
|
|
74
74
|
private readonly byCwd;
|
|
75
75
|
private lastRefreshAt;
|
|
76
76
|
private built;
|
|
77
|
+
private scanError?;
|
|
77
78
|
constructor(projectsDir: string, opts?: SessionIndexOptions);
|
|
78
79
|
/** Whether a full scan has ever completed successfully (readdir succeeded). */
|
|
79
80
|
get isBuilt(): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* 最近一次全量扫描的非 ENOENT 失败(undefined = 上次扫描正常)。
|
|
83
|
+
* list 路径用它把「读不到」与「真的没有」区分开。
|
|
84
|
+
*/
|
|
85
|
+
get lastScanError(): unknown;
|
|
80
86
|
build(): void;
|
|
81
87
|
refresh(): void;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
/**
|
|
89
|
+
* 该 cwd 的全部会话(newest first)。分页不在这里:`listSessions` 的
|
|
90
|
+
* offset/limit 契约由 reader 用共享 `paginate()` 完成,索引只负责有序全集。
|
|
91
|
+
*/
|
|
92
|
+
listByCwd(cwd: string): SessionIndexEntry[];
|
|
86
93
|
/**
|
|
87
94
|
* Find a session entry by sessionId and cwd.
|
|
88
95
|
* Re-stats the file to verify fingerprint before returning (拍板 3):
|
|
@@ -83,6 +83,7 @@ export function parseSessionJsonl(filePath) {
|
|
|
83
83
|
});
|
|
84
84
|
// If file couldn't be opened, return empty result
|
|
85
85
|
if (!ok) {
|
|
86
|
+
getLogger().debug(`[session-index] unreadable session file ${filePath}, treating as empty`);
|
|
86
87
|
return { cwdSet: new Set(), summary: '(无摘要)' };
|
|
87
88
|
}
|
|
88
89
|
return { cwdSet, summary };
|
|
@@ -98,7 +99,12 @@ export function buildFingerprint(filePath) {
|
|
|
98
99
|
try {
|
|
99
100
|
st = fs.statSync(filePath, { bigint: true });
|
|
100
101
|
}
|
|
101
|
-
catch {
|
|
102
|
+
catch (err) {
|
|
103
|
+
// ENOENT = 文件在扫描间隙被删除(正常竞态)。其余(EACCES/EBUSY…)留痕,
|
|
104
|
+
// 否则该会话会静默从 /resume 列表里消失。
|
|
105
|
+
if (err.code !== 'ENOENT') {
|
|
106
|
+
getLogger().warn(`[session-index] cannot stat ${filePath}: ${err?.message ?? String(err)}`);
|
|
107
|
+
}
|
|
102
108
|
return '';
|
|
103
109
|
}
|
|
104
110
|
const parts = [String(st.size), String(st.mtimeNs)];
|
|
@@ -153,6 +159,7 @@ export class SessionIndex {
|
|
|
153
159
|
byCwd = new Map();
|
|
154
160
|
lastRefreshAt = 0;
|
|
155
161
|
built = false;
|
|
162
|
+
scanError;
|
|
156
163
|
constructor(projectsDir, opts = {}) {
|
|
157
164
|
this.projectsDir = projectsDir;
|
|
158
165
|
this.refreshIntervalMs = opts.refreshIntervalMs ?? 5000;
|
|
@@ -162,6 +169,13 @@ export class SessionIndex {
|
|
|
162
169
|
get isBuilt() {
|
|
163
170
|
return this.built;
|
|
164
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* 最近一次全量扫描的非 ENOENT 失败(undefined = 上次扫描正常)。
|
|
174
|
+
* list 路径用它把「读不到」与「真的没有」区分开。
|
|
175
|
+
*/
|
|
176
|
+
get lastScanError() {
|
|
177
|
+
return this.scanError;
|
|
178
|
+
}
|
|
165
179
|
// ── Build (initial full scan) ───────────────────────────
|
|
166
180
|
build() {
|
|
167
181
|
this.fullScan();
|
|
@@ -177,7 +191,11 @@ export class SessionIndex {
|
|
|
177
191
|
this.lastRefreshAt = now;
|
|
178
192
|
}
|
|
179
193
|
// ── Query: listByCwd ────────────────────────────────────
|
|
180
|
-
|
|
194
|
+
/**
|
|
195
|
+
* 该 cwd 的全部会话(newest first)。分页不在这里:`listSessions` 的
|
|
196
|
+
* offset/limit 契约由 reader 用共享 `paginate()` 完成,索引只负责有序全集。
|
|
197
|
+
*/
|
|
198
|
+
listByCwd(cwd) {
|
|
181
199
|
const paths = this.byCwd.get(pathKey(cwd, { platform: this.platform }));
|
|
182
200
|
if (!paths || paths.size === 0)
|
|
183
201
|
return [];
|
|
@@ -194,10 +212,7 @@ export class SessionIndex {
|
|
|
194
212
|
}
|
|
195
213
|
const sorted = [...byId.values()];
|
|
196
214
|
sortByRecencyDesc(sorted, (s) => s.mtimeMs, (s) => s.sessionId);
|
|
197
|
-
|
|
198
|
-
const limit = opts.limit;
|
|
199
|
-
const sliced = limit === undefined ? sorted.slice(offset) : sorted.slice(offset, offset + limit);
|
|
200
|
-
return sliced;
|
|
215
|
+
return sorted;
|
|
201
216
|
}
|
|
202
217
|
// ── Query: findBySessionIdAndCwd ────────────────────────
|
|
203
218
|
/**
|
|
@@ -261,11 +276,18 @@ export class SessionIndex {
|
|
|
261
276
|
try {
|
|
262
277
|
entries = fs.readdirSync(this.projectsDir);
|
|
263
278
|
}
|
|
264
|
-
catch {
|
|
279
|
+
catch (err) {
|
|
265
280
|
// readdir failure → preserve old index, degrade to fallback
|
|
266
|
-
|
|
281
|
+
if (err.code === 'ENOENT') {
|
|
282
|
+
// projectsDir 不存在 = 全新环境(真空态),不是故障。
|
|
283
|
+
this.scanError = undefined;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
this.scanError = err;
|
|
287
|
+
getLogger().warn(`[session-index] readdir ${this.projectsDir} failed, preserving old index: ${err?.message ?? String(err)}`);
|
|
267
288
|
return;
|
|
268
289
|
}
|
|
290
|
+
this.scanError = undefined;
|
|
269
291
|
// readdir succeeded → the snapshot is authoritative for what we could read.
|
|
270
292
|
this.built = true;
|
|
271
293
|
const currentPaths = new Set();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentSession, AgentSessionReader, SessionContent } from '../../
|
|
1
|
+
import type { AgentSession, AgentSessionReader, SessionContent } from '../../session-contract.js';
|
|
2
2
|
/**
|
|
3
3
|
* Adapter that exposes Claude Code's session-reading functions through the
|
|
4
4
|
* unified `AgentSessionReader` interface. This lets `SessionReaderRegistry`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentSession, AgentSessionContentEvent, AgentSessionUsage } from '../../
|
|
1
|
+
import type { AgentSession, AgentSessionContentEvent, AgentSessionUsage } from '../../session-contract.js';
|
|
2
2
|
/**
|
|
3
3
|
* List Claude Code sessions for a given cwd, newest first. Each entry has
|
|
4
4
|
* the sessionId (= jsonl filename stem), a one-line summary extracted from
|
|
@@ -15,7 +15,7 @@ export declare function getNewestSession(cwd: string, opts?: {
|
|
|
15
15
|
}): AgentSession | undefined;
|
|
16
16
|
/**
|
|
17
17
|
* Check if a Claude session (by sessionId + cwd) is still active.
|
|
18
|
-
* Public wrapper around the internal `
|
|
18
|
+
* Public wrapper around the internal `hasRecentFileActivity(filePath, mtimeMs, now)`
|
|
19
19
|
* for use by `ClaudeSessionReader` and the multi-agent adapter layer.
|
|
20
20
|
*
|
|
21
21
|
* Resolves the jsonl path via `projectDirForCwd`, stats it for mtime, then
|
|
@@ -6,6 +6,7 @@ import { isStale } from '../common/constants.js';
|
|
|
6
6
|
import { samePath, encodeProjectDirName } from '../../platform/path.js';
|
|
7
7
|
import { extractContentBlocks } from '../common/content-blocks.js';
|
|
8
8
|
import { scanJsonlOnce, tailOffsetAfter, scanTailEvents } from '../common/two-pass.js';
|
|
9
|
+
import { getLogger } from '../../logger/index.js';
|
|
9
10
|
import { UsageAccumulator, contextWindowOccupancy, cumulativeUsageFields, } from '../common/usage-accumulator.js';
|
|
10
11
|
import { SessionIndex, extractText, isTaskNotificationText, CLAUDE_MAPPING, } from './session-index.js';
|
|
11
12
|
/**
|
|
@@ -61,7 +62,10 @@ function findSessionFileInProjects(sessionId, projectsDir, cwd) {
|
|
|
61
62
|
try {
|
|
62
63
|
entries = fs.readdirSync(projectsDir);
|
|
63
64
|
}
|
|
64
|
-
catch {
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (err.code !== 'ENOENT') {
|
|
67
|
+
getLogger().debug(`[claude-session-reader] cannot read projects dir ${projectsDir}: ${err?.message ?? String(err)}`);
|
|
68
|
+
}
|
|
65
69
|
return undefined;
|
|
66
70
|
}
|
|
67
71
|
const name = `${sessionId}.jsonl`;
|
|
@@ -123,6 +127,13 @@ export function listClaudeSessions(cwd, opts = {}) {
|
|
|
123
127
|
summary: e.summary,
|
|
124
128
|
mtime: e.mtimeMs,
|
|
125
129
|
}));
|
|
130
|
+
// 空列表 + 上次扫描是非 ENOENT 失败 → 是「读不到」而非「真的没有」。
|
|
131
|
+
// 与 opencode/kimi/pi 同口径:router 显示「列表失败」,别假装空态。
|
|
132
|
+
if (sessions.length === 0 && index.lastScanError !== undefined) {
|
|
133
|
+
const err = index.lastScanError;
|
|
134
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
135
|
+
throw new Error(`claude session list 读取失败: ${msg}`, { cause: err });
|
|
136
|
+
}
|
|
126
137
|
return limit === undefined ? sessions : sessions.slice(0, limit);
|
|
127
138
|
}
|
|
128
139
|
/** Get the newest session for a given cwd. Returns undefined if none exist. */
|
|
@@ -145,7 +156,15 @@ export function getNewestSession(cwd, opts = {}) {
|
|
|
145
156
|
*
|
|
146
157
|
* Override `now` for tests.
|
|
147
158
|
*/
|
|
148
|
-
|
|
159
|
+
/** 末行是这些类型之一 = 会话已收尾(模块级:逐文件判定不必每次重建 Set)。 */
|
|
160
|
+
const CLAUDE_TERMINAL_TYPES = new Set(['result', 'last-prompt', 'mode', 'permission-mode']);
|
|
161
|
+
/**
|
|
162
|
+
* 文件侧的活跃判定:mtime 新鲜 + 末行没有终态标记。
|
|
163
|
+
*
|
|
164
|
+
* 刻意不叫 `isSessionActive`——那是 reader 契约上的方法名,语义各家自己定;
|
|
165
|
+
* 本函数只看这一个文件,两者同名不同义曾让人把「最近有写入」当成「turn 在跑」。
|
|
166
|
+
*/
|
|
167
|
+
function hasRecentFileActivity(filePath, mtimeMs, now = Date.now()) {
|
|
149
168
|
// Hard signal: file untouched for over an hour → can't be from a live process
|
|
150
169
|
if (isStale(mtimeMs, now))
|
|
151
170
|
return false;
|
|
@@ -158,8 +177,7 @@ function isSessionActive(filePath, mtimeMs, now = Date.now()) {
|
|
|
158
177
|
// `last-prompt` / `mode` (no `result` event) to mark turn end, so a
|
|
159
178
|
// 24-minute-old session with a last-prompt tail is already done
|
|
160
179
|
// (regression 2026-06-21: previously falsely reported as active).
|
|
161
|
-
|
|
162
|
-
if (typeof obj.type === 'string' && TERMINAL_TYPES.has(obj.type))
|
|
180
|
+
if (typeof obj.type === 'string' && CLAUDE_TERMINAL_TYPES.has(obj.type))
|
|
163
181
|
return false;
|
|
164
182
|
}
|
|
165
183
|
catch {
|
|
@@ -167,9 +185,18 @@ function isSessionActive(filePath, mtimeMs, now = Date.now()) {
|
|
|
167
185
|
}
|
|
168
186
|
return true;
|
|
169
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* stat 失败:ENOENT = 文件在两次查询之间消失(正常竞态);其余 errno 必须留痕,
|
|
190
|
+
* 否则「文件不可读」会被当成「会话不活跃」。
|
|
191
|
+
*/
|
|
192
|
+
function logStatFailure(filePath, err) {
|
|
193
|
+
if (err.code === 'ENOENT')
|
|
194
|
+
return;
|
|
195
|
+
getLogger().debug(`[claude-session-reader] stat ${filePath} failed: ${err?.message ?? String(err)}`);
|
|
196
|
+
}
|
|
170
197
|
/**
|
|
171
198
|
* Check if a Claude session (by sessionId + cwd) is still active.
|
|
172
|
-
* Public wrapper around the internal `
|
|
199
|
+
* Public wrapper around the internal `hasRecentFileActivity(filePath, mtimeMs, now)`
|
|
173
200
|
* for use by `ClaudeSessionReader` and the multi-agent adapter layer.
|
|
174
201
|
*
|
|
175
202
|
* Resolves the jsonl path via `projectDirForCwd`, stats it for mtime, then
|
|
@@ -187,9 +214,10 @@ export function isClaudeSessionActive(sessionId, cwd, opts = {}) {
|
|
|
187
214
|
if (entry) {
|
|
188
215
|
try {
|
|
189
216
|
const st = fs.statSync(entry.path);
|
|
190
|
-
return
|
|
217
|
+
return hasRecentFileActivity(entry.path, st.mtimeMs);
|
|
191
218
|
}
|
|
192
|
-
catch {
|
|
219
|
+
catch (err) {
|
|
220
|
+
logStatFailure(entry.path, err);
|
|
193
221
|
return false;
|
|
194
222
|
}
|
|
195
223
|
}
|
|
@@ -210,9 +238,10 @@ export function isClaudeSessionActive(sessionId, cwd, opts = {}) {
|
|
|
210
238
|
return false;
|
|
211
239
|
try {
|
|
212
240
|
const st = fs.statSync(filePath);
|
|
213
|
-
return
|
|
241
|
+
return hasRecentFileActivity(filePath, st.mtimeMs);
|
|
214
242
|
}
|
|
215
|
-
catch {
|
|
243
|
+
catch (err) {
|
|
244
|
+
logStatFailure(filePath, err);
|
|
216
245
|
return false;
|
|
217
246
|
}
|
|
218
247
|
}
|
|
@@ -411,23 +440,9 @@ export function readSessionContent(sessionId, cwd, opts = {}) {
|
|
|
411
440
|
return { events: [] };
|
|
412
441
|
}
|
|
413
442
|
}
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
// while retaining NO parsed objects and NO whole-file line array. The
|
|
418
|
-
// tailOffset is the byte offset where the tail begins (start of the line
|
|
419
|
-
// after the last user message). A second pass re-reads ONLY the tail from
|
|
420
|
-
// that offset via `readJsonlLinesFromOffset` and re-parses it for events —
|
|
421
|
-
// O(tail) memory for both raw line strings and parsed objects, instead of
|
|
422
|
-
// O(whole file).
|
|
423
|
-
//
|
|
424
|
-
// Parse ratio: ≈1.0–1.5× line count when a user message exists (streaming
|
|
425
|
-
// full-file scan + tail re-parse of the usually-short post-user lines).
|
|
426
|
-
// When the session has NO user message, tailOffset stays -1 and the tail
|
|
427
|
-
// IS the whole file → scan(N) + tail(N) = 2.0× — the known asymptotic
|
|
428
|
-
// upper bound of the body's two-phase design (still better than pre-'s
|
|
429
|
-
// ~3×). Total parse count adds the index-miss fallback guard's fixed
|
|
430
|
-
// early-stop overhead (~2 parses, independent of line count).
|
|
443
|
+
// Two-pass 扫描(pass 1 流式收标量 + pass 2 只重读 tail)的口径与解析比
|
|
444
|
+
// 见 `src/session/common/two-pass.ts` 头注释;claude 侧只多一项:索引 miss
|
|
445
|
+
// 时的 fallback 守卫另有固定 ~2 次早停解析,与行数无关。
|
|
431
446
|
const scan = scalarScan(filePath);
|
|
432
447
|
// 有 usage 即返回:非累计字段已是末轮 scope,命中判定用 session 累计兜底
|
|
433
448
|
// (末轮可能 input/output 为 0 而仍有 cacheRead,累计口径不会被末轮遮挡)。
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* Note: This handles the **rollout file format** (historical logging), NOT the
|
|
24
24
|
* `codex exec --json` stdout format(已随 exec 模式移除)。
|
|
25
25
|
*/
|
|
26
|
-
import type { AgentSessionContentEvent, SessionContent, SessionSummary
|
|
26
|
+
import type { AgentSessionContentEvent, AgentSessionUsage, SessionContent, SessionSummary } from '../../session-contract.js';
|
|
27
27
|
interface CodexRolloutEntry {
|
|
28
28
|
threadId: string;
|
|
29
29
|
cwd: string;
|
|
@@ -32,6 +32,7 @@ import { paginate, capEvents } from '../common/pagination.js';
|
|
|
32
32
|
import { sortByRecencyDesc } from '../common/recency.js';
|
|
33
33
|
import { getLogger } from '../../logger/index.js';
|
|
34
34
|
import { isRecord, stringValue } from '../../common/guards.js';
|
|
35
|
+
import { TtlCache } from '../../common/ttl-cache.js';
|
|
35
36
|
import { resolveCodexHome } from '../../config/codex-config.js';
|
|
36
37
|
/** Field-wise difference (total - previous) for cumulative->incremental. */
|
|
37
38
|
function subtractRawUsage(a, b) {
|
|
@@ -623,7 +624,12 @@ function safeIsDir(p) {
|
|
|
623
624
|
}
|
|
624
625
|
}
|
|
625
626
|
const INDEX_TTL_MS = 5_000; // 5 seconds — covers a single /resume flow
|
|
626
|
-
|
|
627
|
+
/**
|
|
628
|
+
* codexHome → 会话索引。共享 `TtlCache`(TTL 由写入时间判定 + LRU 上限),
|
|
629
|
+
* 与 kimi/pi/opencode 三个 reader 同一套语义。生产侧 `CodexSessionReader`
|
|
630
|
+
* 构造时就把 codexHome 解析定死,实际只会用到 1 个键;上限是防漏的界。
|
|
631
|
+
*/
|
|
632
|
+
const sessionIndexCache = new TtlCache(INDEX_TTL_MS, 4);
|
|
627
633
|
/**
|
|
628
634
|
* Build or return cached session index for a given resolved codexHome.
|
|
629
635
|
* The index maps sessionId → {filePath, cwd, mtimeMs}.
|
|
@@ -635,19 +641,14 @@ const sessionIndexCache = new Map();
|
|
|
635
641
|
* AFTER the cache was built — but still within the
|
|
636
642
|
* 5s TTL — is found without waiting for expiry.
|
|
637
643
|
*
|
|
638
|
-
* Cache shape note:
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
* bridge instance produces exactly one cache entry — the Map does NOT grow
|
|
642
|
-
* unbounded. The TTL refreshes the value on each rebuild; entries are never
|
|
643
|
-
* evicted, but there is effectively just one.
|
|
644
|
+
* Cache shape note: the cache is module-level and keyed by the resolved
|
|
645
|
+
* codexHome string; `TtlCache` caps entries (LRU) so a caller that injects
|
|
646
|
+
* many distinct codexHomes cannot grow memory without bound.
|
|
644
647
|
*/
|
|
645
648
|
function getSessionIndex(resolvedCodexHome, forceRefresh = false) {
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
return cached.index;
|
|
650
|
-
}
|
|
649
|
+
const cached = forceRefresh ? undefined : sessionIndexCache.get(resolvedCodexHome);
|
|
650
|
+
if (cached)
|
|
651
|
+
return cached;
|
|
651
652
|
const sessionsDir = path.join(resolvedCodexHome, 'sessions');
|
|
652
653
|
const files = walkRolloutFiles(sessionsDir);
|
|
653
654
|
const index = new Map();
|
|
@@ -704,7 +705,7 @@ function getSessionIndex(resolvedCodexHome, forceRefresh = false) {
|
|
|
704
705
|
continue;
|
|
705
706
|
}
|
|
706
707
|
}
|
|
707
|
-
sessionIndexCache.set(
|
|
708
|
+
sessionIndexCache.set(resolvedCodexHome, index);
|
|
708
709
|
return index;
|
|
709
710
|
}
|
|
710
711
|
/** Clear the session index cache. Exposed for tests. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentSession, SessionContent, SessionSummary
|
|
1
|
+
import type { AgentSession, AgentSessionReader, SessionContent, SessionSummary } from '../../session-contract.js';
|
|
2
2
|
interface CodexSessionReaderOptions {
|
|
3
3
|
codexHome?: string;
|
|
4
4
|
}
|