@co0ontty/wand 4.42.0 → 4.42.1-beta.g6a621ff

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": "0b6a7b8718c885c69a15d7ee14bd7383b4271459",
3
- "builtAt": "2026-08-09T12:57:20.566Z",
4
- "version": "4.42.0",
5
- "channel": "stable"
2
+ "commit": "6a621ff17512424111dd00205861d1eeea439387",
3
+ "builtAt": "2026-08-14T01:28:27.019Z",
4
+ "version": "4.42.1-beta.g6a621ff",
5
+ "channel": "beta"
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");
@@ -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,
@@ -968,12 +968,16 @@ export class ProcessManager extends EventEmitter {
968
968
  let priorMessages = [];
969
969
  let inheritedSessionSource;
970
970
  let inheritedAutomationId;
971
+ let inheritedWorkspaceId;
972
+ let inheritedWorkspaceTaskId;
971
973
  if (opts?.reuseId) {
972
974
  const oldRecord = this.sessions.get(id);
973
975
  if (oldRecord) {
974
976
  priorMessages = oldRecord.ptyBridge?.getMessages() ?? oldRecord.messages ?? [];
975
977
  inheritedSessionSource = oldRecord.sessionSource;
976
978
  inheritedAutomationId = oldRecord.automationId;
979
+ inheritedWorkspaceId = oldRecord.workspaceId;
980
+ inheritedWorkspaceTaskId = oldRecord.workspaceTaskId;
977
981
  this.cleanupRecord(oldRecord);
978
982
  this.sessions.delete(id);
979
983
  }
@@ -982,6 +986,8 @@ export class ProcessManager extends EventEmitter {
982
986
  priorMessages = stored?.messages ?? [];
983
987
  inheritedSessionSource = stored?.sessionSource;
984
988
  inheritedAutomationId = stored?.automationId;
989
+ inheritedWorkspaceId = stored?.workspaceId;
990
+ inheritedWorkspaceTaskId = stored?.workspaceTaskId;
985
991
  }
986
992
  this.terminalHost.forget(id);
987
993
  }
@@ -1032,8 +1038,8 @@ export class ProcessManager extends EventEmitter {
1032
1038
  id,
1033
1039
  sessionSource: opts?.sessionSource ?? inheritedSessionSource ?? "interactive",
1034
1040
  automationId: opts?.automationId ?? inheritedAutomationId,
1035
- workspaceId: opts?.workspaceId,
1036
- workspaceTaskId: opts?.workspaceTaskId,
1041
+ workspaceId: opts?.workspaceId ?? inheritedWorkspaceId,
1042
+ workspaceTaskId: opts?.workspaceTaskId ?? inheritedWorkspaceTaskId,
1037
1043
  provider,
1038
1044
  command,
1039
1045
  cwd: resolvedCwd,
@@ -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) => {
@@ -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;
@@ -367,7 +368,7 @@ function isPtyProviderCommand(provider, command) {
367
368
  const executable = provider === "qoder" ? "qodercli" : provider;
368
369
  return new RegExp(`^${executable}\\b`, "i").test(command.trim());
369
370
  }
370
- async function startResumedPtySession(processes, existingSession, sessionId, defaultMode, body, initialInput) {
371
+ async function startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, body, initialInput) {
371
372
  if ((existingSession.sessionKind ?? "pty") !== "pty") {
372
373
  throw new Error("结构化会话不支持 PTY resume。");
373
374
  }
@@ -396,6 +397,9 @@ async function startResumedPtySession(processes, existingSession, sessionId, def
396
397
  provider,
397
398
  model: existingSession.selectedModel ?? undefined,
398
399
  thinkingEffort: existingSession.thinkingEffort ?? undefined,
400
+ workspaceId: existingSession.workspaceId
401
+ ?? resolveWorkspaceIdForNewSession(storage, projectCwdForSession(existingSession) || existingSession.cwd),
402
+ workspaceTaskId: existingSession.workspaceTaskId,
399
403
  });
400
404
  }
401
405
  function getAutoResumeInitialInput(snapshot, input, view, shortcutKey) {
@@ -513,8 +517,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
513
517
  const provider = body.provider === "codex" || body.provider === "opencode" || body.provider === "grok" || body.provider === "qoder" || body.provider === "pi" ? body.provider : "claude";
514
518
  const rawModel = typeof body.model === "string" ? body.model.trim() : "";
515
519
  const origin = parseSessionCreationOrigin(body);
520
+ const cwd = resolveSessionCwd(body.cwd, config.defaultCwd);
516
521
  const snapshot = structured.createSession({
517
- cwd: resolveSessionCwd(body.cwd, config.defaultCwd),
522
+ cwd,
518
523
  mode: parseExecutionMode(body.mode, defaultMode),
519
524
  provider,
520
525
  // Omit runner to let StructuredSessionManager apply the configured
@@ -525,7 +530,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
525
530
  thinkingEffort: typeof body.thinkingEffort === "string"
526
531
  ? body.thinkingEffort
527
532
  : config.defaultThinkingEffort,
528
- workspaceId: body.workspaceId,
533
+ workspaceId: resolveWorkspaceIdForNewSession(storage, cwd, body.workspaceId),
529
534
  workspaceTaskId: body.workspaceTaskId,
530
535
  ...origin,
531
536
  });
@@ -1081,7 +1086,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1081
1086
  res.status(400).json({ error: "结构化会话不支持 PTY resume。" });
1082
1087
  return;
1083
1088
  }
1084
- const newSnapshot = await startResumedPtySession(processes, existingSession, sessionId, defaultMode, body);
1089
+ const newSnapshot = await startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, body);
1085
1090
  res.status(201).json(newSnapshot);
