@co0ontty/wand 4.42.1 → 4.43.0

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "f1421de43ef92ef52af6727276871ae4bdec207e",
3
- "builtAt": "2026-08-13T13:38:41.979Z",
4
- "version": "4.42.1",
2
+ "commit": "c05b4f15f592f2c4f335df60c46208a7e0ea82fc",
3
+ "builtAt": "2026-08-22T00:47:50.129Z",
4
+ "version": "4.43.0",
5
5
  "channel": "stable"
6
6
  }
package/dist/cli.js CHANGED
@@ -177,7 +177,6 @@ async function main() {
177
177
  case "session:read":
178
178
  case "session:send":
179
179
  case "session:wait":
180
- case "inbox:list":
181
180
  case "mission:list":
182
181
  case "mission:create":
183
182
  case "mission:diff":
@@ -228,7 +227,6 @@ Agent runtime:
228
227
  wand session:read <id> Read a complete session snapshot
229
228
  wand session:send <id> <text>
230
229
  wand session:wait <id> [--timeout 900]
231
- wand inbox:list List global Agent Inbox items
232
230
  wand mission:list List parallel tasks
233
231
  wand mission:create --prompt <text> --cwd <path> --providers claude,codex
234
232
  wand mission:diff <mission-id> <attempt-id>
@@ -286,7 +284,6 @@ async function runAgentCliCommand(command, args, configPath) {
286
284
  await new Promise((resolve) => setTimeout(resolve, 500));
287
285
  }
288
286
  }
289
- case "inbox:list": return output(await api.get("/api/inbox"));
290
287
  case "mission:list": return output(await api.get("/api/missions"));
