qlogicagent 2.17.10 → 2.18.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.
Files changed (44) hide show
  1. package/dist/agent.js +16 -16
  2. package/dist/cli.js +1 -1
  3. package/dist/index.js +431 -423
  4. package/dist/skills/mcp/astraclaw-native-mcp-server.js +6 -5
  5. package/dist/types/agent/tool-loop.d.ts +6 -0
  6. package/dist/types/agent/types.d.ts +5 -0
  7. package/dist/types/cli/handlers/agents-handler.d.ts +9 -0
  8. package/dist/types/cli/handlers/config-handler.d.ts +5 -0
  9. package/dist/types/cli/handlers/files-handler.d.ts +7 -0
  10. package/dist/types/cli/handlers/session-handler.d.ts +43 -0
  11. package/dist/types/cli/handlers/turn-handler.d.ts +12 -0
  12. package/dist/types/cli/permission-rule-persistence.d.ts +16 -0
  13. package/dist/types/cli/stdio-rpc-handler-hosts.d.ts +4 -0
  14. package/dist/types/cli/stdio-server.d.ts +16 -1
  15. package/dist/types/cli/turn-core.d.ts +2 -0
  16. package/dist/types/orchestration/agent-instance.d.ts +4 -0
  17. package/dist/types/orchestration/delegation-coordinator.d.ts +10 -0
  18. package/dist/types/orchestration/product-persistence.d.ts +4 -0
  19. package/dist/types/protocol/wire/acp-agent-management.d.ts +16 -0
  20. package/dist/types/protocol/wire/agent-methods.d.ts +3 -0
  21. package/dist/types/runtime/infra/acp-detector.d.ts +35 -5
  22. package/dist/types/runtime/infra/acp-protocol-adapter.d.ts +14 -0
  23. package/dist/types/runtime/infra/agent-process.d.ts +15 -0
  24. package/dist/types/runtime/infra/llmrouter-catalog.d.ts +26 -6
  25. package/dist/types/runtime/infra/model-registry.d.ts +43 -2
  26. package/dist/types/runtime/infra/user-dirs.d.ts +21 -0
  27. package/dist/types/runtime/permission-model.d.ts +1 -1
  28. package/dist/types/runtime/ports/permission-contracts.d.ts +5 -2
  29. package/dist/types/runtime/prompt/fresh-workspace-evidence.d.ts +1 -2
  30. package/dist/types/runtime/prompt/identity-section.d.ts +1 -1
  31. package/dist/types/runtime/session/native/claude-session-source.d.ts +23 -0
  32. package/dist/types/runtime/session/native/codex-index.d.ts +21 -0
  33. package/dist/types/runtime/session/native/codex-session-source.d.ts +31 -0
  34. package/dist/types/runtime/session/native/cwd-normalize.d.ts +53 -0
  35. package/dist/types/runtime/session/native/ephemeral-rollout-sweep.d.ts +13 -0
  36. package/dist/types/runtime/session/native/native-cwd-agents.d.ts +17 -0
  37. package/dist/types/runtime/session/native/native-session-source.d.ts +89 -0
  38. package/dist/types/runtime/session/native/native-title.d.ts +1 -0
  39. package/dist/types/runtime/session/native/register-builtin-sources.d.ts +1 -0
  40. package/dist/types/runtime/session/session-types.d.ts +8 -0
  41. package/dist/types/skills/permissions/hook-runner.d.ts +9 -0
  42. package/dist/types/skills/permissions/settings-watcher.d.ts +7 -0
  43. package/dist/types/transport/acp-server.d.ts +6 -0
  44. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import type { ModelCapabilityProfile, ModelEntry, ModelRegistry, ReasoningMode } from "./model-registry.js";
1
+ import type { ModelCapabilityProfile, ModelEntry, ModelPurpose, ModelRegistry, ReasoningMode } from "./model-registry.js";
2
2
  import type { ConfigPort } from "../ports/index.js";
3
3
  export declare const LLMROUTER_CATALOG_UNAVAILABLE_MESSAGE = "\u65E0\u6CD5\u8FDE\u63A5 llmrouter \u6A21\u578B\u76EE\u5F55";
4
4
  export declare const LLMROUTER_PROVIDER_ID = "llmrouter";
@@ -97,14 +97,34 @@ export declare function refreshRegistryFromLlmrouterApiKey(registry: ModelRegist
97
97
  forceCatalog?: boolean;
98
98
  }): Promise<ModelEntry[]>;
99
99
  export declare function resetLlmrouterCatalogClientCache(): void;