1086
1091
  }
1087
1092
  catch (error) {
@@ -1128,6 +1133,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1128
1133
  reuseId: existingSession.id,
1129
1134
  cols: reqCols,
1130
1135
  rows: reqRows,
1136
+ workspaceId: existingSession.workspaceId
1137
+ ?? resolveWorkspaceIdForNewSession(storage, projectCwdForSession(existingSession) || existingSession.cwd),
1138
+ workspaceTaskId: existingSession.workspaceTaskId,
1131
1139
  ...(requestedOrigin ?? {}),
1132
1140
  });
1133
1141
  res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...sessionResponseDTO(newSnapshot) });
@@ -1145,6 +1153,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1145
1153
  const newSnapshot = await processes.start(resumeCommand, cwd, newMode, undefined, {
1146
1154
  cols: reqCols,
1147
1155
  rows: reqRows,
1156
+ workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
1148
1157
  ...(requestedOrigin ?? {}),
1149
1158
  });
1150
1159
  res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...sessionResponseDTO(newSnapshot) });
@@ -1189,7 +1198,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1189
1198
  const existingSession = processes.get(sessionId) || storage.getSession(sessionId);
1190
1199
  const autoResumeInput = getAutoResumeInitialInput(existingSession, input, view, shortcutKey);
1191
1200
  if (autoResumeInput !== null && canAutoResumePtyForInput(existingSession, autoResumeInput)) {
1192
- const snapshot = await startResumedPtySession(processes, existingSession, sessionId, defaultMode, {}, autoResumeInput);
1201
+ const snapshot = await startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, {}, autoResumeInput);
1193
1202
  res.json(sessionResponseDTO(snapshot));
1194
1203
  return;
1195
1204
  }
@@ -1233,6 +1242,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1233
1242
  runner: "codex-cli-exec",
1234
1243
  worktreeEnabled: body.worktreeEnabled === true,
1235
1244
  claudeSessionId: threadId,
1245
+ workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
1236
1246
  ...origin,
1237
1247
  });
1238
1248
  onSessionCreated?.(cwd);
@@ -1273,6 +1283,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1273
1283
  runner: "opencode-cli-run",
1274
1284
  worktreeEnabled: body.worktreeEnabled === true,
1275
1285
  claudeSessionId: sessionId,
1286
+ workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
1276
1287
  ...parseSessionCreationOrigin(body),
1277
1288
  });
1278
1289
  onSessionCreated?.(cwd);
@@ -1307,6 +1318,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1307
1318
  runner: "qoder-cli-print",
1308
1319
  worktreeEnabled: body.worktreeEnabled === true,
1309
1320
  claudeSessionId: sessionId,
1321
+ workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
1310
1322
  ...parseSessionCreationOrigin(body),
1311
1323
  });
1312
1324
  onSessionCreated?.(cwd);
@@ -5,6 +5,8 @@ import { asyncRoute } from "./express-async.js";
5
5
  import { getErrorMessage } from "./error-utils.js";
6
6
  import { expandHomePath } from "./middleware/path-safety.js";
7
7
  import { checkSessionWorktreeMergeabilityAsync, cleanupWorktreeSync, prepareSessionWorktree, resolveWorktreeTargetBranchAsync, } from "./git-worktree.js";
8
+ import { resolveSessionDisplayTitle } from "./session-transport.js";
9
+ import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, } from "./workspace-binding.js";
8
10
  const PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
9
11
  function parseDefaultProvider(value) {
10
12
  return typeof value === "string" && PROVIDERS.has(value) ? value : undefined;
@@ -29,11 +31,13 @@ function deleteSessions(storage, sessions, sessionIds) {
29
31
  storage.deleteSession(sessionId);
30
32
  }
31
33
  }
