qlogicagent 2.17.10 → 2.18.2

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 (70) hide show
  1. package/dist/agent.js +25 -21
  2. package/dist/cli.js +1 -1
  3. package/dist/index.js +451 -435
  4. package/dist/protocol.js +1 -1
  5. package/dist/skills/mcp/astraclaw-native-mcp-server.js +6 -5
  6. package/dist/types/agent/tool-loop/compression-pipeline.d.ts +2 -0
  7. package/dist/types/agent/tool-loop/loop-helpers.d.ts +1 -0
  8. package/dist/types/agent/tool-loop/tool-failure-policy.d.ts +13 -1
  9. package/dist/types/agent/tool-loop.d.ts +14 -0
  10. package/dist/types/agent/types.d.ts +7 -0
  11. package/dist/types/cli/handlers/agents-handler.d.ts +9 -0
  12. package/dist/types/cli/handlers/config-handler.d.ts +5 -0
  13. package/dist/types/cli/handlers/files-handler.d.ts +7 -0
  14. package/dist/types/cli/handlers/session-handler.d.ts +43 -0
  15. package/dist/types/cli/handlers/turn-handler.d.ts +12 -0
  16. package/dist/types/cli/handlers/workflow-handler.d.ts +1 -0
  17. package/dist/types/cli/permission-rule-persistence.d.ts +16 -0
  18. package/dist/types/cli/stdio-rpc-handler-hosts.d.ts +4 -0
  19. package/dist/types/cli/stdio-server.d.ts +16 -1
  20. package/dist/types/cli/turn-core.d.ts +2 -0
  21. package/dist/types/orchestration/agent-instance.d.ts +11 -1
  22. package/dist/types/orchestration/agent-roster.d.ts +12 -0
  23. package/dist/types/orchestration/dag-scheduler.d.ts +3 -0
  24. package/dist/types/orchestration/delegation-coordinator.d.ts +16 -1
  25. package/dist/types/orchestration/goal-loop-coordinator.d.ts +4 -1
  26. package/dist/types/orchestration/goal-mode-adapters.d.ts +3 -1
  27. package/dist/types/orchestration/goal-run-types.d.ts +2 -0
  28. package/dist/types/orchestration/product-budget.d.ts +13 -0
  29. package/dist/types/orchestration/product-persistence.d.ts +4 -0
  30. package/dist/types/orchestration/product-planner.d.ts +7 -2
  31. package/dist/types/orchestration/product-run-coordinator.d.ts +1 -0
  32. package/dist/types/orchestration/workflow/budget-permission-gate.d.ts +9 -0
  33. package/dist/types/orchestration/workflow/node-schema.d.ts +4 -1
  34. package/dist/types/orchestration/workflow/workflow-trigger.d.ts +5 -1
  35. package/dist/types/protocol/wire/acp-agent-management.d.ts +16 -0
  36. package/dist/types/protocol/wire/agent-events.d.ts +2 -2
  37. package/dist/types/protocol/wire/agent-methods.d.ts +3 -0
  38. package/dist/types/protocol/wire/gateway-rpc.d.ts +1 -0
  39. package/dist/types/runtime/execution/streaming-tool-executor.d.ts +2 -0
  40. package/dist/types/runtime/execution/tool-result-storage.d.ts +6 -4
  41. package/dist/types/runtime/infra/acp-detector.d.ts +35 -5
  42. package/dist/types/runtime/infra/acp-protocol-adapter.d.ts +23 -1
  43. package/dist/types/runtime/infra/agent-process.d.ts +15 -0
  44. package/dist/types/runtime/infra/external-agent-pool.d.ts +12 -0
  45. package/dist/types/runtime/infra/llmrouter-catalog.d.ts +26 -6
  46. package/dist/types/runtime/infra/model-registry.d.ts +43 -2
  47. package/dist/types/runtime/infra/user-dirs.d.ts +21 -0
  48. package/dist/types/runtime/permission-model.d.ts +1 -1
  49. package/dist/types/runtime/ports/agent-runtime-ports.d.ts +4 -1
  50. package/dist/types/runtime/ports/permission-contracts.d.ts +5 -2
  51. package/dist/types/runtime/prompt/environment-context.d.ts +10 -0
  52. package/dist/types/runtime/prompt/fresh-workspace-evidence.d.ts +1 -4
  53. package/dist/types/runtime/prompt/identity-section.d.ts +1 -1
  54. package/dist/types/runtime/session/inbound-upload-persistence.d.ts +8 -0
  55. package/dist/types/runtime/session/native/claude-session-source.d.ts +23 -0
  56. package/dist/types/runtime/session/native/codex-index.d.ts +21 -0
  57. package/dist/types/runtime/session/native/codex-session-source.d.ts +31 -0
  58. package/dist/types/runtime/session/native/cwd-normalize.d.ts +53 -0
  59. package/dist/types/runtime/session/native/ephemeral-rollout-sweep.d.ts +13 -0
  60. package/dist/types/runtime/session/native/native-cwd-agents.d.ts +17 -0
  61. package/dist/types/runtime/session/native/native-session-source.d.ts +89 -0
  62. package/dist/types/runtime/session/native/native-title.d.ts +1 -0
  63. package/dist/types/runtime/session/native/register-builtin-sources.d.ts +1 -0
  64. package/dist/types/runtime/session/session-resource-key.d.ts +28 -0
  65. package/dist/types/runtime/session/session-transcript-store.d.ts +2 -0
  66. package/dist/types/runtime/session/session-types.d.ts +37 -0
  67. package/dist/types/skills/permissions/hook-runner.d.ts +9 -0
  68. package/dist/types/skills/permissions/settings-watcher.d.ts +7 -0
  69. package/dist/types/transport/acp-server.d.ts +6 -0
  70. package/package.json +4 -3