100
+ export interface DefaultBindingsResult {
101
+ /** 本次真正写入的绑定(用途 → modelId)。 */
102
+ applied: Array<{
103
+ purpose: ModelPurpose;
104
+ modelId: string;
105
+ }>;
106
+ /** 跳过的用途 + 原因(已有配置不覆盖 / 该用途 llmrouter 无可用模型)。 */
107
+ skipped: Array<{
108
+ purpose: ModelPurpose;
109
+ reason: "already-bound" | "no-model";
110
+ }>;
111
+ }
100
112
  /**
101
- * 零配置默认模型:全新安装(无 textGeneration binding)且网关注入了设备账号推理 key 时,
102
- * 自动把默认文本模型设为一个 DeepSeek(经 llmrouter)。调用方需先刷新过目录(this.models 里要有
103
- * `llmrouter:deepseek-*` 条目)。已有 binding 则尊重用户配置、不覆盖。返回是否设置了默认。
113
+ * 零配置默认绑定:给一组「安全」用途补上 llmrouter 默认模型 —— **只补空白**,已有 binding 的用途一律
114
+ * 不动(尊重用户/老配置)。每用途三级挑选:①服务端推荐(模型 capability_profile.default_for 声明本
115
+ * 用途)优先 → ②回退硬编码偏好子串(DEFAULT_BINDING_PREFERENCES,向后兼容) ③再回退该用途第一个
116
+ * 可用模型;都没有则不绑。**「哪些用途该默认绑」是客户端产品边界(安全白名单),「每用途选哪个模型」
117
+ * 交服务端** —— 两层职责分离,llmrouter 换模型客户端零发版跟随。
104
118
  *
105
- * key 不在此注册 —— ModelRegistry 取 llmrouter key 时会回退到 QLOGIC_LLMROUTER_INFERENCE_KEY env。
119
+ * 触发场景:①agent 启动(设备账号推理 key 注入后,stdio-server) ②刷新目录后(settings refresh)
120
+ * ③手动添加 llmrouter key。"有没有 llmrouter 可用"由 listModels 结果隐含——refresh 成功(key 有效)
121
+ * 才有 llmrouter 模型条目;无则每个用途 no-model 跳过、不绑。key 不在此注册(ModelRegistry 取 llmrouter
122
+ * key 时回退到 QLOGIC_LLMROUTER_INFERENCE_KEY env 或用户 key)。调用方需先刷新过目录。
106
123
  */
107
- export declare function ensureLlmrouterDefaultBinding(registry: ModelRegistry, configPort?: ConfigPort): boolean;
124
+ export declare function ensureLlmrouterDefaultBindings(registry: ModelRegistry, opts?: {
125
+ requireInferenceKey?: boolean;
126
+ configPort?: ConfigPort;
127
+ }): DefaultBindingsResult;
108
128
  export declare function toModelEntry(model: LlmrouterCatalogModel): ModelEntry | null;
109
129
  export declare function toModelEntries(model: LlmrouterCatalogModel): ModelEntry[];
110
130
  export {};