32
- function workspaceWithWorktreeCount(storage, workspace) {
34
+ function workspaceWithCounts(storage, workspace, sessionCounts) {
33
35
  const worktreeCount = storage.listWorkspaceTasks(workspace.id)
34
36
  .filter((task) => task.worktree !== null)
35
37
  .length;
36
- return { ...workspace, worktreeCount };
38
+ const sessionCount = sessionCounts?.get(workspace.id)
39
+ ?? storage.listSessionsByWorkspace(workspace.id).length;
40
+ return { ...workspace, worktreeCount, sessionCount };
37
41
  }
38
42
  // ── Layout validation / sanitization(前端 PUT 与测试共用)──
39
43
  function sanitizePaneTab(value) {
@@ -148,7 +152,9 @@ export function sanitizeTaskLayout(value) {
148
152
  export function registerWorkspaceRoutes(app, storage, sessions) {
149
153
  // 列出所有项目(按最近打开排序)
150
154
  app.get("/api/workspaces", (_req, res) => {
151
- res.json(storage.listWorkspaces().map((workspace) => workspaceWithWorktreeCount(storage, workspace)));
155
+ backfillSessionWorkspaces(storage);
156
+ const sessionCounts = storage.countSessionsByWorkspace();
157
+ res.json(storage.listWorkspaces().map((workspace) => workspaceWithCounts(storage, workspace, sessionCounts)));
152
158
  });
153
159
  // 新建项目:名称 + 目录 + 默认 IDE,不启动会话
154
160
  app.post("/api/workspaces", asyncRoute(async (req, res) => {
@@ -168,7 +174,8 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
168
174
  }
169
175
  const defaultProvider = parseDefaultProvider(body.defaultProvider);
170
176
  const workspace = storage.createWorkspace({ name, cwd, defaultProvider });
171
- res.status(201).json({ ...workspace, worktreeCount: 0 });
177
+ const attached = attachUnboundSessionsToWorkspace(storage, workspace);
178
+ res.status(201).json({ ...workspace, worktreeCount: 0, sessionCount: attached });
172
179
  }));
173
180
  // 项目详情:meta + 会话 + 布局;访问即更新 lastOpenedAt
174
181
  app.get("/api/workspaces/:id", (req, res) => {
@@ -179,8 +186,11 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
179
186
  }
180
187
  storage.touchWorkspace(workspace.id);
181
188
  res.json({
182
- ...workspaceWithWorktreeCount(storage, workspace),
183
- sessions: storage.listSessionsByWorkspace(workspace.id),
189
+ ...workspaceWithCounts(storage, workspace),
190
+ sessions: storage.listSessionsByWorkspace(workspace.id).map((session) => ({
191
+ ...session,
192
+ title: resolveSessionDisplayTitle(session),
193
+ })),
184
194
  });
185
195
  });
186
196
  // 改名 / 目录 / 默认 IDE
@@ -213,7 +223,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
213
223
  }
214
224
  storage.updateWorkspace(existing.id, patch);
215
225
  const updated = storage.getWorkspace(existing.id);
216
- res.json(updated ? workspaceWithWorktreeCount(storage, updated) : null);
226
+ res.json(updated ? workspaceWithCounts(storage, updated) : null);
217
227
  });
218
228
  // 删除项目;cascade=true 连带删会话,否则仅解绑