291
288
  case "mission:create": {
292
289
  const prompt = required(readFlagValue(args, "--prompt"), "wand mission:create --prompt <text> --cwd <path> --providers claude,codex");
@@ -16,6 +16,8 @@ export interface ResolvedDistributionAsset {
16
16
  size: number;
17
17
  source: "local" | "github";
18
18
  releaseNotes?: string;
19
+ /** 本地分发文件的 SHA-256(hex);GitHub 来源不提供,客户端据此跳过校验。 */
20
+ sha256?: string;
19
21
  }
20
22
  export interface DistributionSettings {
21
23
  androidApk: Record<string, unknown>;
@@ -45,6 +47,12 @@ export declare class DistributionManager {
45
47
  resolveLatestDmg(): Promise<ResolvedDistributionAsset | null>;
46
48
  resolveMacosDownload(): Promise<LocalDistributionAsset | null>;
47
49
  getSettings(): Promise<DistributionSettings>;
50
+ private readonly sha256Cache;
51
+ /**
52
+ * 计算本地分发文件的 SHA-256(hex)。按 path + size + mtime 缓存:更新检查
53
+ * 可能被多个客户端频繁触发,避免每次都全量读盘。
54
+ */
55
+ private computeLocalFileSha256;
48
56
  private refreshConfig;
49
57
  private resolveLocalAsset;
50
58
  private readLocalAsset;
@@ -1,3 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
1
3
  import { mkdir, readdir, readFile, stat } from "node:fs/promises";
2
4
  import path from "node:path";
3
5
  import { compareApkInstallOrder, compareSemver, extractSemver } from "./version-utils.js";
@@ -48,10 +50,27 @@ export class DistributionManager {
48
50
  source: "local",
49
51
  } : null;
50
52
  const github = githubApk ? { ...githubApk, source: "github" } : null;
53
+ let winner;
51
54
  if (local && github) {
52
- return compareApkInstallOrder(github.version, local.version) > 0 ? github : local;
55
+ winner = compareApkInstallOrder(github.version, local.version) > 0 ? github : local;
53
56
  }
54
- return local ?? github;
57
+ else {
58
+ winner = local ?? github;
59
+ }
60
+ // 本地分发的 APK 附带 SHA-256,客户端下载后校验(GitHub 来源走正规 TLS,不提供)。
61
+ if (winner?.source === "local" && localApk) {
62
+ try {
63
+ const fileStat = await stat(localApk.filePath);
64
+ winner = {
65
+ ...winner,
66
+ sha256: await this.computeLocalFileSha256(localApk.filePath, fileStat.size, fileStat.mtimeMs),
67
+ };
68
+ }
69
+ catch {
70
+ // 哈希计算失败不阻断更新检查;客户端拿到空 sha256 会跳过校验。
71
+ }
72
+ }
73
+ return winner;
55
74
  }
56
75
  async resolveAndroidDownload(channel = "beta") {
57
76
  await this.refreshConfig();
@@ -109,6 +128,26 @@ export class DistributionManager {
109
128
  macosDmg: this.buildSettings("dmg", dmgDir, this.options.config.macos?.enabled === true, localDmg, githubDmg),
110
129
  };
111
130
  }
131
+ sha256Cache = new Map();
132
+ /**
133
+ * 计算本地分发文件的 SHA-256(hex)。按 path + size + mtime 缓存:更新检查
134
+ * 可能被多个客户端频繁触发,避免每次都全量读盘。
135
+ */
136
+ async computeLocalFileSha256(filePath, size, mtimeMs) {
137
+ const key = `${size}:${mtimeMs}`;
138
+ const cached = this.sha256Cache.get(filePath);
139
+ if (cached && cached.key === key)
140
+ return cached.digest;
141
+ const digest = await new Promise((resolve, reject) => {
142
+ const hash = createHash("sha256");
143
+ const stream = createReadStream(filePath);
144
+ stream.on("error", reject);
145
+ stream.on("data", (chunk) => hash.update(chunk));
146
+ stream.on("end", () => resolve(hash.digest("hex")));
147
+ });
148
+ this.sha256Cache.set(filePath, { key, digest });
149
+ return digest;
150
+ }
112
151
  async refreshConfig() {
113
152
  let raw;
114
153
  try {
@@ -1,11 +1,11 @@
1
- import type { AgentActivityItem, CreateMissionInput, CreateReviewCommentInput, MissionDetails, MissionDiff, MissionReviewComment } from "./mission-types.js";
1
+ import type { CreateMissionInput, CreateReviewCommentInput, MissionDetails, MissionDiff, MissionReviewComment } from "./mission-types.js";
2
2
  import type { SessionRegistry } from "./session-registry.js";
3
3
  import type { StructuredSessionManager } from "./structured-session-manager.js";
4
4
  import type { WandStorage } from "./storage.js";
5
5
  import type { ProcessEvent } from "./types.js";
6
6
  /**
7
- * Deep task-orchestration module. Callers create missions, observe one inbox,
8
- * and review diffs without coordinating session/worktree/storage details.
7
+ * Deep task-orchestration module. Callers create missions and review diffs
8
+ * without coordinating session/worktree/storage details.
9
9
  */
10
10
  export declare class Missions {
11
11
  private readonly storage;
@@ -14,17 +14,14 @@ export declare class Missions {
14
14
  constructor(storage: WandStorage, structured: StructuredSessionManager, sessions: SessionRegistry);
15
15
  list(): MissionDetails[];
16
16
  get(id: string): MissionDetails | null;
17
- inbox(): AgentActivityItem[];
18
- markInboxRead(sessionId?: string): void;
19
17
  create(input: CreateMissionInput): MissionDetails;
20
- ingest(event: ProcessEvent): AgentActivityItem | null;
18
+ ingest(event: ProcessEvent): void;
21
19
  diff(missionId: string, attemptId: string): MissionDiff;
22
20
  addReviewComment(missionId: string, attemptId: string, input: CreateReviewCommentInput): MissionReviewComment;
23
21
  sendReview(missionId: string, attemptId: string, commentIds?: string[]): MissionReviewComment[];
24
22
  resolveReview(missionId: string, attemptId: string, commentIds: string[]): MissionReviewComment[];
25
23
  archive(id: string): MissionDetails;
26
24
  private dispatchAttempt;
27
- private seedInbox;
28
25
  private details;
29
26
  private getIncludingArchived;
30
27
  private requireAttempt;
package/dist/missions.js CHANGED
@@ -94,8 +94,8 @@ function reviewPrompt(comments) {
94
94
  ].join("\n");
95
95
  }
96
96
  /**
97
- * Deep task-orchestration module. Callers create missions, observe one inbox,
98
- * and review diffs without coordinating session/worktree/storage details.
97
+ * Deep task-orchestration module. Callers create missions and review diffs
98
+ * without coordinating session/worktree/storage details.
99
99
  */
100
100
  export class Missions {
101
101
  storage;
@@ -113,13 +113,6 @@ export class Missions {
113
113
  const mission = this.storage.getMission(id);
114
114
  return mission ? this.details(mission) : null;
115
115
  }
116
- inbox() {
117
- this.seedInbox();
118
- return this.storage.listAgentActivity();
119
- }
120
- markInboxRead(sessionId) {
121
- this.storage.markAgentActivityRead(sessionId);
122
- }
123
116
  create(input) {
124
117
  const prompt = input.prompt?.trim();
125
118
  if (!prompt)
@@ -156,38 +149,23 @@ export class Missions {
156
149
  }
157
150
  ingest(event) {
158
151
  if (!event.sessionId || event.sessionId === "__system__")
159
- return null;
152
+ return;
160
153
  const snapshot = this.sessions.getLatest(event.sessionId);
161
154
  if (!snapshot)
162
- return null;
155
+ return;
163
156
  const attempt = this.storage.getMissionAttemptBySession(event.sessionId);
164
- const mission = attempt ? this.storage.getMission(attempt.missionId) : null;
157
+ if (!attempt)
158
+ return;
165
159
  const state = activityState(snapshot, event);
166
160
  const updatedAt = nowIso();
167
- const item = {
168
- sessionId: snapshot.id,
169
- missionId: attempt?.missionId ?? null,
170
- attemptId: attempt?.id ?? null,
171
- state,
172
- title: mission?.title || snapshot.title || `${snapshot.provider ?? "agent"} 会话`,
161
+ this.storage.saveMissionAttempt({
162
+ ...attempt,
163
+ state: state,
173
164
  summary: sessionSummary(snapshot),
174
- provider: snapshot.provider ?? snapshot.structuredState?.provider ?? null,
175
- cwd: snapshot.cwd || null,
165
+ error: state === "failed" ? snapshot.structuredState?.lastError ?? "任务执行失败" : null,
176
166
  updatedAt,
177
- readAt: null,
178
- };
179
- this.storage.upsertAgentActivity(item);
180
- if (attempt) {
181
- this.storage.saveMissionAttempt({
182
- ...attempt,
183
- state: state,
184
- summary: item.summary,
185
- error: state === "failed" ? snapshot.structuredState?.lastError ?? "任务执行失败" : null,
186
- updatedAt,
187
- });
188
- this.refreshMissionStatus(attempt.missionId);
189
- }
190
- return item;
167
+ });
168
+ this.refreshMissionStatus(attempt.missionId);
191
169
  }
192
170
  diff(missionId, attemptId) {
193
171
  const attempt = this.requireAttempt(missionId, attemptId);
@@ -277,11 +255,6 @@ export class Missions {
277
255
  updatedAt: nowIso(),
278
256
  };
279
257
  this.storage.saveMissionAttempt(attempt);
280
- this.storage.upsertAgentActivity({
281
- sessionId: session.id, missionId: mission.id, attemptId, state: "working",
282
- title: mission.title, summary: `已分派给 ${provider}`, provider, cwd: session.cwd,
283
- updatedAt: attempt.updatedAt, readAt: null,
284
- });
285
258
  const completion = this.structured.sendMessage(session.id, mission.prompt);
286
259
  completion.catch((error) => {
287
260
  console.error(`[Missions] Attempt ${attemptId} failed after dispatch:`, error);
@@ -296,28 +269,6 @@ export class Missions {
296
269
  });
297
270
  }
298
271
  }
299
- seedInbox() {
300
- const existing = new Set(this.storage.listAgentActivity().map((item) => item.sessionId));
301
- for (const snapshot of this.sessions.listSlim()) {
302
- if (existing.has(snapshot.id))
303
- continue;
304
- const attempt = this.storage.getMissionAttemptBySession(snapshot.id);
305
- const mission = attempt ? this.storage.getMission(attempt.missionId) : null;
306
- const at = nowIso();
307
- this.storage.upsertAgentActivity({
308
- sessionId: snapshot.id,
309
- missionId: attempt?.missionId ?? null,
310
- attemptId: attempt?.id ?? null,
311
- state: activityState(snapshot),
312
- title: mission?.title || snapshot.title || `${snapshot.provider ?? "agent"} 会话`,
313
- summary: sessionSummary(snapshot),
314
- provider: snapshot.provider ?? snapshot.structuredState?.provider ?? null,
315
- cwd: snapshot.cwd || null,
316
- updatedAt: snapshot.endedAt || snapshot.startedAt || at,
317
- readAt: at,
318
- });
319
- }
320
- }
321
272
  details(mission) {
322
273
  return {
323
274
  ...mission,
@@ -369,7 +369,11 @@ function listOpenCodeSessionCandidates() {
369
369
  let db = null;
370
370
  try {
371
371
  db = new DatabaseSync(dbPath, { readOnly: true });
372
- const rows = db.prepare("SELECT id, directory, time_created, time_updated FROM session ORDER BY time_updated DESC LIMIT 500").all();
372
+ const sessionColumns = db.prepare("PRAGMA table_info(session)").all();
373
+ const rootSessionFilter = sessionColumns.some((column) => column.name === "parent_id")
374
+ ? "WHERE parent_id IS NULL"
375
+ : "";
376
+ const rows = db.prepare(`SELECT id, directory, time_created, time_updated FROM session ${rootSessionFilter} ORDER BY time_updated DESC LIMIT 500`).all();
373
377
  return rows.flatMap((row) => {
374
378
  if (typeof row.id !== "string" || typeof row.directory !== "string")
375
379
  return [];
@@ -968,12 +972,16 @@ export class ProcessManager extends EventEmitter {
968
972
  let priorMessages = [];
969
973
  let inheritedSessionSource;
970
974
  let inheritedAutomationId;
975
+ let inheritedWorkspaceId;
976
+ let inheritedWorkspaceTaskId;
971
977
  if (opts?.reuseId) {
972
978
  const oldRecord = this.sessions.get(id);
973
979
  if (oldRecord) {
974
980
  priorMessages = oldRecord.ptyBridge?.getMessages() ?? oldRecord.messages ?? [];
975
981
  inheritedSessionSource = oldRecord.sessionSource;
976
982
  inheritedAutomationId = oldRecord.automationId;
983
+ inheritedWorkspaceId = oldRecord.workspaceId;
984
+ inheritedWorkspaceTaskId = oldRecord.workspaceTaskId;
977
985
  this.cleanupRecord(oldRecord);
978
986
  this.sessions.delete(id);
979
987
  }
@@ -982,6 +990,8 @@ export class ProcessManager extends EventEmitter {
982
990
  priorMessages = stored?.messages ?? [];
983
991
  inheritedSessionSource = stored?.sessionSource;
984
992
  inheritedAutomationId = stored?.automationId;
993
+ inheritedWorkspaceId = stored?.workspaceId;
994
+ inheritedWorkspaceTaskId = stored?.workspaceTaskId;
985
995
  }
986
996
  this.terminalHost.forget(id);
987
997
  }
@@ -1032,8 +1042,8 @@ export class ProcessManager extends EventEmitter {
1032
1042
  id,
1033
1043
  sessionSource: opts?.sessionSource ?? inheritedSessionSource ?? "interactive",
1034
1044
  automationId: opts?.automationId ?? inheritedAutomationId,
1035
- workspaceId: opts?.workspaceId,
1036
- workspaceTaskId: opts?.workspaceTaskId,
1045
+ workspaceId: opts?.workspaceId ?? inheritedWorkspaceId,
1046
+ workspaceTaskId: opts?.workspaceTaskId ?? inheritedWorkspaceTaskId,
1037
1047
  provider,
1038
1048
  command,
1039
1049
  cwd: resolvedCwd,
@@ -2352,10 +2362,8 @@ export class ProcessManager extends EventEmitter {
2352
2362
  const escapedModel = trimmedModel.replace(/'/g, "'\\''");
2353
2363
  result += ` --model '${escapedModel}'`;
2354
2364
  }
2355
- const variant = thinkingEffortToOpenCodeVariant(thinkingEffort ?? null);
2356
- if (variant && !/--variant(?:\s|=)/.test(result)) {
2357
- result += ` --variant '${variant.replace(/'/g, "'\\''")}'`;
2358
- }
2365
+ // thinkingEffort → --variant 仅适用于 `opencode run`(结构化 runner);
2366
+ // 交互 TUI(裸 `opencode`)没有该选项,注入会导致 CLI 直接报错退出。
2359
2367
  if ((mode === "managed" || mode === "full-access" || mode === "auto-edit") && !/--auto(?:\s|$)/.test(result)) {
2360
2368
  result += " --auto";
2361
2369
  }
@@ -434,6 +434,10 @@ export class ProviderHistoryScanner {
434
434
  let database = null;
435
435
  try {
436
436
  database = new DatabaseSync(this.openCodeDatabasePath, { readOnly: true });
437
+ const sessionColumns = database.prepare("PRAGMA table_info(session)").all();
438
+ const rootSessionFilter = sessionColumns.some((column) => column.name === "parent_id")
439
+ ? "WHERE session.parent_id IS NULL"
440
+ : "";
437
441
  const rows = database.prepare(`
438
442
  SELECT
439
443
  session.id AS id,
@@ -445,6 +449,7 @@ export class ProviderHistoryScanner {
445
449
  SUM(CASE WHEN json_extract(message.data, '$.role') = 'assistant' THEN 1 ELSE 0 END) AS assistant_count
446
450
  FROM session
447
451
  LEFT JOIN message ON message.session_id = session.id
452
+ ${rootSessionFilter}
448
453
  GROUP BY session.id
449
454
  ORDER BY session.time_updated DESC
450
455
  LIMIT 1000
@@ -6,11 +6,9 @@ function sendMissionError(res, error) {
6
6
  }
7
7
  export function registerMissionRoutes(app, missions) {
8
8
  app.get("/api/inbox", (_req, res) => {
9
- res.json({ items: missions.inbox() });
9
+ res.json({ items: [] });
10
10
  });
11
- app.post("/api/inbox/read", (req, res) => {
12
- const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : undefined;
13
- missions.markInboxRead(sessionId);
11
+ app.post("/api/inbox/read", (_req, res) => {
14
12
  res.json({ ok: true });
15
13
  });
16
14
  app.get("/api/missions", (_req, res) => {
@@ -37,13 +37,6 @@ export type SessionListPageEntry = {
37
37
  key: string;
38
38
  sortTimestamp: number;
39
39
  session: ReturnType<typeof toSessionListItemDTO>;
40
- } | {
41
- type: "recoverable";
42
- key: string;
43
- sortTimestamp: number;
44
- history: ProviderHistorySession & {
45
- provider: "claude" | "codex" | "opencode" | "qoder";
46
- };
47
40
  };
48
41
  export interface SessionListPage {
49
42
  entries: SessionListPageEntry[];
@@ -60,9 +53,9 @@ export interface SessionDirectoryTreeResponse {
60
53
  /** Full directory-tree revision, including custom workspace names. */
61
54
  treeRevision: string;
62
55
  }
63
- export declare function buildSessionListEntries(sessions: SessionSnapshot[], claudeHistory: ProviderHistorySession[], codexHistory: ProviderHistorySession[], hiddenHistoryIds: Set<string>, openCodeHistory?: ProviderHistorySession[], qoderHistory?: ProviderHistorySession[]): SessionListPageEntry[];
64
- export declare function buildSessionListPage(sessions: SessionSnapshot[], claudeHistory: ProviderHistorySession[], codexHistory: ProviderHistorySession[], hiddenHistoryIds: Set<string>, offset: number, limit: number, openCodeHistory?: ProviderHistorySession[], qoderHistory?: ProviderHistorySession[]): SessionListPage;
65
- export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], claudeHistory: ProviderHistorySession[], codexHistory: ProviderHistorySession[], hiddenHistoryIds: Set<string>, openCodeHistory?: ProviderHistorySession[], qoderHistory?: ProviderHistorySession[], customNames?: ReadonlyMap<string, string>): SessionDirectoryTreeResponse;
56
+ export declare function buildSessionListEntries(sessions: SessionSnapshot[]): SessionListPageEntry[];
57
+ export declare function buildSessionListPage(sessions: SessionSnapshot[], offset: number, limit: number): SessionListPage;
58
+ export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], customNames?: ReadonlyMap<string, string>): SessionDirectoryTreeResponse;
66
59
  /**
67
60
  * Provider history is scanned by ProcessManager, but structured sessions live
68
61
  * in StructuredSessionManager. Annotate against the combined session list so a
@@ -71,5 +64,5 @@ export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], c
71
64
  */
72
65
  export declare function markManagedProviderHistory<T extends ProviderHistorySession>(history: T[], sessions: SessionSnapshot[], provider: "claude" | "codex" | "opencode" | "qoder"): T[];
73
66
  export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void): void;
74
- export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, _structured: StructuredSessionManager, storage: WandStorage, sessionRegistry: SessionRegistry): void;
67
+ export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, _structured: StructuredSessionManager, storage: WandStorage, _sessionRegistry: SessionRegistry): void;
75
68
  export {};
@@ -13,6 +13,7 @@ import { parseBoundedInteger } from "./request-limits.js";
13
13
  import { asyncRoute } from "./express-async.js";
14
14
  import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
15
15
  import { buildDirectoryTree, normalizeSessionDirectory, } from "./session-directory-tree.js";
16
+ import { projectCwdForSession, resolveWorkspaceIdForNewSession, } from "./workspace-binding.js";
16
17
  export function parseExecutionMode(value, fallback) {
17
18
  if (value === undefined)
18
19
  return fallback;
@@ -197,33 +198,13 @@ function sessionSortTimestamp(snapshot) {
197
198
  const timestamp = Date.parse(snapshot.startedAt);
198
199
  return Number.isFinite(timestamp) ? timestamp : 0;
199
200
  }
200
- export function buildSessionListEntries(sessions, claudeHistory, codexHistory, hiddenHistoryIds, openCodeHistory = [], qoderHistory = []) {
201
- const managed = sessions.map((session) => ({
201
+ export function buildSessionListEntries(sessions) {
202
+ return sessions.map((session) => ({
202
203
  type: "managed",
203
204
  key: `session-${session.id}`,
204
205
  sortTimestamp: sessionSortTimestamp(session),
205
206
  session: toSessionListItemDTO(session),
206
- }));
207
- const recoverable = [
208
- ...markManagedProviderHistory(claudeHistory, sessions, "claude"),
209
- ...markManagedProviderHistory(codexHistory, sessions, "codex"),
210
- ...markManagedProviderHistory(openCodeHistory, sessions, "opencode"),
211
- ...markManagedProviderHistory(qoderHistory, sessions, "qoder"),
212
- ].flatMap((history) => {
213
- const provider = history.provider === "codex" || history.provider === "opencode" || history.provider === "qoder"
214
- ? history.provider
215
- : "claude";
216
- if (!history.hasConversation || history.managedByWand || hiddenHistoryIds.has(history.claudeSessionId)) {
217
- return [];
218
- }
219
- return [{
220
- type: "recoverable",
221
- key: `recoverable-${provider}-${history.claudeSessionId}`,
222
- sortTimestamp: history.mtimeMs,
223
- history: { ...history, provider },
224
- }];
225
- });
226
- return [...managed, ...recoverable].sort((left, right) => {
207
+ })).sort((left, right) => {
227
208
  const timestampOrder = right.sortTimestamp - left.sortTimestamp;
228
209
  return timestampOrder || left.key.localeCompare(right.key);
229
210
  });
@@ -240,8 +221,8 @@ function sessionListRevision(entries, directoryNames = []) {
240
221
  .update(JSON.stringify(revisionState))
241
222
  .digest("base64url");
242
223
  }
243
- export function buildSessionListPage(sessions, claudeHistory, codexHistory, hiddenHistoryIds, offset, limit, openCodeHistory = [], qoderHistory = []) {
244
- const entries = buildSessionListEntries(sessions, claudeHistory, codexHistory, hiddenHistoryIds, openCodeHistory, qoderHistory);
224
+ export function buildSessionListPage(sessions, offset, limit) {
225
+ const entries = buildSessionListEntries(sessions);
245
226
  const boundedOffset = Math.min(Math.max(offset, 0), entries.length);
246
227
  const revision = sessionListRevision(entries);
247
228
  return {
@@ -251,11 +232,11 @@ export function buildSessionListPage(sessions, claudeHistory, codexHistory, hidd
251
232
  revision,
252
233
  };
253
234
  }
254
- export function buildSessionDirectoryTree(sessions, claudeHistory, codexHistory, hiddenHistoryIds, openCodeHistory = [], qoderHistory = [], customNames = new Map()) {
255
- const entries = buildSessionListEntries(sessions, claudeHistory, codexHistory, hiddenHistoryIds, openCodeHistory, qoderHistory);
235
+ export function buildSessionDirectoryTree(sessions, customNames = new Map()) {
236
+ const entries = buildSessionListEntries(sessions);
256
237
  const tree = buildDirectoryTree(entries.map((entry) => ({
257
238
  entry,
258
- cwd: entry.type === "managed" ? entry.session.cwd : entry.history.cwd,
239
+ cwd: entry.session.cwd,
259
240
  sortTimestamp: entry.sortTimestamp,
260
241
  })), "未知目录", customNames);
261
242
  const appliedCustomNames = [];
@@ -367,7 +348,7 @@ function isPtyProviderCommand(provider, command) {
367
348
  const executable = provider === "qoder" ? "qodercli" : provider;
368
349
  return new RegExp(`^${executable}\\b`, "i").test(command.trim());
369
350
  }
370
- async function startResumedPtySession(processes, existingSession, sessionId, defaultMode, body, initialInput) {
351
+ async function startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, body, initialInput) {
371
352
  if ((existingSession.sessionKind ?? "pty") !== "pty") {
372
353
  throw new Error("结构化会话不支持 PTY resume。");
373
354
  }
@@ -396,6 +377,9 @@ async function startResumedPtySession(processes, existingSession, sessionId, def
396
377
  provider,
397
378
  model: existingSession.selectedModel ?? undefined,
398
379
  thinkingEffort: existingSession.thinkingEffort ?? undefined,
380
+ workspaceId: existingSession.workspaceId
381
+ ?? resolveWorkspaceIdForNewSession(storage, projectCwdForSession(existingSession) || existingSession.cwd),
382
+ workspaceTaskId: existingSession.workspaceTaskId,
399
383
  });
400
384
  }
401
385
  function getAutoResumeInitialInput(snapshot, input, view, shortcutKey) {
@@ -437,14 +421,14 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
437
421
  messageTotal: windowed.messageTotal,
438
422
  });
439
423
  };
440
- const currentDirectoryTree = () => buildSessionDirectoryTree(sessions.listSlim(), processes.listClaudeHistorySessions(), processes.listCodexHistorySessions(), getHiddenClaudeSessionIds(storage), processes.listOpenCodeHistorySessions(), processes.listQoderHistorySessions(), storage.listSessionDirectoryNames());
424
+ const currentDirectoryTree = () => buildSessionDirectoryTree(sessions.listSlim(), storage.listSessionDirectoryNames());
441
425
  app.get("/api/session-list", (req, res) => {
442
426
  try {
443
427
  const offset = parseBoundedInteger(req.query.offset, 0, 0, Number.MAX_SAFE_INTEGER);
444
428
  const limit = parseBoundedInteger(req.query.limit, 40, 1, 200);
445
429
  const requestedRevision = typeof req.query.revision === "string" ? req.query.revision : "";
446
430
  const currentSessions = sessions.listSlim();
447
- const page = buildSessionListPage(currentSessions, processes.listClaudeHistorySessions(), processes.listCodexHistorySessions(), getHiddenClaudeSessionIds(storage), offset, limit, processes.listOpenCodeHistorySessions(), processes.listQoderHistorySessions());
431
+ const page = buildSessionListPage(currentSessions, offset, limit);
448
432
  if (offset > 0 && requestedRevision !== page.revision) {
449
433
  res.status(409).json({ error: "会话列表已更新,请重新加载。", revision: page.revision });
450
434
  return;
@@ -513,8 +497,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
513
497
  const provider = body.provider === "codex" || body.provider === "opencode" || body.provider === "grok" || body.provider === "qoder" || body.provider === "pi" ? body.provider : "claude";
514
498
  const rawModel = typeof body.model === "string" ? body.model.trim() : "";
515
499
  const origin = parseSessionCreationOrigin(body);
500
+ const cwd = resolveSessionCwd(body.cwd, config.defaultCwd);
516
501
  const snapshot = structured.createSession({
517
- cwd: resolveSessionCwd(body.cwd, config.defaultCwd),
502
+ cwd,
518
503
  mode: parseExecutionMode(body.mode, defaultMode),
519
504
  provider,
520
505
  // Omit runner to let StructuredSessionManager apply the configured
@@ -525,7 +510,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
525
510
  thinkingEffort: typeof body.thinkingEffort === "string"
526
511
  ? body.thinkingEffort
527
512
  : config.defaultThinkingEffort,
528
- workspaceId: body.workspaceId,
513
+ workspaceId: resolveWorkspaceIdForNewSession(storage, cwd, body.workspaceId),
529
514
  workspaceTaskId: body.workspaceTaskId,
530
515
  ...origin,
531
516
  });
@@ -1081,7 +1066,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1081
1066
  res.status(400).json({ error: "结构化会话不支持 PTY resume。" });
1082
1067
  return;
1083
1068
  }
1084
- const newSnapshot = await startResumedPtySession(processes, existingSession, sessionId, defaultMode, body);
1069
+ const newSnapshot = await startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, body);
1085
1070
  res.status(201).json(newSnapshot);
1086
1071
  }
1087
1072
  catch (error) {
@@ -1128,6 +1113,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1128
1113
  reuseId: existingSession.id,
1129
1114
  cols: reqCols,
1130
1115
  rows: reqRows,
1116
+ workspaceId: existingSession.workspaceId
1117
+ ?? resolveWorkspaceIdForNewSession(storage, projectCwdForSession(existingSession) || existingSession.cwd),
1118
+ workspaceTaskId: existingSession.workspaceTaskId,
1131
1119
  ...(requestedOrigin ?? {}),
1132
1120
  });
1133
1121
  res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...sessionResponseDTO(newSnapshot) });
@@ -1145,6 +1133,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1145
1133
  const newSnapshot = await processes.start(resumeCommand, cwd, newMode, undefined, {
1146
1134
  cols: reqCols,
1147
1135
  rows: reqRows,
1136
+ workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
1148
1137
  ...(requestedOrigin ?? {}),
1149
1138
  });
1150
1139
  res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...sessionResponseDTO(newSnapshot) });
@@ -1189,11 +1178,19 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1189
1178
  const existingSession = processes.get(sessionId) || storage.getSession(sessionId);
1190
1179
  const autoResumeInput = getAutoResumeInitialInput(existingSession, input, view, shortcutKey);
1191
1180
  if (autoResumeInput !== null && canAutoResumePtyForInput(existingSession, autoResumeInput)) {
1192
- const snapshot = await startResumedPtySession(processes, existingSession, sessionId, defaultMode, {}, autoResumeInput);
1181
+ const snapshot = await startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, {}, autoResumeInput);
1182
+ if (body.responseMode === "accepted") {
1183
+ res.status(202).json({ accepted: true });
1184
+ return;
1185
+ }
1193
1186
  res.json(sessionResponseDTO(snapshot));
1194
1187
  return;
1195
1188
  }
1196
1189
  const snapshot = processes.sendInput(sessionId, input, view, shortcutKey);
1190
+ if (body.responseMode === "accepted") {
1191
+ res.status(202).json({ accepted: true });
1192
+ return;
1193
+ }
1197
1194
  res.json(sessionResponseDTO(snapshot));
1198
1195
  }
1199
1196
  catch (error) {
@@ -1233,6 +1230,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1233
1230
  runner: "codex-cli-exec",
1234
1231
  worktreeEnabled: body.worktreeEnabled === true,
1235
1232
  claudeSessionId: threadId,
1233
+ workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
1236
1234
  ...origin,
1237
1235
  });
1238
1236
  onSessionCreated?.(cwd);
@@ -1273,6 +1271,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1273
1271
  runner: "opencode-cli-run",
1274
1272
  worktreeEnabled: body.worktreeEnabled === true,
1275
1273
  claudeSessionId: sessionId,
1274
+ workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
1276
1275
  ...parseSessionCreationOrigin(body),
1277
1276
  });
1278
1277
  onSessionCreated?.(cwd);
@@ -1307,6 +1306,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1307
1306
  runner: "qoder-cli-print",
1308
1307
  worktreeEnabled: body.worktreeEnabled === true,
1309
1308
  claudeSessionId: sessionId,
1309
+ workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
1310
1310
  ...parseSessionCreationOrigin(body),
1311
1311
  });
1312
1312
  onSessionCreated?.(cwd);
@@ -1422,19 +1422,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1422
1422
  }
1423
1423
  });
1424
1424
  }
1425
- export function registerClaudeHistoryRoutes(app, processes, _structured, storage, sessionRegistry) {
1425
+ export function registerClaudeHistoryRoutes(app, processes, _structured, storage, _sessionRegistry) {
1426
+ // Kept as an empty compatibility endpoint for older native clients. Wand no
1427
+ // longer imports provider-native history into its session list.
1426
1428
  app.get("/api/claude-history", (_req, res) => {
1427
- try {
1428
- const history = markManagedProviderHistory(processes.listClaudeHistorySessions(), sessionRegistry.listSlim(), "claude");
1429
- const hidden = getHiddenClaudeSessionIds(storage);
1430
- const filtered = hidden.size > 0
1431
- ? history.filter((s) => !s.claudeSessionId || !hidden.has(s.claudeSessionId))
1432
- : history;
1433
- res.json(filtered);
1434
- }
1435
- catch (error) {
1436
- res.status(500).json({ error: getErrorMessage(error, "无法扫描 Claude 历史会话。") });
1437
- }
1429
+ res.json([]);
1438
1430
  });
1439
1431
  app.delete("/api/claude-history/:claudeSessionId", (req, res) => {
1440
1432
  const claudeSessionId = req.params.claudeSessionId?.trim();
@@ -1529,17 +1521,7 @@ export function registerClaudeHistoryRoutes(app, processes, _structured, storage
1529
1521
  // 字段),用户发第一条消息时 buildCodexArgs 自动拼 `codex exec ... resume <thread_id>`。
1530
1522
  // hidden 集合与 claude 共用(id 全局唯一,不会冲突)。
1531
1523
  app.get("/api/codex-history", (_req, res) => {
1532
- try {
1533
- const history = markManagedProviderHistory(processes.listCodexHistorySessions(), sessionRegistry.listSlim(), "codex");
1534
- const hidden = getHiddenClaudeSessionIds(storage);
1535
- const filtered = hidden.size > 0
1536
- ? history.filter((s) => !hidden.has(s.claudeSessionId))
1537
- : history;
1538
- res.json(filtered);
1539
- }
1540
- catch (error) {
1541
- res.status(500).json({ error: getErrorMessage(error, "无法扫描 Codex 历史会话。") });
1542
- }
1524
+ res.json([]);
1543
1525
  });
1544
1526
  app.delete("/api/codex-history/:threadId", (req, res) => {
1545
1527
  const threadId = req.params.threadId?.trim();
@@ -1615,14 +1597,7 @@ export function registerClaudeHistoryRoutes(app, processes, _structured, storage
1615
1597
  ];
1616
1598
  for (const config of externalHistoryProviders) {
1617
1599
  app.get(`/api/${config.provider}-history`, (_req, res) => {
1618
- try {
1619
- const history = markManagedProviderHistory(config.list(), sessionRegistry.listSlim(), config.provider);
1620
- const hidden = getHiddenClaudeSessionIds(storage);
1621
- res.json(hidden.size > 0 ? history.filter((session) => !hidden.has(session.claudeSessionId)) : history);
1622
- }
1623
- catch (error) {
1624
- res.status(500).json({ error: getErrorMessage(error, `无法扫描 ${config.label} 历史会话。`) });
1625
- }
1600
+ res.json([]);
1626
1601
  });
1627
1602
  app.delete(`/api/${config.provider}-history/:sessionId`, (req, res) => {
1628
1603
  const sessionId = req.params.sessionId?.trim();