@@ -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;
@@ -39,6 +39,8 @@ export interface StreamingToolExecutorPortConfig {
39
39
  signal?: AbortSignal;
40
40
  maxConcurrentTools?: number;
41
41
  maxExecutionMs?: number;
42
+ /** Turn workspace/cwd — routes oversized tool-result spillover to the session's reachable data dir. */
43
+ projectRoot?: string;
42
44
  }
43
45
  export interface ContextCompressionEnginePort {
44
46
  compressAsync(messages: RuntimeCompressibleMessage[], budget: number, context: {
@@ -52,9 +54,10 @@ export interface ToolLoopRuntimePorts {
52
54
  role: string;
53
55
  content?: string | unknown;
54
56
  tool_call_id?: string;
55
- }>, state: unknown, sessionId: string): Promise<{
57
+ }>, state: unknown, sessionId: string, projectRoot: string | undefined): Promise<{
56
58
  messages: unknown[];
57
59
  newlyReplacedCount: number;
60
+ newlyPersistedBytes?: number;
58
61
  }>;
59
62
  getActiveContextCompressionEngine(): ContextCompressionEnginePort | null;
60
63
  compressMessages(messages: RuntimeCompressibleMessage[], options: {
@@ -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;
@@ -32,6 +32,16 @@ export interface ShellInfo {
32
32
  psEdition?: "5.1" | "7+";
33
33
  isGitBashOnWindows?: boolean;
34
34
  }
35
+ /**
36
+ * Dynamic, platform-specific shell guidance lines appended to `# Environment`.
37
+ *
38
+ * When `shellInfo` is present it reflects the REAL active shell (edition +
39
+ * Git-Bash-on-Windows), so the advice matches what commands will actually run
40
+ * against. When absent, we fall back to the env-guessed `shellType` and keep the
41
+ * old one-line PowerShell/CMD note (never crashes, never over-claims an edition
42
+ * we don't know).
43
+ */
44
+ export declare function shellGuidanceLines(shellInfo: ShellInfo | undefined, shellType: ShellType): string[];
35
45
  /**
36
46
  * Create a system prompt section that provides environment context.
37
47
  * Memoized — computed once per session (environment doesn't change mid-session).
@@ -11,14 +11,11 @@ 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;
20
- /** 显式构建/创建类请求(做个游戏/网站/app…)。命中 → 第一回合就强制工具调用。 */
21
- export declare function looksLikeBuildIntent(messages: readonly PromptMessageLike[] | string): boolean;
22
19
  export declare function isLightweightChatTurn(messages: readonly PromptMessageLike[] | string): boolean;
23
20
  export declare function isExplicitNoToolTurn(messages: readonly PromptMessageLike[] | string): boolean;
24
21
  /**
@@ -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;
@@ -15,6 +15,14 @@ import type { ChatMessage } from "../../protocol/wire/index.js";
15
15
  export interface PersistedUpload {
16
16
  url: string;
17
17
  localPath?: string;
18
+ /**
19
+ * Whether the bytes were actually written to durable storage. `true` on success, `false` when a
20
+ * transport upload was found but persisting it FAILED (fetch error / non-ok / write error) — the
21
+ * returned `url` is then the short-lived transport URL that dies with the turn. `undefined` means
22
+ * the input was not a transport upload to begin with (already-persisted / remote / file://), which
23
+ * is not a failure. Honesty over silence: callers use `false` to warn instead of pretending it saved.
24
+ */
25
+ persisted?: boolean;
18
26
  }
19
27
  /** 同 persistInboundUploadUrl,但返回持久化后的本地绝对路径(供模型直接 read/exec)。 */
20
28
  export declare function persistInboundUploadUrlDetailed(url: string, projectId: string, sessionId: string): Promise<PersistedUpload>;
@@ -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;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Single source of truth for session resource keys (canonical identity) and the derived
3
+ * resident-pool / delegation keys. Forbids scattered ad-hoc string concatenation of session keys
4
+ * (invariant I2, docs/session-identity-alignment-spec.md). Leaf module — imports nothing.
5
+ */
6
+ /** Canonical session identity: our-minted, stable-from-creation id + its owning agent + project. */
7
+ export interface SessionResource {
8
+ /** Owning agent kind: own ("qlogicagent") / codex / claude / … (= SessionMetadata.agentId). */
9
+ agentId: string;
10
+ /** Owning project id (path-derived / opaque; not minted here). */
11
+ projectId: string;
12
+ /** Our-minted session id, stable from creation — never the native Y (Y lives in nativeBinding). */
13
+ canonicalSessionId: string;
14
+ }
15
+ /** Serialize the canonical session resource to its single opaque key string. */
16
+ export declare function buildSessionResourceKey(r: SessionResource): string;
17
+ /** Parse a key produced by {@link buildSessionResourceKey}; throws on a malformed key. */
18
+ export declare function parseSessionResourceKey(key: string): SessionResource;
19
+ /**
20
+ * Resident external-agent pool key: same agent + workspace + session ⇒ same resident process.
21
+ * `sessionId` may be a canonical id, the WARM_SESSION token, or undefined (defaults to "default").
22
+ * Byte-identical to the historical inline pool key it replaces.
23
+ */
24
+ export declare function buildAgentPoolKey(agentId: string, cwd: string, sessionId?: string): string;
25
+ /** Prefix matching every pool key for `agentId` (agent-scoped eviction, exact id-boundary). */
26
+ export declare function agentPoolKeyPrefix(agentId: string): string;
27
+ /** @mention delegation serialization key for one (agent × conversation) pair. */
28
+ export declare function buildConvKey(agentId: string, canonicalSessionId: string): string;
@@ -3,6 +3,8 @@ import type { MessageDisplayMetadata, PersistedChatMessage, TokenUsage } from ".
3
3
  export interface TranscriptLoadResult {
4
4
  messages: PersistedChatMessage[];
5
5
  corruptLines: number;
6
+ /** 1-based line numbers that failed to parse (bounded to the first MAX_REPORTED_CORRUPT_LINES). */
7
+ corruptLineNumbers: number[];
6
8
  }
7
9
  export declare function appendMessage(sessionId: string, message: ChatMessage, projectRoot: string, turnId?: string, metadata?: {
8
10
  usage?: TokenUsage;
@@ -18,6 +18,25 @@ export interface InflightTurnMarker {
18
18
  /** 该轮是否已执行过有副作用的工具(写文件/发消息/外部 API)。安全档不据此自动续,仅用于 UI"可能重复"提示 + 为智能档铺路。 */
19
19
  hadSideEffectTool?: boolean;
20
20
  }
21
+ /**
22
+ * Native (codex/claude) session binding — present only on native sessions, written once the
23
+ * external agent reports its own session id (Y). Our `sessionId` stays the canonical identity;
24
+ * `nativeSessionId` is an ATTRIBUTE used ONLY when talking to the external CLI (ACP session/load),
25
+ * never as a map/pool/list key. `nativeRef` independently locates the external transcript file so
26
+ * content loads (and the binding is recoverable) without relying on `sessionId === nativeSessionId`.
27
+ */
28
+ export interface NativeBinding {
29
+ /** External agent's own session id (Y) — used only for ACP session/load resume. */
30
+ nativeSessionId: string;
31
+ /** Locator for the external transcript file (loadTranscript + binding-loss fallback). */
32
+ nativeRef: {
33
+ agentId: string;
34
+ kind: "codex" | "claude";
35
+ file: string;
36
+ };
37
+ /** ISO time the binding was established (first turn the external agent reported Y). */
38
+ boundAt: string;
39
+ }
21
40
  export interface SessionMetadata {
22
41
  schemaVersion?: number;
23
42
  sessionId: string;
@@ -52,10 +71,18 @@ export interface SessionMetadata {
52
71
  interruptedTurnCount?: number;
53
72
  /** 达熔断阈值放弃恢复的时间戳(UI 显示"多次中断已停止自动恢复")。 */
54
73
  recoveryAbandonedAt?: string;
74
+ /** native(codex/claude)会话绑定:外部 Y + nativeRef;own 会话恒 undefined。见 {@link NativeBinding}。 */
75
+ nativeBinding?: NativeBinding;
55
76
  }
56
77
  export interface PersistedSession {
57
78
  metadata: SessionMetadata;
58
79
  messages: PersistedChatMessage[];
80
+ /**
81
+ * Count of transcript lines that failed to parse and were skipped on load. Surfaced so the UI can
82
+ * tell the user "N 条消息无法读取" instead of silently resuming a conversation with holes in it.
83
+ * Omitted/0 for a clean transcript.
84
+ */
85
+ corruptLines?: number;
59
86
  }
60
87
  export interface MessageDisplayMetadata {
61
88
  messageId?: string;
@@ -65,6 +92,14 @@ export interface MessageDisplayMetadata {
65
92
  chatId?: string;
66
93
  userId?: string;
67
94
  userName?: string;
95
+ /**
96
+ * Presentation kind for a persisted message whose ROLE alone doesn't capture how
97
+ * the UI should render it. Currently: "mid_turn_guidance" — a user-authored message
98
+ * injected into a running turn (运行中引导). It stays role:"user" (the model must see
99
+ * it as the user's words), but the desktop renders it as an agent-side "收到引导"
100
+ * receipt line rather than a standalone user bubble.
101
+ */
102
+ kind?: string;
68
103
  }
69
104
  export interface PersistedChatMessage extends ChatMessage {
70
105
  timestamp?: string;
@@ -88,6 +123,8 @@ export interface SessionListEntry {
88
123
  groupKey?: string;
89
124
  previousSessionId?: string;
90
125
  carryoverSummary?: string | null;
126
+ /** N4: native (codex/claude) binding, surfaced so session.list can shape source:"native" + nativeRef. */
127
+ nativeBinding?: NativeBinding;
91
128
  }
92
129
  export interface TaskSummaryDeps {
93
130
  transport: import("../ports/index.js").LLMTransport;