219
229
  app.delete("/api/workspaces/:id", (req, res) => {
package/dist/server.js CHANGED
@@ -28,6 +28,7 @@ import { refreshProviderCliUpdateState, registerAdminUpdateRoutes, registerPubli
28
28
  import { parseSessionCreationOrigin, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
29
29
  import { registerWorkspaceRoutes } from "./server-workspace-routes.js";
30
30
  import { resolveSessionCwd } from "./session-cwd.js";
31
+ import { resolveWorkspaceIdForNewSession } from "./workspace-binding.js";
31
32
  import { getErrorMessage } from "./error-utils.js";
32
33
  import { asyncRoute, jsonErrorHandler } from "./express-async.js";
33
34
  import { checkPackageUpdateAsync, installPackageGloballyAsync, normalizeUpdateChannel, resolveGlobalWandCli, } from "./npm-update-utils.js";
@@ -1020,23 +1021,25 @@ export async function startServer(config, configPath, options = {}) {
1020
1021
  : undefined;
1021
1022
  const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
1022
1023
  const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
1024
+ const sessionCwd = resolveSessionCwd(body.cwd, config.defaultCwd);
1025
+ const workspaceId = resolveWorkspaceIdForNewSession(storage, sessionCwd, body.workspaceId);
1023
1026
  const snapshot = await (interactiveShell
1024
- ? processes.startShell(body.cwd, body.mode ?? "default", {
1027
+ ? processes.startShell(sessionCwd, body.mode ?? "default", {
1025
1028
  worktreeEnabled: body.worktreeEnabled === true,
1026
1029
  cols: reqCols,
1027
1030
  rows: reqRows,
1028
- workspaceId: body.workspaceId,
1031
+ workspaceId,
1029
1032
  workspaceTaskId: body.workspaceTaskId,
1030
1033
  ...origin,
1031
1034
  })
1032
- : processes.start(command, body.cwd, body.mode ?? config.defaultMode, initialInput || undefined, {
1035
+ : processes.start(command, sessionCwd, body.mode ?? config.defaultMode, initialInput || undefined, {
1033
1036
  worktreeEnabled: body.worktreeEnabled === true,
1034
1037
  provider,
1035
1038
  model: effectiveModel,
1036
1039
  cols: reqCols,
1037
1040
  rows: reqRows,
1038
1041
  thinkingEffort: body.thinkingEffort ?? config.defaultThinkingEffort,
1039
- workspaceId: body.workspaceId,
1042
+ workspaceId,
1040
1043
  workspaceTaskId: body.workspaceTaskId,
1041
1044
  ...origin,
1042
1045
  }));
package/dist/storage.d.ts CHANGED
@@ -60,6 +60,14 @@ export declare class WandStorage {
60
60
  listSessionsByWorkspace(workspaceId: string): SessionSnapshot[];
61
61
  /** 显式更新某会话的工作空间归属(用于创建时绑定)。 */
62
62
  setSessionWorkspaceId(sessionId: string, workspaceId: string | null): void;
63
+ /** Lightweight count of persisted sessions grouped by workspace. */
64
+ countSessionsByWorkspace(): Map<string, number>;
65
+ /** Sessions not yet attached to a project. Omits messages/output. */
66
+ listUnboundSessionBindings(): Array<{
67
+ id: string;
68
+ cwd: string;
69
+ worktree: SessionSnapshot["worktree"];
70
+ }>;
63
71
  listWorkspaceTasks(workspaceId: string): WorkspaceTask[];
64
72
  getWorkspaceTask(id: string): WorkspaceTask | null;
65
73
  createWorkspaceTask(input: {
package/dist/storage.js CHANGED
@@ -861,6 +861,29 @@ export class WandStorage {
861
861
  setSessionWorkspaceId(sessionId, workspaceId) {
862
862
  this.db.prepare("UPDATE command_sessions SET workspace_id = ? WHERE id = ?").run(workspaceId, sessionId);
863
863
  }
864
+ /** Lightweight count of persisted sessions grouped by workspace. */
865
+ countSessionsByWorkspace() {
866
+ const rows = this.db
867
+ .prepare(`SELECT workspace_id AS id, COUNT(*) AS n
868
+ FROM command_sessions
869
+ WHERE workspace_id IS NOT NULL AND workspace_id != ''
870
+ GROUP BY workspace_id`)
871
+ .all();
872
+ return new Map(rows.map((row) => [row.id, Number(row.n) || 0]));
873
+ }
874
+ /** Sessions not yet attached to a project. Omits messages/output. */
875
+ listUnboundSessionBindings() {
876
+ const rows = this.db
877
+ .prepare(`SELECT id, cwd, worktree_info
878
+ FROM command_sessions
879
+ WHERE workspace_id IS NULL OR workspace_id = ''`)
880
+ .all();
881
+ return rows.map((row) => ({
882
+ id: row.id,
883
+ cwd: row.cwd,
884
+ worktree: parseWorktreeInfo(row.worktree_info) ?? null,
885
+ }));
886
+ }
864
887
  // ── Workspace tasks(任务 = 命名 + 独立 worktree + 一组标签)──
865
888
  listWorkspaceTasks(workspaceId) {
866
889
  const rows = this.db
@@ -1111,7 +1134,7 @@ export class WandStorage {
1111
1134
  deleteExpiredAuthSessions(now) {
1112
1135
  this.db.prepare("DELETE FROM auth_sessions WHERE expires_at < ?").run(now);
1113
1136
  }
1114
- // ============ Missions / Agent Inbox ============
1137
+ // ============ Missions ============
1115
1138
  saveMission(mission) {
1116
1139
  this.db.prepare(`INSERT INTO missions (
1117
1140
  id, title, prompt, cwd, status, base_ref, shared_directories, copy_paths, created_at, updated_at