@@ -29,6 +29,13 @@ export interface ModelEntry {
29
29
  routeIntents: ModelPurpose[];
30
30
  baseUrl?: string;
31
31
  enabled: boolean;
32
+ /**
33
+ * Purposes this model has been individually turned OFF for. Absent/empty = on for all of its
34
+ * routeIntents (still subject to the master `enabled`). Lets one model serve several purposes
35
+ * with independent on/off switches, so disabling it under one purpose group never strands the
36
+ * bindings that use it for another.
37
+ */
38
+ disabledPurposes?: ModelPurpose[];
32
39
  nativeModelId?: string;
33
40
  transport?: string;
34
41
  contextWindow?: number;
@@ -40,6 +47,11 @@ export interface ModelEntry {
40
47
  pricing?: Record<string, unknown>;
41
48
  /** Thinking-strength control class for the UI (see ReasoningMode). */
42
49
  reasoningMode?: ReasoningMode;
50
+ /**
51
+ * 归一后的「服务端推荐默认用途」(来自 capabilityProfile.default_for)。零配置默认绑定优先选
52
+ * defaultForIntents 命中本用途的模型,读不到则回退硬编码偏好。空/缺 → 无服务端推荐。
53
+ */
54
+ defaultForIntents?: ModelPurpose[];
43
55
  }
44
56
  export interface ModelCapabilityProfile {
45
57
  tasks?: string[];
@@ -55,6 +67,12 @@ export interface ModelCapabilityProfile {
55
67
  requires_confirmation?: boolean;
56
68
  default_selectable?: boolean;
57
69
  };
70
+ /**
71
+ * 服务端声明:该模型是这些 route_intent 的「推荐默认」(零配置绑定优先读它)。原始 intent 字符串
72
+ * (由 llmrouter 归一为规范名);客户端过滤到已知 ModelPurpose 得 defaultForIntents。空/缺 → 无服务端
73
+ * 推荐 → 客户端回退硬编码偏好(向后兼容:llmrouter 未部署 default_for 时行为与现状完全一致)。
74
+ */
75
+ default_for?: string[];
58
76
  }
59
77
  export type PurposeBindings = Partial<Record<ModelPurpose, string>>;
60
78
  export interface ModelRegistryConfig {
@@ -62,6 +80,7 @@ export interface ModelRegistryConfig {
62
80
  models: Array<{
63
81
  id: string;
64
82
  enabled?: boolean;
83
+ disabledPurposes?: ModelPurpose[];
65
84
  }>;
66
85
  bindings: PurposeBindings;
67
86
  tunables?: Record<string, unknown>;
@@ -84,6 +103,7 @@ export declare class ModelRegistry {
84
103
  private catalogProviders;
85
104
  private models;
86
105
  private modelEnabledOverrides;
106
+ private modelPurposeDisabled;
87
107
  private bindings;
88
108
  private injectedKeyStates;
89
109
  private settingsPath;
@@ -106,6 +126,12 @@ export declare class ModelRegistry {
106
126
  */
107
127
  acceptsInputModality(modality: string, purpose?: ModelPurpose): boolean;
108
128
  isAvailable(purpose: ModelPurpose): boolean;
129
+ /**
130
+ * Whether `id` is enabled for `purpose`. The master `enabled` flag is the global gate; on top of it
131
+ * a single purpose can be switched off (modelPurposeDisabled) without touching the model's other
132
+ * purposes. This is what keeps "disable in 文本生成" from stranding the 图像理解 binding.
133
+ */
134
+ isEnabledForPurpose(id: string, purpose: ModelPurpose): boolean;
109
135
  resolveModelForPurpose(purpose: ModelPurpose): string | null;
110
136
  addProvider(providerId: string, opts?: {
111
137
  baseUrl?: string;
@@ -132,8 +158,23 @@ export declare class ModelRegistry {
132
158
  replaceCatalogModels(entries: ModelEntry[]): void;
133
159
  replaceProviderModels(providerId: string, entries: ModelEntry[]): void;
134
160
  removeModel(id: string): void;
135
- enableModel(id: string): void;
136
- disableModel(id: string): void;
161
+ /**
162
+ * Enable a model. With `purpose`, enables it for just that purpose (clearing its per-purpose OFF
163
+ * switch); if the whole model was off, it flips on but keeps its OTHER purposes off so a single
164
+ * "turn on imageUnderstanding" never silently re-enables textGeneration too. Without `purpose`,
165
+ * enables the model globally (on for all of its purposes).
166
+ */
167
+ enableModel(id: string, purpose?: ModelPurpose): void;
168
+ /**
169
+ * Disable a model. With `purpose`, turns it off for just that purpose; the master flag only flips
170
+ * off once EVERY one of its routeIntents is off, so purpose-agnostic consumers (default-model
171
+ * pickers) still see the model while any purpose stays live. Without `purpose`, disables globally.
172
+ */
173
+ disableModel(id: string, purpose?: ModelPurpose): void;
174
+ private isMasterEnabled;
175
+ private setMasterEnabled;
176
+ private setPurposeDisabled;
177
+ private purposeDisabledArray;
137
178
  listModels(filter?: {
138
179
  purpose?: ModelPurpose;
139
180
  provider?: string;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 跨平台获取用户常见目录(桌面/下载/文档)的真实绝对路径,供 environment-context 注入给 agent——
3
+ * 让「读桌面/下载的文件」这类桌面端最常见请求有一个确定的绝对路径锚点,而不是相对会话工作区瞎找。
4
+ *
5
+ * 各平台权威来源(都不能简单 homedir + "/Desktop" 硬拼——OneDrive/自定义重定向会漏):
6
+ * - Windows: 注册表 User Shell Folders(含 OneDrive/自定义重定向);值是 REG_EXPAND_SZ,含 %VAR% 需展开。
7
+ * 展开用 node 的 process.env(编码正确),所以即便用户名是中文,%USERPROFILE% 模板展开后仍准;
8
+ * 只有用户把桌面手动重定向到硬编码中文路径时,reg 的 OEM 输出可能乱码 → existsSync 兜底回退。
9
+ * - macOS: ~/Desktop 等固定英文路径(Finder 里的本地化名只是显示,真实路径是英文)。
10
+ * - Linux: XDG user-dirs(~/.config/user-dirs.dirs 的 XDG_DESKTOP_DIR 等),回退 ~/Desktop。
11
+ * 一次性探测 + 缓存(目录在进程生命周期内不变)。任何一项失败都回退到 homedir 拼接,绝不抛。
12
+ */
13
+ export interface UserDirs {
14
+ desktop: string;
15
+ downloads: string;
16
+ documents: string;
17
+ }
18
+ /** 用户桌面/下载/文档的真实绝对路径(缓存)。失败项回退 homedir 拼接,绝不抛。 */
19
+ export declare function getUserDirs(): UserDirs;
20
+ /** 仅测试用:清缓存,让不同平台/环境的断言各自重探。 */
21
+ export declare function resetUserDirsCache(): void;
@@ -3,7 +3,7 @@ export interface PermissionPromptOption {
3
3
  optionId: string;
4
4
  name: string;
5
5
  kind: "allow_once" | "allow_always" | "reject_once" | "reject_always";
6
- scope?: "once" | "directory";
6
+ scope?: "once" | "directory" | "directory_persistent";
7
7
  }
8
8
  export interface PermissionModeDescriptor {
9
9
  id: PermissionMode;
@@ -105,8 +105,11 @@ export interface ApprovalResponse {
105
105
  approvalId: string;
106
106
  decision: "allow" | "deny";
107
107
  updatedInput?: Record<string, unknown>;
108
- /** Approval scope chosen by the user: just this call, or the whole directory. */
109
- scope?: "once" | "directory";
108
+ /**
109
+ * Approval scope chosen by the user: just this call, the whole directory for
110
+ * this session, or the whole directory persisted across restarts.
111
+ */
112
+ scope?: "once" | "directory" | "directory_persistent";
110
113
  }
111
114
  export interface PermissionRuleEnginePort {
112
115
  getMode(): PermissionMode;
@@ -11,9 +11,8 @@ export interface ToolNameLike {
11
11
  }
12
12
  export interface FreshWorkspaceEvidencePolicy {
13
13
  requiresFreshTool: boolean;
14
- reason?: "file-read" | "file-mutation" | "memory-mutation" | "media-generation" | "directory-list" | "content-search" | "shell" | "web";
14
+ reason?: "file-read" | "memory-mutation" | "media-generation" | "directory-list" | "content-search" | "shell" | "web";
15
15
  mediaKind?: "image" | "tts" | "music";
16
- workflowKind?: "artifact-index";
17
16
  allowedToolNames: string[];
18
17
  }
19
18
  export declare function getFreshWorkspaceEvidencePolicy(messages: readonly PromptMessageLike[] | string): FreshWorkspaceEvidencePolicy;
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { type SystemPromptSection } from "./system-prompt-sections.js";
8
8
  export interface IdentityInfo {
9
- /** 产品名(运营可配置),缺省 QLogicAgent */
9
+ /** 产品名(运营可配置),缺省 AstraClaw */
10
10
  productName?: string;
11
11
  /** 可选的品牌/构建方说明行(运营可配置) */
12
12
  vendorLine?: string;
@@ -0,0 +1,23 @@
1
+ import type { NativeSessionEntry, NativeSessionRef, NativeSessionSource, TranscriptMessage } from "./native-session-source.js";
2
+ /**
3
+ * Native session source for Claude Code (claude CLI). Reads directly from
4
+ * `~/.claude/projects/<claudeProjectDirName(cwd)>/<sessionUuid>.jsonl` — the same on-disk format
5
+ * Claude Code itself writes, so this is read-only and never touched by qlogicagent.
6
+ *
7
+ * `isSidechain: true` events are subagent side-chains, not part of the user-visible conversation,
8
+ * and are filtered out of both list() (message counting / title / lastActiveAt) and
9
+ * loadTranscript() (transcript body).
10
+ */
11
+ export declare class ClaudeSessionSource implements NativeSessionSource {
12
+ private readonly home;
13
+ readonly agentId = "claude";
14
+ readonly kind: "claude";
15
+ /** `home` is injectable so tests can point at a temp directory instead of the real user home. */
16
+ constructor(home?: string);
17
+ list(cwd: string, opts?: {
18
+ limit?: number;
19
+ }): Promise<NativeSessionEntry[]>;
20
+ /** A claude native session is exactly the file `<encoded-cwd>/<sessionUuid>.jsonl` — a single stat. */
21
+ has(sessionId: string, cwd: string): Promise<boolean>;
22
+ loadTranscript(ref: NativeSessionRef): Promise<TranscriptMessage[]>;
23
+ }
@@ -0,0 +1,21 @@
1
+ import type { NativeSessionEntry } from "./native-session-source.js";
2
+ /**
3
+ * Lists codex-native sessions belonging to `normalizedTargetCwd` (already run through
4
+ * {@link normalizeCwd} by the caller), sorted by `lastActiveAt` descending.
5
+ *
6
+ * Synchronous by design — built entirely on sync fs calls, since it's a Cut 3 building block; a
7
+ * future CodexSessionSource adapter (mirroring claude-session-source.ts's shape) would wrap this in
8
+ * the async `NativeSessionSource.list()` contract.
9
+ *
10
+ * `messageCount` is always 0: unlike claude's one-line-per-message transcript format, a codex
11
+ * rollout interleaves many event types per turn, so computing an exact count means fully parsing
12
+ * every candidate rollout's entire event stream — deliberately out of scope here. Revisit if/when a
13
+ * caller actually needs an exact count.
14
+ *
15
+ * Never throws: a missing/unreadable codexHome, sessions dir, session_index.jsonl, or cache file all
16
+ * degrade to "as if empty" rather than propagating an error.
17
+ */
18
+ export declare function listCodexSessionsByCwd(normalizedTargetCwd: string, opts?: {
19
+ codexHome?: string;
20
+ cacheDir?: string;
21
+ }): NativeSessionEntry[];
@@ -0,0 +1,31 @@
1
+ import type { NativeSessionEntry, NativeSessionRef, NativeSessionSource, TranscriptMessage } from "./native-session-source.js";
2
+ /**
3
+ * Native session source for Codex (codex CLI / codex_cli_rs) — Cut 3 Task 3.2.
4
+ *
5
+ * `list()` delegates entirely to {@link listCodexSessionsByCwd} (Task 3.1), which already handles
6
+ * cwd filtering, title/lastActiveAt joining against session_index.jsonl, and mtime-keyed caching;
7
+ * this adapter only adds the async `NativeSessionSource.list()` contract and cwd normalization on
8
+ * top of that synchronous building block (mirroring ClaudeSessionSource's shape).
9
+ *
10
+ * `loadTranscript()` reads a single rollout file and keeps only `event_msg` lines whose payload is
11
+ * `user_message` or `agent_message` with `phase: "final_answer"` — see {@link CodexEventMsgLine}'s
12
+ * doc for why every other line/event/payload type in a rollout is intentionally skipped.
13
+ */
14
+ export declare class CodexSessionSource implements NativeSessionSource {
15
+ private readonly opts;
16
+ readonly agentId = "codex";
17
+ readonly kind: "codex";
18
+ /** `opts` is injectable (codexHome/cacheDir) so tests can point at temp directories instead of
19
+ * the real user home — mirrors listCodexSessionsByCwd's own opts shape. */
20
+ constructor(opts?: {
21
+ codexHome?: string;
22
+ cacheDir?: string;
23
+ });
24
+ list(cwd: string, opts?: {
25
+ limit?: number;
26
+ }): Promise<NativeSessionEntry[]>;
27
+ /** Codex has no id->file mapping without the index (id lives in each rollout's session_meta), so
28
+ * membership goes through the mtime-cached listCodexSessionsByCwd (no full re-scan when warm). */
29
+ has(sessionId: string, cwd: string): Promise<boolean>;
30
+ loadTranscript(ref: NativeSessionRef): Promise<TranscriptMessage[]>;
31
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Normalize a cwd for CODEX session-cwd equality comparisons (matching a codex rollout's
3
+ * `session_meta.cwd` against our own workspace cwd). Same rule in spirit as projectId derivation
4
+ * (see agent-paths.ts `deriveProjectId`: resolve + lowercase) — resolve to an absolute path, then
5
+ * lowercase and force forward-slash separators so both `E:\Foo\Bar` and `e:/foo/bar` compare equal.
6
+ * (path.resolve() already normalizes `/` to `\` on win32, so the explicit slash replace below is
7
+ * belt-and-suspenders rather than load-bearing — verified via `path.resolve("e:/xiaozhiclaw/openclaw")`
8
+ * === `"e:\\xiaozhiclaw\\openclaw"` on this machine — but kept explicit so the output format doesn't
9
+ * silently depend on that internal, unstated Node behavior.)
10
+ */
11
+ export declare function normalizeCwd(cwd: string): string;
12
+ /**
13
+ * Encode a cwd into the directory name Claude Code uses under `~/.claude/projects/<encoded>`.
14
+ *
15
+ * This is Claude Code's OWN encoding (not ours) — we must replicate it byte-for-byte to locate
16
+ * existing native claude session directories on disk. It has no public spec, so this was reverse
17
+ * engineered two ways and cross-checked against each other:
18
+ *
19
+ * 1. Real directory names from `ls ~/.claude/projects` on this dev machine (see
20
+ * cwd-normalize.test.ts for the samples, notably a workspace with a literal space in its name
21
+ * — "QA Agent Mention Pool 20260701-1325" — which is what falsified the initially-assumed
22
+ * narrower rule).
23
+ * 2. Decompiling the bundled VS Code extension (anthropic.claude-code-2.1.168,
24
+ * extension/extension.js). The relevant minified functions (renamed here for readability;
25
+ * originals were Hv/yv/pOe/cOe):
26
+ *
27
+ * const MAX_LEN = 200;
28
+ * function hashCode(e) { // Java String.hashCode()-style,
29
+ * let t = 0; // 32-bit signed, wraps via `|0`
30
+ * for (let r = 0; r < e.length; r++) t = (t << 5) - t + e.charCodeAt(r) | 0;
31
+ * return t;
32
+ * }
33
+ * function encodeProjectDirName(e) {
34
+ * const t = e.replace(/[^a-zA-Z0-9]/g, "-");
35
+ * if (t.length <= MAX_LEN) return t;
36
+ * return `${t.slice(0, MAX_LEN)}-${Math.abs(hashCode(e)).toString(36)}`;
37
+ * }
38
+ *
39
+ * Two things this rule does that a naive "just swap \\/:. for -" guess would get wrong:
40
+ * - The replacement set is "everything that isn't [a-zA-Z0-9]", not a fixed 4-character set —
41
+ * spaces, underscores, parens, plus signs, and non-ASCII (e.g. Chinese) characters are all
42
+ * replaced too. Original casing is preserved (no lowercasing, unlike normalizeCwd above).
43
+ * - Encoded names longer than 200 characters are truncated to 200 chars plus a
44
+ * "-<base36 32-bit hash>" suffix, where the hash is computed over the ORIGINAL
45
+ * (pre-replacement) string, not the truncated one.
46
+ *
47
+ * This encoding is lossy/one-way by construction (e.g. `...\\qlogicagent-hub` and
48
+ * `...\\qlogicagent\\hub` both encode to the same name) — it can only be used to look up a
49
+ * directory for a known cwd, never to recover a cwd from a directory name. If Claude Code changes
50
+ * this encoding in a future version, update this function and the fixtures in
51
+ * cwd-normalize.test.ts together.
52
+ */
53
+ export declare function claudeProjectDirName(cwd: string): string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Sweep both native stores. Best-effort: never throws (all I/O is guarded); returns how many rollouts were
3
+ * reclaimed. `home`/`codexHome`/`ttlMs`/`now` are injectable for tests.
4
+ */
5
+ export declare function sweepEphemeralWorktreeRollouts(opts?: {
6
+ home?: string;
7
+ codexHome?: string;
8
+ ttlMs?: number;
9
+ now?: number;
10
+ }): Promise<{
11
+ claudeDeleted: number;
12
+ codexDeleted: number;
13
+ }>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Single source of truth for WHICH third-party ACP backend ids have local, cwd-discoverable
3
+ * native session history ("native-cwd" adapters registered in register-builtin-sources.ts).
4
+ *
5
+ * Consumed by both:
6
+ * - acp-detector.ts (assembleBackendDescriptor) — descriptor's capabilities.sessionHistory
7
+ * - session-handler.ts (resolveAgentSessionHistory) — session.list routing
8
+ *
9
+ * so the two can never drift on which ids are native-adapted (the DRY debt Cut 1 intentionally
10
+ * left behind — see Cut 3 Task 3.0). To add or remove a native-cwd agent: change this set AND
11
+ * register/unregister the matching NativeSessionSource adapter in register-builtin-sources.ts.
12
+ *
13
+ * Zero imports by design — this file must stay a dependency-free leaf so both acp-detector.ts and
14
+ * session-handler.ts (which itself imports ACP_BACKENDS from acp-detector.ts) can import it without
15
+ * risking an import cycle.
16
+ */
17
+ export declare const NATIVE_CWD_BACKEND_IDS: ReadonlySet<string>;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Native session source registry.
3
+ *
4
+ * A "native" session source lets a third-party ACP agent (codex/claude) expose ITS OWN
5
+ * CWD-scoped session history — recorded by the agent's own CLI, not by qlogicagent — through
6
+ * the same session.list / session.get surface as our own sessions.
7
+ *
8
+ * Cut 1 shipped only the registry contract with a loose shape; no adapter registered itself, so
9
+ * {@link getNativeSessionSource} always returned undefined and callers (session-handler's
10
+ * resolveSessionSource) fell back to an empty list for native-cwd agents. Cut 2 refines the
11
+ * contract below and adds the claude adapter (see claude-session-source.ts) that calls
12
+ * {@link registerNativeSessionSource}.
13
+ */
14
+ /** Opaque locator for a single native session's transcript, round-tripped by the frontend without
15
+ * interpretation (session.list returns it, session.loadNativeTranscript consumes it). */
16
+ export interface NativeSessionRef {
17
+ agentId: string;
18
+ kind: "codex" | "claude";
19
+ /** Absolute path to the on-disk transcript file (adapter-specific format). */
20
+ file: string;
21
+ }
22
+ /** One native session list entry — the `source: "native"` variant of session.list's
23
+ * SessionListEntry (see session-types.ts SessionListEntry for the "own" counterpart). */
24
+ export interface NativeSessionEntry {
25
+ sessionId: string;
26
+ source: "native";
27
+ agentId: string;
28
+ title?: string;
29
+ lastActiveAt: string;
30
+ messageCount: number;
31
+ cwd: string;
32
+ nativeRef: NativeSessionRef;
33
+ /** Stamped by handleSessionList from the requested project so the frontend's per-project grouping
34
+ * (project-tree filters by projectId) shows native entries under it. The source discovers by cwd
35
+ * and cannot know the project, so it leaves this unset. */
36
+ projectId?: string;
37
+ }
38
+ /**
39
+ * Minimal transcript-line shape for native session.loadTranscript. This intentionally does NOT
40
+ * import the frontend's `InboundMessagePayload` (ui-next/src/types/gateway.ts) — that type lives
41
+ * in a separate package/repo from this engine and carries many fields irrelevant to a bare
42
+ * transcript line (sessionKey, blocks, attachments, mediaUrl, chatId, ...). Instead this is the
43
+ * minimal subset every native adapter can populate, documented field-for-field against the
44
+ * frontend contract so a caller can widen a TranscriptMessage into an InboundMessagePayload by
45
+ * filling in the rest as optional/absent:
46
+ * - messageId -> InboundMessagePayload.messageId (required)
47
+ * - content -> InboundMessagePayload.content (required)
48
+ * - timestamp -> InboundMessagePayload.timestamp (required)
49
+ * - source -> InboundMessagePayload.source (required, narrowed here to "user"|"assistant"
50
+ * since that's all a transcript line can be)
51
+ * - platform -> InboundMessagePayload.platform (required there; optional here since every
52
+ * current adapter always sets it to "desktop" — kept optional so future adapters
53
+ * aren't forced to fabricate a value)
54
+ */
55
+ export interface TranscriptMessage {
56
+ messageId: string;
57
+ content: string;
58
+ timestamp: string;
59
+ source: "user" | "assistant";
60
+ platform?: string;
61
+ }
62
+ export interface NativeSessionSource {
63
+ readonly agentId: string;
64
+ readonly kind: "codex" | "claude";
65
+ /** `cwd` is the RAW (un-normalized) project cwd — callers (session-handler's
66
+ * resolveSessionSource, via handleSessionList) pass the REQUESTED project's workspaceDir
67
+ * (params.projectId's, when present) straight through, falling back to getActiveProjectRoot()
68
+ * only when no projectId was given. Each adapter is responsible for whatever
69
+ * normalization/encoding its own on-disk storage requires (e.g. claude's
70
+ * claudeProjectDirName preserves original casing). */
71
+ list(cwd: string, opts?: {
72
+ limit?: number;
73
+ }): Promise<NativeSessionEntry[]>;
74
+ /** Cheap existence check: is `sessionId` an existing native session under `cwd` for this agent?
75
+ * Used on a COLD spawn to decide whether to resume it (ACP session/load) instead of creating a
76
+ * fresh session — so a native conversation regains its authoritative history after pool eviction/
77
+ * restart, not only on the one-shot "继续". Optional so test fakes need not implement it. */
78
+ has?(sessionId: string, cwd: string): Promise<boolean>;
79
+ loadTranscript(ref: NativeSessionRef): Promise<TranscriptMessage[]>;
80
+ }
81
+ /** Register a native session source for an agentId. Last registration for a given id wins. */
82
+ export declare function registerNativeSessionSource(source: NativeSessionSource): void;
83
+ /** Look up a registered native session source by agentId. Undefined when none is registered for
84
+ * that id (no adapter registered itself yet, or the agentId has no native adapter at all). */
85
+ export declare function getNativeSessionSource(agentId?: string): NativeSessionSource | undefined;
86
+ /** Remove a registered native session source by agentId. Symmetric with {@link registerNativeSessionSource};
87
+ * exists primarily so tests can deregister a fake adapter afterward without leaking it into other
88
+ * test cases that share this module-level registry. No-op if nothing is registered for that id. */
89
+ export declare function unregisterNativeSessionSource(agentId: string): void;
@@ -0,0 +1 @@
1
+ export declare function cleanNativeTitle(message: string, maxLen?: number): string | undefined;
@@ -0,0 +1 @@
1
+ export declare function registerBuiltinNativeSources(): void;
@@ -65,6 +65,14 @@ export interface MessageDisplayMetadata {
65
65
  chatId?: string;
66
66
  userId?: string;
67
67
  userName?: string;
68
+ /**
69
+ * Presentation kind for a persisted message whose ROLE alone doesn't capture how
70
+ * the UI should render it. Currently: "mid_turn_guidance" — a user-authored message
71
+ * injected into a running turn (运行中引导). It stays role:"user" (the model must see
72
+ * it as the user's words), but the desktop renders it as an agent-side "收到引导"
73
+ * receipt line rather than a standalone user bubble.
74
+ */
75
+ kind?: string;
68
76
  }
69
77
  export interface PersistedChatMessage extends ChatMessage {
70
78
  timestamp?: string;
@@ -43,6 +43,14 @@ export interface PermissionCheckerDeps {
43
43
  sessionId?: string;
44
44
  /** Getter for current turn ID (changes per turn). */
45
45
  getTurnId?: () => string;
46
+ /**
47
+ * Persist a directory allow-rule across restarts (user picked "始终允许此文件夹").
48
+ * Wired by the CLI layer to append into the user-level settings.json; optional so
49
+ * headless/test checkers degrade to session-only memory.
50
+ */
51
+ persistPermissionRule?: (rule: {
52
+ pathPrefix: string;
53
+ }) => void | Promise<void>;
46
54
  }
47
55
  export declare class PermissionChecker {
48
56
  private readonly ruleEngine;
@@ -57,6 +65,7 @@ export declare class PermissionChecker {
57
65
  private readonly auditLogger;
58
66
  private readonly sessionId;
59
67
  private readonly getTurnId;
68
+ private readonly persistPermissionRule;
60
69
  private unregisterHook;
61
70
  /** Tool meta cache — populated from ToolDefinition[] at agent creation */
62
71
  private toolMetaCache;
@@ -1,6 +1,13 @@
1
1
  /**
2
2
  * Settings watcher for canonical qlogicagent permission settings.
3
3
  *
4
+ * Two layers (CC settings semantics):
5
+ * - user level ~/.qlogicagent/profiles/<owner>/settings.json — where config.update persists
6
+ * - project level <projectRoot>/.qlogicagent/settings.json — repo-local overrides
7
+ * The project file's `permissions` object, when present, overrides the user file's.
8
+ * Both layers are watched; either changing re-resolves the effective permissions.
9
+ * (写读同源:此前 config.update 写用户级、watcher 只读项目级,重启后持久模式永远丢失。)
10
+ *
4
11
  * Only the `permissions` object is accepted. Removed top-level aliases such as
5
12
  * `permissionMode`, `permissionRules`, and `defaultBehavior` must fail loudly.
6
13
  */
@@ -154,6 +154,12 @@ export declare class AcpServer {
154
154
  * Returns a promise that resolves with the host's chosen option.
155
155
  */
156
156
  requestPermission(params: AcpPermissionRequestParams): Promise<AcpPermissionRequestResult>;
157
+ /**
158
+ * Cancel every in-flight permission request (turn abort / session cancel). Each
159
+ * awaiter resolves to "deny" and its maps are cleared, so a cancelled turn's
160
+ * pending approval can't survive to poison the next turn.
161
+ */
162
+ cancelAllPendingPermissions(): void;
157
163
  /** Whether the host supports extended events */
158
164
  get supportsExtendedEvents(): boolean;
159
165
  /** Whether the host supports extended methods */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qlogicagent",
3
- "version": "2.17.10",
3
+ "version": "2.18.1",
4
4
  "description": "XiaozhiClaw Agent CLI — subprocess architecture (JSON-RPC over stdio)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",