@co0ontty/wand 4.97.0 → 4.98.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.
- package/README.md +0 -4
- package/dist/agent-dispatch.d.ts +1 -0
- package/dist/agent-dispatch.js +13 -2
- package/dist/ai-team-types.d.ts +1 -1
- package/dist/ai-team-types.js +1 -1
- package/dist/build-info.json +3 -3
- package/dist/cli.js +2 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -2
- package/dist/core-runner.d.ts +0 -4
- package/dist/core-runner.js +41 -3
- package/dist/decision-expert-employee.js +1 -0
- package/dist/distribution-manager.d.ts +29 -4
- package/dist/distribution-manager.js +147 -57
- package/dist/employee-types.d.ts +2 -1
- package/dist/employee-types.js +2 -4
- package/dist/feedback-feature-map.d.ts +0 -1
- package/dist/feedback-feature-map.js +0 -1
- package/dist/feedback.d.ts +0 -1
- package/dist/feedback.js +1 -1
- package/dist/git-quick-commit.js +13 -52
- package/dist/git-tag-generation.d.ts +11 -0
- package/dist/git-tag-generation.js +47 -0
- package/dist/hr-employee.js +1 -0
- package/dist/local-model-setup.d.ts +1 -9
- package/dist/local-model-setup.js +52 -166
- package/dist/local-model-types.d.ts +2 -5
- package/dist/managed-temp.d.ts +1 -1
- package/dist/managed-temp.js +1 -1
- package/dist/middleware/api-scope.js +2 -4
- package/dist/migration-backup-retention.d.ts +17 -0
- package/dist/migration-backup-retention.js +91 -0
- package/dist/pi-tmux-host.d.ts +28 -0
- package/dist/pi-tmux-host.js +189 -0
- package/dist/server-employee-routes.d.ts +1 -0
- package/dist/server-employee-routes.js +17 -4
- package/dist/server-local-model-routes.js +2 -15
- package/dist/server-session-input-routes.js +3 -3
- package/dist/server-session-lifecycle-routes.js +94 -18
- package/dist/server-task-routes.js +18 -13
- package/dist/server-update-routes.d.ts +11 -1
- package/dist/server-update-routes.js +25 -12
- package/dist/server-workspace-routes.js +11 -5
- package/dist/server.js +29 -129
- package/dist/storage/conversations-repository.d.ts +2 -0
- package/dist/storage/conversations-repository.js +4 -0
- package/dist/storage/employees-repository.d.ts +2 -2
- package/dist/storage/employees-repository.js +12 -15
- package/dist/storage/internal.js +2 -2
- package/dist/storage/migrations.js +20 -0
- package/dist/storage/readonly.js +3 -0
- package/dist/storage/sessions-repository.d.ts +2 -0
- package/dist/storage/sessions-repository.js +13 -0
- package/dist/storage/tasks-repository.d.ts +22 -0
- package/dist/storage/tasks-repository.js +52 -3
- package/dist/storage/workspaces-repository.d.ts +1 -0
- package/dist/storage/workspaces-repository.js +3 -1
- package/dist/storage-cleanup.d.ts +37 -2
- package/dist/storage-cleanup.js +62 -2
- package/dist/storage-schema.js +9 -0
- package/dist/structured-claude-adapter.js +5 -1
- package/dist/structured-claude-protocol.d.ts +1 -1
- package/dist/structured-claude-protocol.js +12 -1
- package/dist/structured-runner.d.ts +2 -0
- package/dist/structured-session-manager.d.ts +14 -2
- package/dist/structured-session-manager.js +87 -14
- package/dist/system-ai-call.d.ts +5 -1
- package/dist/system-ai-call.js +32 -1
- package/dist/system-ai-session-recorder.d.ts +2 -0
- package/dist/system-ai-session-recorder.js +35 -3
- package/dist/system-cli.js +6 -1
- package/dist/system-employee.js +1 -0
- package/dist/task-comment-dispatch.d.ts +18 -0
- package/dist/task-comment-dispatch.js +121 -0
- package/dist/task-types.d.ts +14 -0
- package/dist/version-utils.d.ts +31 -0
- package/dist/version-utils.js +35 -0
- package/dist/web-ui/content/ai-teams.js +10 -10
- package/dist/web-ui/content/module-manifest.json +1 -1
- package/dist/web-ui/content/modules/02b1ec89c69dc0dd/host.js +30 -0
- package/dist/web-ui/content/modules/{e8f0cda6b9ac8a70 → 0434a45abd2c1d15}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{e9a01a25ec573294 → 0afa04591224bc23}/library-bridge.js +1 -1
- package/dist/web-ui/content/modules/{b33c567946044d83 → 0bf4a78c8be1bdc9}/host.js +1 -1
- package/dist/web-ui/content/modules/17f72e0fb014a302/chat-render.js +75 -0
- package/dist/web-ui/content/modules/{5903104a2e0e3888 → 183db4802a7d99ed}/host.js +1 -1
- package/dist/web-ui/content/modules/23d2006fcfcf3c0c/host-bridge.js +1 -0
- package/dist/web-ui/content/modules/{0c96341a022aa360 → 32f151da1a006041}/host.js +1 -1
- package/dist/web-ui/content/modules/{3172fe0b46d51f8d → 34a7674efc329383}/chunk.js +1 -1
- package/dist/web-ui/content/modules/3541ba4cb2886e29/chunk.js +1 -0
- package/dist/web-ui/content/modules/{801e7f686ce7eb28 → 395257bd20cd0098}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{3ddcbd8505bfcdf4 → 3fd75af0a6891549}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{c41d8ffa27816766 → 428352e422932c40}/host.js +1 -1
- package/dist/web-ui/content/modules/485a72b2c83e7680/workspace-window.js +1 -0
- package/dist/web-ui/content/modules/489dfe1f7441ef2c/markdown.js +1 -0
- package/dist/web-ui/content/modules/4b42b4336458b7b5/runtime.js +550 -0
- package/dist/web-ui/content/modules/{de00ab5329b5e05a → 528c5d97e8f9d582}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{3086df78444998b3 → 535aad05d34079d2}/host.js +1 -1
- package/dist/web-ui/content/modules/62028b9bbf8ffc88/page.js +6 -0
- package/dist/web-ui/content/modules/6868f75a5981ad08/host.js +69 -0
- package/dist/web-ui/content/modules/6c72c8c166e34de7/host.js +1 -0
- package/dist/web-ui/content/modules/6ff6ce370ef26753/host.js +1 -0
- package/dist/web-ui/content/modules/{87379801875d1921 → 72bef6889fdafefe}/host.js +1 -1
- package/dist/web-ui/content/modules/{5e5eabd9a9366a56 → 763c79f5527d762a}/panel.js +1 -1
- package/dist/web-ui/content/modules/{d30ac9af6dc7d555 → 77ea205020273767}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{e01e1e25b7fe37e9 → 7fb02c44fd6f877b}/host.js +1 -1
- package/dist/web-ui/content/modules/{349ee129eef5a182 → 818d87c1651fc6a1}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{66f36cd2c38cfb07 → 8cb5d84a2731789e}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{f5b3fb6fedd9b05d → 96e78c7f2cd9acd5}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{64c1b0217f5377aa → a60a07ba1abcdc3f}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{ee12d3c4421a5c12 → b4bcb9bf5cfb0b3a}/host.js +1 -1
- package/dist/web-ui/content/modules/b71cda413318d71f/employee-card.js +1 -0
- package/dist/web-ui/content/modules/bb0c299cf006e157/task-board-host.js +17 -0
- package/dist/web-ui/content/modules/{42ae495da5903b07 → c8c6adee81d4bd98}/sha256.js +1 -1
- package/dist/web-ui/content/modules/{854b0f6dc887923b → cd0cec0ba1887ffd}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{3338b55c7767bacb → d673a76becc7d1f5}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{2bce1137f402bd70 → d99ea7e921488348}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{a9f15bd03bc590e7 → e862ccabc908eb0c}/host.js +1 -1
- package/dist/web-ui/content/modules/{9984f6bb05ac55bb → eb46bf62b1ae1bea}/host.js +1 -1
- package/dist/web-ui/content/plush-avatar.js +166 -166
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +177 -177
- package/dist/web-ui/embedded-assets.js +6 -6
- package/dist/web-ui/page-route.d.ts +24 -0
- package/dist/web-ui/page-route.js +130 -0
- package/dist/web-ui/pty-input-state.d.ts +2 -0
- package/dist/web-ui/pty-input-state.js +2 -2
- package/dist/web-ui/react/workspaces/types.d.ts +5 -0
- package/package.json +1 -3
- package/dist/server-speech-routes.d.ts +0 -10
- package/dist/server-speech-routes.js +0 -93
- package/dist/speech-models.d.ts +0 -24
- package/dist/speech-models.js +0 -16
- package/dist/speech-polisher-employee.d.ts +0 -6
- package/dist/speech-polisher-employee.js +0 -23
- package/dist/speech-polisher-identity.d.ts +0 -4
- package/dist/speech-polisher-identity.js +0 -4
- package/dist/speech-polisher-service.d.ts +0 -33
- package/dist/speech-polisher-service.js +0 -110
- package/dist/speech-service.d.ts +0 -59
- package/dist/speech-service.js +0 -420
- package/dist/speech-support.d.ts +0 -6
- package/dist/speech-support.js +0 -24
- package/dist/speech-types.d.ts +0 -52
- package/dist/speech-types.js +0 -3
- package/dist/web-ui/content/modules/15e32e85e086f6b4/page.js +0 -6
- package/dist/web-ui/content/modules/1e43259bddbc6805/markdown.js +0 -1
- package/dist/web-ui/content/modules/271d488d262909c2/host.js +0 -30
- package/dist/web-ui/content/modules/43746e7237d61da9/task-board-host.js +0 -17
- package/dist/web-ui/content/modules/4485e6c1f0459aa8/chunk.js +0 -1
- package/dist/web-ui/content/modules/5b541e0f296a3592/employee-card.js +0 -1
- package/dist/web-ui/content/modules/6ed52e6604661678/chat-render.js +0 -75
- package/dist/web-ui/content/modules/89888da0972e5033/host.js +0 -50
- package/dist/web-ui/content/modules/c11165f843d818f4/host-bridge.js +0 -1
- package/dist/web-ui/content/modules/c1829693f91fd25c/host.js +0 -1
- package/dist/web-ui/content/modules/cdb882ddab12e325/host.js +0 -1
- package/dist/web-ui/content/modules/ea67fd4e96c9f45a/workspace-window.js +0 -1
- package/dist/web-ui/content/modules/fd767282c29173b5/runtime.js +0 -539
- package/dist/web-ui/speech-audio.d.ts +0 -2
- package/dist/web-ui/speech-audio.js +0 -41
- package/scripts/install-speech-runtime.js +0 -82
|
@@ -19,6 +19,28 @@ export declare class TaskRepository {
|
|
|
19
19
|
listWandTasks(workspaceId?: string | null): WandTask[];
|
|
20
20
|
mapWandTaskRow(row: Record<string, unknown>): WandTask;
|
|
21
21
|
listWandTaskComments(taskId: string): WandTaskComment[];
|
|
22
|
+
getWandTaskComment(id: string): WandTaskComment | null;
|
|
23
|
+
findTaskCommentRequest(taskId: string, requestId: string): {
|
|
24
|
+
comment: WandTaskComment;
|
|
25
|
+
signature: string;
|
|
26
|
+
} | null;
|
|
27
|
+
/** Atomically persist both the visible comment and the one-shot dispatch intent before any side effect. */
|
|
28
|
+
createTaskCommentIntent(input: {
|
|
29
|
+
taskId: string;
|
|
30
|
+
requestId: string;
|
|
31
|
+
signature: string;
|
|
32
|
+
body: string;
|
|
33
|
+
instruction: string;
|
|
34
|
+
sourceCommentId: string | null;
|
|
35
|
+
employeeId: string | null;
|
|
36
|
+
employeeName: string | null;
|
|
37
|
+
taskVersion: string;
|
|
38
|
+
}): WandTaskComment;
|
|
39
|
+
updateTaskCommentDispatch(id: string, state: "pending" | "accepted" | "failed" | "unknown", sessionId: string | null, error?: string | null): void;
|
|
40
|
+
/** Read-only recovery of the create/bind crash gap, by a server-generated correlation ID, never message text. */
|
|
41
|
+
findTaskCommentExecutionSessionId(commentId: string, employeeId: string): string | null;
|
|
42
|
+
/** Event-owned facts, including stop outcomes which structured sessions intentionally represent as idle. */
|
|
43
|
+
recordTaskCommentSessionEvent(event: import("../types.js").ProcessEvent): void;
|
|
22
44
|
getWandTask(id: string): WandTask | null;
|
|
23
45
|
getWandTaskByWorkspaceTaskId(workspaceTaskId: string): WandTask | null;
|
|
24
46
|
insertWandTask(input: CreateWandTaskInput, createdAt?: string): WandTask;
|
|
@@ -99,9 +99,58 @@ export class TaskRepository {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
listWandTaskComments(taskId) {
|
|
102
|
-
const rows = this.db.prepare(`SELECT id
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
const rows = this.db.prepare(`SELECT id FROM wand_task_comments WHERE task_id = ? ORDER BY rowid DESC LIMIT 50`).all(taskId);
|
|
103
|
+
return rows.reverse().map(row => this.getWandTaskComment(row.id));
|
|
104
|
+
}
|
|
105
|
+
getWandTaskComment(id) {
|
|
106
|
+
const row = this.db.prepare(`SELECT c.*, d.request_id, d.source_comment_id, d.employee_id, d.employee_name,
|
|
107
|
+
d.instruction, d.task_version, d.state, d.session_id, d.error, d.execution_status FROM wand_task_comments c
|
|
108
|
+
LEFT JOIN wand_task_comment_dispatches d ON d.comment_id = c.id WHERE c.id = ?`).get(id);
|
|
109
|
+
if (!row)
|
|
110
|
+
return null;
|
|
111
|
+
return { id: row.id, taskId: row.task_id, authorName: row.author_name, body: row.body, createdAt: row.created_at,
|
|
112
|
+
...(row.request_id ? { requestId: row.request_id, sourceCommentId: row.source_comment_id, instruction: row.instruction, taskVersion: row.task_version,
|
|
113
|
+
dispatch: row.employee_id ? { employeeId: row.employee_id, employeeName: row.employee_name,
|
|
114
|
+
state: row.state, sessionId: row.session_id, error: row.error, ...(row.execution_status ? { status: row.execution_status } : {}) } : null } : {}) };
|
|
115
|
+
}
|
|
116
|
+
findTaskCommentRequest(taskId, requestId) {
|
|
117
|
+
const row = this.db.prepare(`SELECT comment_id, signature FROM wand_task_comment_dispatches WHERE task_id = ? AND request_id = ?`)
|
|
118
|
+
.get(taskId, requestId);
|
|
119
|
+
return row ? { comment: this.getWandTaskComment(row.comment_id), signature: row.signature } : null;
|
|
120
|
+
}
|
|
121
|
+
/** Atomically persist both the visible comment and the one-shot dispatch intent before any side effect. */
|
|
122
|
+
createTaskCommentIntent(input) {
|
|
123
|
+
return this.s.transaction(() => {
|
|
124
|
+
const id = crypto.randomUUID();
|
|
125
|
+
this.db.prepare(`INSERT INTO wand_task_comments(id, task_id, author_name, body, created_at) VALUES (?, ?, ?, ?, ?)`)
|
|
126
|
+
.run(id, input.taskId, "用户", input.body, nowIso());
|
|
127
|
+
this.db.prepare(`INSERT INTO wand_task_comment_dispatches(comment_id, task_id, request_id, signature, source_comment_id,
|
|
128
|
+
employee_id, employee_name, instruction, task_version, state) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
129
|
+
.run(id, input.taskId, input.requestId, input.signature, input.sourceCommentId, input.employeeId, input.employeeName, input.instruction, input.taskVersion, input.employeeId ? "pending" : "accepted");
|
|
130
|
+
return this.getWandTaskComment(id);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
updateTaskCommentDispatch(id, state, sessionId, error = null) {
|
|
134
|
+
this.db.prepare(`UPDATE wand_task_comment_dispatches SET state = ?, session_id = COALESCE(?, session_id), error = ? WHERE comment_id = ?`)
|
|
135
|
+
.run(state, sessionId, error, id);
|
|
136
|
+
}
|
|
137
|
+
/** Read-only recovery of the create/bind crash gap, by a server-generated correlation ID, never message text. */
|
|
138
|
+
findTaskCommentExecutionSessionId(commentId, employeeId) {
|
|
139
|
+
const rows = this.db.prepare("SELECT id FROM command_sessions WHERE automation_id = ? LIMIT 2")
|
|
140
|
+
.all(`wand-task-comment:${commentId}`);
|
|
141
|
+
return rows.length === 1 && this.s.getSessionSlim(rows[0].id)?.employeeId === employeeId ? rows[0].id : null;
|
|
142
|
+
}
|
|
143
|
+
/** Event-owned facts, including stop outcomes which structured sessions intentionally represent as idle. */
|
|
144
|
+
recordTaskCommentSessionEvent(event) {
|
|
145
|
+
const data = event.data;
|
|
146
|
+
if (!data || typeof data !== "object")
|
|
147
|
+
return;
|
|
148
|
+
const status = data.turnOutcome === "stopped" ? "stopped" : data.structuredState?.inFlight ? "running"
|
|
149
|
+
: data.structuredState?.lastError ? "failed" : data.status;
|
|
150
|
+
if (!status)
|
|
151
|
+
return;
|
|
152
|
+
this.db.prepare("UPDATE wand_task_comment_dispatches SET execution_status = ? WHERE session_id = ?")
|
|
153
|
+
.run(status, event.sessionId);
|
|
105
154
|
}
|
|
106
155
|
getWandTask(id) {
|
|
107
156
|
const row = this.db.prepare(`SELECT ${WAND_TASK_FIELDS} FROM wand_tasks WHERE id = ?`)
|
|
@@ -37,6 +37,7 @@ export declare class WorkspaceRepository {
|
|
|
37
37
|
/** Normal workspace counts exclude archived sessions and sessions in archived tasks. */
|
|
38
38
|
countSessionsByWorkspace(options?: {
|
|
39
39
|
includeArchived?: boolean;
|
|
40
|
+
excludeSystemAiCalls?: boolean;
|
|
40
41
|
}): Map<string, number>;
|
|
41
42
|
/** Count user-initiated CLI launches (one per session), including archived sessions. */
|
|
42
43
|
countInteractiveSessionsByProvider(): Record<SessionProvider, number>;
|
|
@@ -8,6 +8,7 @@ import { isUnnamedWorkspaceTaskName } from "../wand-task-sync.js";
|
|
|
8
8
|
import { ensureWorkspaceForCwd } from "../workspace-binding.js";
|
|
9
9
|
import { parseWorktreeInfo, sessionSelectFields, mapWorkspaceRow, mapWorkspaceTaskRow, mapSessionCore, sessionRowQuery } from "./mappers.js";
|
|
10
10
|
import { WAND_TASK_FIELDS, WORKSPACE_TASK_PROJECTION } from "./internal.js";
|
|
11
|
+
import { SYSTEM_AI_CALL_PREFIX } from "../system-ai-call.js";
|
|
11
12
|
/** Workspace storage: one domain of the WandStorage facade. Shares the connection and the facade's transaction. */
|
|
12
13
|
export class WorkspaceRepository {
|
|
13
14
|
db;
|
|
@@ -173,8 +174,9 @@ export class WorkspaceRepository {
|
|
|
173
174
|
SELECT status FROM wand_tasks WHERE workspace_task_id = session.workspace_task_id
|
|
174
175
|
ORDER BY updated_at DESC, rowid DESC LIMIT 1
|
|
175
176
|
), '') <> 'archived'))
|
|
177
|
+
AND (? = 0 OR COALESCE(session.automation_id, '') NOT GLOB ?)
|
|
176
178
|
GROUP BY session.workspace_id`)
|
|
177
|
-
.all(options.includeArchived === true ? 1 : 0);
|
|
179
|
+
.all(options.includeArchived === true ? 1 : 0, options.excludeSystemAiCalls === true ? 1 : 0, `${SYSTEM_AI_CALL_PREFIX}*`);
|
|
178
180
|
return new Map(rows.map((row) => [row.id, Number(row.n) || 0]));
|
|
179
181
|
}
|
|
180
182
|
/** Count user-initiated CLI launches (one per session), including archived sessions. */
|
|
@@ -11,7 +11,18 @@ export interface CleanupReport {
|
|
|
11
11
|
allocatedBytes: number;
|
|
12
12
|
skipped: number;
|
|
13
13
|
issues: string[];
|
|
14
|
-
byKind: Partial<Record<TempKind | "upload" | "diagnostic-log", number>>;
|
|
14
|
+
byKind: Partial<Record<TempKind | "upload" | "diagnostic-log" | "distribution-package" | "native-run" | "migration-snapshot", number>>;
|
|
15
|
+
}
|
|
16
|
+
/** pi-native 回收结果(与 PiTmuxHost.collectGarbage 的返回一致)。 */
|
|
17
|
+
export interface PiNativeGcReport {
|
|
18
|
+
removedSlots: number;
|
|
19
|
+
removedTurns: number;
|
|
20
|
+
freedBytes: number;
|
|
21
|
+
}
|
|
22
|
+
/** 分发包清理结果(由 DistributionManager 提供,只保留最新一套产物)。 */
|
|
23
|
+
export interface DistributionPruneResult {
|
|
24
|
+
deleted: string[];
|
|
25
|
+
freedBytes: number;
|
|
15
26
|
}
|
|
16
27
|
interface CleanupOptions {
|
|
17
28
|
roots: string[];
|
|
@@ -23,6 +34,24 @@ interface CleanupOptions {
|
|
|
23
34
|
uploads?: Pick<UploadRepository, "listManagedUploads" | "managedUploadReferenced" | "observeManagedUpload" | "forgetManagedUpload">;
|
|
24
35
|
uploadsActive?: () => boolean;
|
|
25
36
|
onUploadRemoved?: (cwd: string) => void;
|
|
37
|
+
/**
|
|
38
|
+
* 清理分发目录里的历史构建包(Android APK / macOS zip+dmg / iOS IPA)。
|
|
39
|
+
* 只在 apply 时调用:预览不能删文件。保留规则由 DistributionManager 统一持有,
|
|
40
|
+
* 这里不重复一份「哪个是最新」的判断。
|
|
41
|
+
*/
|
|
42
|
+
pruneDistributionArtifacts?: () => Promise<DistributionPruneResult>;
|
|
43
|
+
/**
|
|
44
|
+
* 回收 pi-native 下废弃的回合目录。同样只在 apply 时调用,失败不阻断其他回收。
|
|
45
|
+
*/
|
|
46
|
+
collectNativeArtifacts?: () => PiNativeGcReport | null;
|
|
47
|
+
/**
|
|
48
|
+
* 修剪迁移前 SQLite 快照(旧版本遗留的无上限堆积)。迁移成功路径已经在
|
|
49
|
+
* storage-schema 里修剪一次,这里只是把历史遗留的一起收掉。
|
|
50
|
+
*/
|
|
51
|
+
pruneMigrationSnapshots?: () => {
|
|
52
|
+
deleted: string[];
|
|
53
|
+
freedBytes: number;
|
|
54
|
+
};
|
|
26
55
|
}
|
|
27
56
|
/** Runtime-only, explicit ownership. User history, downloads and rollback inputs are never candidates. */
|
|
28
57
|
export declare class StorageCleanup {
|
|
@@ -44,7 +73,13 @@ export declare class StorageCleanup {
|
|
|
44
73
|
stop(): Promise<void>;
|
|
45
74
|
run(apply?: boolean): Promise<CleanupReport>;
|
|
46
75
|
private sweep;
|
|
76
|
+
/**
|
|
77
|
+
* 历史构建包回收。这是唯一需要把「版本比较」交给外部的清理:哪个包是「最新」
|
|
78
|
+
* 必须和更新检查同一个答案,所以保留规则住在 DistributionManager,这里只调用。
|
|
79
|
+
* 失败不阻断其他回收。
|
|
80
|
+
*/
|
|
81
|
+
private sweepDistributionArtifacts;
|
|
47
82
|
private sweepTemporary;
|
|
48
83
|
}
|
|
49
|
-
export declare function createStorageCleanup(configDir: string, options?: Pick<CleanupOptions, "uploads" | "uploadsActive" | "onUploadRemoved">): StorageCleanup;
|
|
84
|
+
export declare function createStorageCleanup(configDir: string, options?: Pick<CleanupOptions, "uploads" | "uploadsActive" | "onUploadRemoved" | "pruneDistributionArtifacts" | "collectNativeArtifacts" | "pruneMigrationSnapshots">): StorageCleanup;
|
|
50
85
|
export {};
|
package/dist/storage-cleanup.js
CHANGED
|
@@ -212,8 +212,68 @@ export class StorageCleanup {
|
|
|
212
212
|
report.skipped++;
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
|
+
await this.sweepDistributionArtifacts(apply, report);
|
|
215
216
|
return report;
|
|
216
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* 历史构建包回收。这是唯一需要把「版本比较」交给外部的清理:哪个包是「最新」
|
|
220
|
+
* 必须和更新检查同一个答案,所以保留规则住在 DistributionManager,这里只调用。
|
|
221
|
+
* 失败不阻断其他回收。
|
|
222
|
+
*/
|
|
223
|
+
async sweepDistributionArtifacts(apply, report) {
|
|
224
|
+
if (!apply || this.stopped)
|
|
225
|
+
return;
|
|
226
|
+
const detail = [];
|
|
227
|
+
if (this.options.pruneDistributionArtifacts) {
|
|
228
|
+
try {
|
|
229
|
+
const pruned = await this.options.pruneDistributionArtifacts();
|
|
230
|
+
if (pruned.deleted.length > 0) {
|
|
231
|
+
report.removed += pruned.deleted.length;
|
|
232
|
+
report.logicalBytes += pruned.freedBytes;
|
|
233
|
+
report.byKind["distribution-package"] = pruned.deleted.length;
|
|
234
|
+
detail.push(`分发构建包 ${pruned.deleted.length} 个(${(pruned.freedBytes / 1024 / 1024).toFixed(1)} MB):${pruned.deleted.join(", ")}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
report.issues.push("Distribution package cleanup failed; packages preserved.");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (this.options.collectNativeArtifacts) {
|
|
242
|
+
try {
|
|
243
|
+
const native = this.options.collectNativeArtifacts();
|
|
244
|
+
const removed = (native?.removedSlots ?? 0) + (native?.removedTurns ?? 0);
|
|
245
|
+
if (native && removed > 0) {
|
|
246
|
+
report.removed += removed;
|
|
247
|
+
report.logicalBytes += native.freedBytes;
|
|
248
|
+
report.byKind["native-run"] = removed;
|
|
249
|
+
detail.push(`Pi 原生产物 ${removed} 个(${(native.freedBytes / 1024 / 1024).toFixed(1)} MB)`
|
|
250
|
+
+ `:slot ${native.removedSlots} 个 / turn ${native.removedTurns} 个`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
report.issues.push("Native run artifact cleanup failed; artifacts preserved.");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (this.options.pruneMigrationSnapshots) {
|
|
258
|
+
try {
|
|
259
|
+
const snapshots = this.options.pruneMigrationSnapshots();
|
|
260
|
+
if (snapshots.deleted.length > 0) {
|
|
261
|
+
report.removed += snapshots.deleted.length;
|
|
262
|
+
report.logicalBytes += snapshots.freedBytes;
|
|
263
|
+
report.byKind["migration-snapshot"] = snapshots.deleted.length;
|
|
264
|
+
detail.push(`迁移快照 ${snapshots.deleted.length} 份(${(snapshots.freedBytes / 1024 / 1024).toFixed(1)} MB):${snapshots.deleted.join(", ")}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
report.issues.push("Migration snapshot cleanup failed; snapshots preserved.");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// 后台删除必须留痕迹:这些路径不是用户主动触发,没有日志就无法事后追溯删了什么。
|
|
272
|
+
if (detail.length > 0)
|
|
273
|
+
process.stdout.write(`[storage-cleanup] 已回收 ${detail.join(";")}\n`);
|
|
274
|
+
for (const issue of report.issues)
|
|
275
|
+
process.stdout.write(`[storage-cleanup] ${issue}\n`);
|
|
276
|
+
}
|
|
217
277
|
async sweepTemporary(apply) {
|
|
218
278
|
const now = (this.options.now ?? Date.now)();
|
|
219
279
|
const cutoff = now - STORAGE_CLEANUP_RETENTION_DAYS * DAY;
|
|
@@ -266,7 +326,7 @@ export class StorageCleanup {
|
|
|
266
326
|
report.skipped++;
|
|
267
327
|
continue;
|
|
268
328
|
}
|
|
269
|
-
const stat = await stats(directory, (await lstat(root)).dev, lease.kind === "test-run"
|
|
329
|
+
const stat = await stats(directory, (await lstat(root)).dev, lease.kind === "test-run");
|
|
270
330
|
if (stat.latest >= cutoff) {
|
|
271
331
|
report.skipped++;
|
|
272
332
|
continue;
|
|
@@ -311,7 +371,7 @@ export class StorageCleanup {
|
|
|
311
371
|
report.skipped++;
|
|
312
372
|
continue;
|
|
313
373
|
}
|
|
314
|
-
const latest = await stats(candidate.directory, candidate.stat.dev, lease.kind === "test-run"
|
|
374
|
+
const latest = await stats(candidate.directory, candidate.stat.dev, lease.kind === "test-run");
|
|
315
375
|
if (latest.ino !== candidate.stat.ino || latest.latest >= cutoff) {
|
|
316
376
|
report.skipped++;
|
|
317
377
|
continue;
|
package/dist/storage-schema.js
CHANGED
|
@@ -3,6 +3,7 @@ import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
5
|
import { STORAGE_MIGRATIONS } from "./storage/migrations.js";
|
|
6
|
+
import { pruneMigrationBackups } from "./migration-backup-retention.js";
|
|
6
7
|
import { SCHEDULED_CLAIM_DATE_SQL } from "./storage-schedules.js";
|
|
7
8
|
/** Additive schema only. Domain migrations and transactions remain owned by WandStorage. */
|
|
8
9
|
const INIT_SQL = `
|
|
@@ -733,6 +734,14 @@ export function migrateStorageSchema(db, options) {
|
|
|
733
734
|
throw new Error(`[wand] storage migration v${migration.version} (${migration.name}) failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
734
735
|
}
|
|
735
736
|
}
|
|
737
|
+
// 只有全部迁移提交成功后才修剪旧快照:失败时回滚点必须完整保留。
|
|
738
|
+
// 本次刚生成的这一份始终保留,修剪失败不影响迁移结果。
|
|
739
|
+
if (backupPath) {
|
|
740
|
+
try {
|
|
741
|
+
pruneMigrationBackups(options.dbPath, { keepPath: backupPath });
|
|
742
|
+
}
|
|
743
|
+
catch { /* best-effort */ }
|
|
744
|
+
}
|
|
736
745
|
return { fromVersion, toVersion: readStorageSchemaVersion(db), backupPath, newerSchema: false };
|
|
737
746
|
}
|
|
738
747
|
/** Open (creating if needed) and migrate a database file. Owner-only permissions. */
|
|
@@ -151,7 +151,11 @@ export class ClaudeCliRunner {
|
|
|
151
151
|
file: "claude",
|
|
152
152
|
args,
|
|
153
153
|
cwd: context.session.cwd,
|
|
154
|
-
|
|
154
|
+
// Claude print mode otherwise kills live background agents ten minutes
|
|
155
|
+
// after the parent's last turn, even while children are making progress,
|
|
156
|
+
// then exits 0. Wand owns cancellation; keep the CLI alive until its work
|
|
157
|
+
// settles instead of imposing that independent parent-idle deadline.
|
|
158
|
+
env: { ...context.env, CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS: "0" },
|
|
155
159
|
stdinData: context.prompt,
|
|
156
160
|
observer,
|
|
157
161
|
execHost: this.execHost,
|
|
@@ -20,7 +20,7 @@ interface ClaudeCliTurnState extends StructuredRunnerTurnState {
|
|
|
20
20
|
export declare class ClaudeCliProtocolReducer {
|
|
21
21
|
readonly state: ClaudeCliTurnState;
|
|
22
22
|
askUserQuestionDetected: boolean;
|
|
23
|
-
/** result
|
|
23
|
+
/** 原生 result 错误或系统强停子代理的说明,作为这一轮的主错误。 */
|
|
24
24
|
errorText: string | null;
|
|
25
25
|
/** 协议里出现过带 is_error 的 result 事件;没有本轮助手内容时才是可降级的执行前拒绝。 */
|
|
26
26
|
errorResultObserved: boolean;
|
|
@@ -121,7 +121,7 @@ export function extractClaudeAssistantMessage(message) {
|
|
|
121
121
|
export class ClaudeCliProtocolReducer {
|
|
122
122
|
state;
|
|
123
123
|
askUserQuestionDetected = false;
|
|
124
|
-
/** result
|
|
124
|
+
/** 原生 result 错误或系统强停子代理的说明,作为这一轮的主错误。 */
|
|
125
125
|
errorText = null;
|
|
126
126
|
/** 协议里出现过带 is_error 的 result 事件;没有本轮助手内容时才是可降级的执行前拒绝。 */
|
|
127
127
|
errorResultObserved = false;
|
|
@@ -269,6 +269,17 @@ export class ClaudeCliProtocolReducer {
|
|
|
269
269
|
if (detail)
|
|
270
270
|
this.errorText = detail;
|
|
271
271
|
}
|
|
272
|
+
else if (!this.qoder) {
|
|
273
|
+
// Claude 2.1.296 can report success / exit 0 after its background-wait
|
|
274
|
+
// ceiling has killed children. Use the native execution facts, never
|
|
275
|
+
// the parent's prose or stderr keywords, and never classify this as an
|
|
276
|
+
// upstream rejection that would authorize replay of already-run work.
|
|
277
|
+
const stats = event.subagent_stats;
|
|
278
|
+
const killed = stats?.killed?.system;
|
|
279
|
+
if (typeof killed === "number" && Number.isSafeInteger(killed) && killed > 0 && !this.errorText) {
|
|
280
|
+
this.errorText = `Claude Code 被系统停止了 ${killed} 个子代理,任务未完整完成;请检查已有改动后再续接,不要自动重跑。`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
272
283
|
this.state.model = extractClaudeModelName(event.modelUsage && typeof event.modelUsage === "object"
|
|
273
284
|
? event.modelUsage
|
|
274
285
|
: undefined) ?? this.state.model;
|
|
@@ -49,6 +49,8 @@ export interface StructuredRunnerContext {
|
|
|
49
49
|
env: NodeJS.ProcessEnv;
|
|
50
50
|
/** Execution-only group boundary; never persisted/projected to session DTOs. */
|
|
51
51
|
modelGroupModels?: readonly string[];
|
|
52
|
+
/** Core-only: synchronously persist one queued user turn and the preceding assistant segment. */
|
|
53
|
+
takeSteering?(state: StructuredRunnerTurnState): string | null;
|
|
52
54
|
}
|
|
53
55
|
export interface StructuredRunnerObserver {
|
|
54
56
|
isActive(): boolean;
|
|
@@ -10,6 +10,7 @@ import type { CoreStatus } from "./core-status-cli.js";
|
|
|
10
10
|
import { type EngineResolution } from "./harness-engine.js";
|
|
11
11
|
import { type PiSessionSettings } from "./pi-session-settings.js";
|
|
12
12
|
import { type DecisionRuntimeAccess } from "./decision-runner.js";
|
|
13
|
+
import { type PiNativeGcReport } from "./pi-tmux-host.js";
|
|
13
14
|
import { type StructuredExecHost } from "./structured-exec-host.js";
|
|
14
15
|
import type { StructuredRunnerAdapter } from "./structured-runner.js";
|
|
15
16
|
export { structuredQuietStatusData, getLastSubmittedStructuredInput, isDuplicateStructuredQueueInput } from "./structured-session-manager-replay.js";
|
|
@@ -177,6 +178,15 @@ export declare class StructuredSessionManager {
|
|
|
177
178
|
private saveAuthoritativeSession;
|
|
178
179
|
private checkpointSessionMessages;
|
|
179
180
|
list(): SessionSnapshot[];
|
|
181
|
+
/**
|
|
182
|
+
* 回收 Pi 原生执行宿主下的废弃产物(旧 turn 目录与无主 slot)。
|
|
183
|
+
* 只有真在跑原生 tmux 时才有意义,否则返回 null 表示本次未执行。
|
|
184
|
+
*/
|
|
185
|
+
collectNativeGarbage(options: {
|
|
186
|
+
liveSessionIds: ReadonlySet<string>;
|
|
187
|
+
retentionMs: number;
|
|
188
|
+
now?: number;
|
|
189
|
+
}): PiNativeGcReport | null;
|
|
180
190
|
/** Return lightweight snapshots for the session list (no output/messages). */
|
|
181
191
|
listSlim(): SessionSnapshot[];
|
|
182
192
|
get(id: string): SessionSnapshot | null;
|
|
@@ -240,6 +250,7 @@ export declare class StructuredSessionManager {
|
|
|
240
250
|
private settleAutoAssignForSend;
|
|
241
251
|
/** Runs one turn through the runner that owns the session's provider (and, for Pi, its engine). */
|
|
242
252
|
private runProviderTurn;
|
|
253
|
+
private usesCoreEngine;
|
|
243
254
|
private runPiTurn;
|
|
244
255
|
/**
|
|
245
256
|
* A turn threw before the runner reported its own outcome. Re-throws errors that
|
|
@@ -255,7 +266,7 @@ export declare class StructuredSessionManager {
|
|
|
255
266
|
* 期间改"已经被 flushNextQueuedMessage 拿走的队首",但本方法只动 queue 数组
|
|
256
267
|
* 本身,flushNext 在另一段时序里读 sessions.get(...) 当前快照,已经天然安全。
|
|
257
268
|
*/
|
|
258
|
-
reorderQueuedMessages(sessionId: string, order: number[]): SessionSnapshot;
|
|
269
|
+
reorderQueuedMessages(sessionId: string, order: number[], expectedQueue?: readonly string[]): SessionSnapshot;
|
|
259
270
|
/** Edit only the pending item still identified by both index and original text. */
|
|
260
271
|
editQueuedMessage(sessionId: string, index: number, expectedText: string, text: string): SessionSnapshot;
|
|
261
272
|
/** Remove a single queued message only while index and text still identify the same item. */
|
|
@@ -266,8 +277,9 @@ export declare class StructuredSessionManager {
|
|
|
266
277
|
* queue entry remains available for the automatic flush path.
|
|
267
278
|
*/
|
|
268
279
|
promoteQueuedMessage(sessionId: string, index: number, expectedText?: string, idempotencyKey?: string): Promise<SessionSnapshot>;
|
|
280
|
+
private assertExpectedQueue;
|
|
269
281
|
/** Clear all queued messages. No-op when queue is already empty. */
|
|
270
|
-
clearQueuedMessages(sessionId: string): SessionSnapshot;
|
|
282
|
+
clearQueuedMessages(sessionId: string, expectedQueue?: readonly string[]): SessionSnapshot;
|
|
271
283
|
/** 仅空白独立对话能改目录:不建替代会话、不迁移任务/worktree 或已接受的输入。 */
|
|
272
284
|
setSessionDirectory(sessionId: string, cwd: string): SessionSnapshot;
|
|
273
285
|
/** 空白对话原位换执行工具;引擎也是工具身份,不跨工具搬历史或能力设置。 */
|
|
@@ -1039,6 +1039,16 @@ export class StructuredSessionManager {
|
|
|
1039
1039
|
.map(withSummary)
|
|
1040
1040
|
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
1041
1041
|
}
|
|
1042
|
+
/**
|
|
1043
|
+
* 回收 Pi 原生执行宿主下的废弃产物(旧 turn 目录与无主 slot)。
|
|
1044
|
+
* 只有真在跑原生 tmux 时才有意义,否则返回 null 表示本次未执行。
|
|
1045
|
+
*/
|
|
1046
|
+
collectNativeGarbage(options) {
|
|
1047
|
+
const host = this.execHost;
|
|
1048
|
+
if (!(host instanceof PiTmuxHost))
|
|
1049
|
+
return null;
|
|
1050
|
+
return host.collectGarbage(options);
|
|
1051
|
+
}
|
|
1042
1052
|
/** Return lightweight snapshots for the session list (no output/messages). */
|
|
1043
1053
|
listSlim() {
|
|
1044
1054
|
return Array.from(this.sessions.values())
|
|
@@ -1399,7 +1409,10 @@ export class StructuredSessionManager {
|
|
|
1399
1409
|
return await this.relayUserMessage(id, prompt, relay);
|
|
1400
1410
|
if (opts?.idempotencyKey)
|
|
1401
1411
|
this.claimIdempotencyKey(id, opts.idempotencyKey);
|
|
1402
|
-
//
|
|
1412
|
+
// The drain owns Core admission, not CLI admission. Check before any
|
|
1413
|
+
// asynchronous selection/topic work; accepted Core inputs remain durable.
|
|
1414
|
+
if (this.coreRestartDrains > 0 && this.usesCoreEngine(session))
|
|
1415
|
+
return this.enqueueInput(session, prompt);
|
|
1403
1416
|
this.maybeGenerateSessionTopic(id, prompt);
|
|
1404
1417
|
// 「智能分配」结算窗口:还没有 runner 归属,新输入先排队,不能另起一轮。
|
|
1405
1418
|
if (this.autoAssignPending.has(id))
|
|
@@ -1412,8 +1425,20 @@ export class StructuredSessionManager {
|
|
|
1412
1425
|
const assigned = await this.settleAutoAssignForSend(id, session, prompt);
|
|
1413
1426
|
if ("result" in assigned)
|
|
1414
1427
|
return assigned.result;
|
|
1415
|
-
session =
|
|
1416
|
-
|
|
1428
|
+
session = this.requireSession(id);
|
|
1429
|
+
// Selection yielded: a drain or another input may now own admission.
|
|
1430
|
+
// This request preceded inputs accepted during its selection window.
|
|
1431
|
+
if (this.coreRestartDrains > 0 && this.usesCoreEngine(session))
|
|
1432
|
+
return this.enqueueInput(session, prompt, true);
|
|
1433
|
+
const ready = this.resolveInFlightSend(id, session, prompt, opts);
|
|
1434
|
+
if ("result" in ready)
|
|
1435
|
+
return ready.result;
|
|
1436
|
+
session = ready.session;
|
|
1437
|
+
}
|
|
1438
|
+
// From this gate through runner.start/ownership registration there is no
|
|
1439
|
+
// await, so the drain cannot observe idle and then admit a new Core turn.
|
|
1440
|
+
if (this.coreRestartDrains > 0 && this.usesCoreEngine(session))
|
|
1441
|
+
return this.enqueueInput(session, prompt);
|
|
1417
1442
|
// 检测上一轮 assistant 是否有未配对的 AskUserQuestion tool_use(说明前一次
|
|
1418
1443
|
// child 是被 SIGTERM 主动 kill 的,正在等用户回答)。如果有,把这次的输入打包
|
|
1419
1444
|
// 成 tool_result 注入到 messages,让 UI 把卡片渲染为 answered。
|
|
@@ -1643,11 +1668,15 @@ export class StructuredSessionManager {
|
|
|
1643
1668
|
await this.runStructuredStreaming(id, updated, cliClaudePrompt, requestId);
|
|
1644
1669
|
}
|
|
1645
1670
|
}
|
|
1671
|
+
usesCoreEngine(session) {
|
|
1672
|
+
if ((session.provider ?? session.structuredState?.provider) !== "pi")
|
|
1673
|
+
return false;
|
|
1674
|
+
const decided = session.structuredState?.engine;
|
|
1675
|
+
return decided ? decided === "core" : session.employeeCandidates?.[session.employeeCandidateIndex ?? 0]?.engine === "sdk";
|
|
1676
|
+
}
|
|
1646
1677
|
async runPiTurn(id, updated, prompt, requestId) {
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
const decidedEngine = updated.structuredState?.engine;
|
|
1650
|
-
const sdkRequested = decidedEngine ? decidedEngine === "core" : employeeAgent?.engine === "sdk";
|
|
1678
|
+
// Admission and dispatch must agree for both pinned and legacy employee engines.
|
|
1679
|
+
const sdkRequested = this.usesCoreEngine(updated);
|
|
1651
1680
|
// 没显式选择时对话框固定走 CLI;只有选定 Wand Agent 或员工候选配置 SDK 才是 core。
|
|
1652
1681
|
// Register the selected Core runner synchronously before its lazy SDK/model
|
|
1653
1682
|
// preparation. This preserves cancellation ownership and the restart drain;
|
|
@@ -1747,9 +1776,10 @@ export class StructuredSessionManager {
|
|
|
1747
1776
|
* 期间改"已经被 flushNextQueuedMessage 拿走的队首",但本方法只动 queue 数组
|
|
1748
1777
|
* 本身,flushNext 在另一段时序里读 sessions.get(...) 当前快照,已经天然安全。
|
|
1749
1778
|
*/
|
|
1750
|
-
reorderQueuedMessages(sessionId, order) {
|
|
1779
|
+
reorderQueuedMessages(sessionId, order, expectedQueue) {
|
|
1751
1780
|
const session = this.requireSession(sessionId);
|
|
1752
1781
|
const queue = session.queuedMessages ?? [];
|
|
1782
|
+
this.assertExpectedQueue(queue, expectedQueue);
|
|
1753
1783
|
if (!Array.isArray(order) || order.length !== queue.length) {
|
|
1754
1784
|
throw new Error("排序长度与当前队列不一致,请刷新后重试。");
|
|
1755
1785
|
}
|
|
@@ -1816,6 +1846,9 @@ export class StructuredSessionManager {
|
|
|
1816
1846
|
async promoteQueuedMessage(sessionId, index, expectedText, idempotencyKey) {
|
|
1817
1847
|
const session = this.requireSession(sessionId);
|
|
1818
1848
|
this.requireRecoveryControl(sessionId);
|
|
1849
|
+
if (this.coreRestartDrains > 0 && this.usesCoreEngine(session)) {
|
|
1850
|
+
throw new Error("服务正在排空 Wand Agent,排队消息已保留,请稍后重试立即发送。");
|
|
1851
|
+
}
|
|
1819
1852
|
if (idempotencyKey && this.seenIdempotencyKeys.has(`${sessionId}:${idempotencyKey}`)) {
|
|
1820
1853
|
return session;
|
|
1821
1854
|
}
|
|
@@ -1850,9 +1883,16 @@ export class StructuredSessionManager {
|
|
|
1850
1883
|
return this.requireSession(sessionId);
|
|
1851
1884
|
}
|
|
1852
1885
|
}
|
|
1886
|
+
assertExpectedQueue(queue, expected) {
|
|
1887
|
+
if (expected !== undefined && (!Array.isArray(expected) || expected.length !== queue.length
|
|
1888
|
+
|| expected.some((text, index) => typeof text !== "string" || text !== queue[index]))) {
|
|
1889
|
+
throw new Error("排队消息已变化,请刷新后重试。");
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1853
1892
|
/** Clear all queued messages. No-op when queue is already empty. */
|
|
1854
|
-
clearQueuedMessages(sessionId) {
|
|
1893
|
+
clearQueuedMessages(sessionId, expectedQueue) {
|
|
1855
1894
|
const session = this.requireSession(sessionId);
|
|
1895
|
+
this.assertExpectedQueue(session.queuedMessages ?? [], expectedQueue);
|
|
1856
1896
|
if (!session.queuedMessages || session.queuedMessages.length === 0) {
|
|
1857
1897
|
return session;
|
|
1858
1898
|
}
|
|
@@ -2424,7 +2464,7 @@ export class StructuredSessionManager {
|
|
|
2424
2464
|
data,
|
|
2425
2465
|
});
|
|
2426
2466
|
}
|
|
2427
|
-
enqueueInput(session, prompt) {
|
|
2467
|
+
enqueueInput(session, prompt, prepend = false) {
|
|
2428
2468
|
if (isDuplicateStructuredQueueInput(session, prompt)) {
|
|
2429
2469
|
const err = new Error("与上一条消息相同,已忽略,不会加入排队。");
|
|
2430
2470
|
err.code = "duplicate_queued_message";
|
|
@@ -2433,7 +2473,7 @@ export class StructuredSessionManager {
|
|
|
2433
2473
|
const queue = session.queuedMessages ?? [];
|
|
2434
2474
|
if (queue.length >= 10)
|
|
2435
2475
|
throw new Error("排队消息已满(最多 10 条),请等待当前消息处理完成。");
|
|
2436
|
-
const queued = { ...session, queuedMessages: [...queue, prompt] };
|
|
2476
|
+
const queued = { ...session, queuedMessages: prepend ? [prompt, ...queue] : [...queue, prompt] };
|
|
2437
2477
|
this.sessions.set(session.id, queued);
|
|
2438
2478
|
this.storage.updateSessionRuntimeMetadata(queued);
|
|
2439
2479
|
this.emitStructuredSnapshot(queued);
|
|
@@ -2446,7 +2486,7 @@ export class StructuredSessionManager {
|
|
|
2446
2486
|
if (!current || (current.queuedMessages?.length ?? 0) === 0) {
|
|
2447
2487
|
return;
|
|
2448
2488
|
}
|
|
2449
|
-
if (current.structuredState?.inFlight) {
|
|
2489
|
+
if (current.structuredState?.inFlight || (this.coreRestartDrains > 0 && this.usesCoreEngine(current))) {
|
|
2450
2490
|
return;
|
|
2451
2491
|
}
|
|
2452
2492
|
const [nextInput, ...restQueue] = current.queuedMessages ?? [];
|
|
@@ -2465,9 +2505,10 @@ export class StructuredSessionManager {
|
|
|
2465
2505
|
}
|
|
2466
2506
|
catch (error) {
|
|
2467
2507
|
console.error("[WAND] flushNextQueuedMessage failed:", error);
|
|
2468
|
-
//
|
|
2508
|
+
// Only an explicit pre-execution rejection permits replay. A persisted
|
|
2509
|
+
// runner failure or arbitrary exception can mean delivery is unknown.
|
|
2469
2510
|
const afterFail = this.sessions.get(sessionId);
|
|
2470
|
-
if (afterFail) {
|
|
2511
|
+
if (afterFail && error instanceof UnacceptedStructuredInputError) {
|
|
2471
2512
|
const rescued = {
|
|
2472
2513
|
...afterFail,
|
|
2473
2514
|
queuedMessages: [nextInput, ...(afterFail.queuedMessages ?? [])],
|
|
@@ -2476,6 +2517,17 @@ export class StructuredSessionManager {
|
|
|
2476
2517
|
this.storage.updateSessionRuntimeMetadata(rescued);
|
|
2477
2518
|
this.emitStructuredSnapshot(rescued);
|
|
2478
2519
|
}
|
|
2520
|
+
else if (afterFail && (afterFail.messages?.length ?? 0) <= (current.messages?.length ?? 0)) {
|
|
2521
|
+
// If dispatch failed before a user turn was checkpointed, retain the
|
|
2522
|
+
// original visibly for reconciliation, never as another sendable item.
|
|
2523
|
+
const retained = { ...afterFail, messages: appendNoticeTurn(afterFail.messages, {
|
|
2524
|
+
role: "assistant", notice: true, content: [{ type: "text",
|
|
2525
|
+
text: "排队消息送达未确认,未自动重发。以下原文仅供核对,不是新的执行指令:\n\n" + nextInput }],
|
|
2526
|
+
}) };
|
|
2527
|
+
this.saveAuthoritativeSession(retained);
|
|
2528
|
+
this.sessions.set(sessionId, retained);
|
|
2529
|
+
this.emitStructuredSnapshot(retained);
|
|
2530
|
+
}
|
|
2479
2531
|
}
|
|
2480
2532
|
}
|
|
2481
2533
|
emit(event) {
|
|
@@ -2560,6 +2612,27 @@ export class StructuredSessionManager {
|
|
|
2560
2612
|
session,
|
|
2561
2613
|
prompt,
|
|
2562
2614
|
env: buildChildEnv(this.config.inheritEnv !== false),
|
|
2615
|
+
...(provider === "pi" && session.structuredState?.engine === "core" ? {
|
|
2616
|
+
takeSteering: (turnState) => {
|
|
2617
|
+
const current = this.currentSessionForRequest(sessionId, requestId);
|
|
2618
|
+
if (!current || this.disposed || this.interruptedWith.has(sessionId) || this.coreRestartDrains > 0)
|
|
2619
|
+
return null;
|
|
2620
|
+
const [input, ...remaining] = current.queuedMessages ?? [];
|
|
2621
|
+
if (!input)
|
|
2622
|
+
return null;
|
|
2623
|
+
const preceding = turnState.blocks.length || turnState.result
|
|
2624
|
+
? this.buildCompletedAssistantMessages(current, turnState) : [...(current.messages ?? [])];
|
|
2625
|
+
const consumed = {
|
|
2626
|
+
...current, queuedMessages: remaining,
|
|
2627
|
+
messages: [...preceding, buildUserTurn(input, undefined, isoNow())],
|
|
2628
|
+
};
|
|
2629
|
+
// Persist queue removal and transcript together before exposing consumption.
|
|
2630
|
+
this.saveAuthoritativeSession(consumed);
|
|
2631
|
+
this.sessions.set(sessionId, consumed);
|
|
2632
|
+
this.emitStructuredSnapshot(consumed);
|
|
2633
|
+
return input;
|
|
2634
|
+
},
|
|
2635
|
+
} : {}),
|
|
2563
2636
|
}, {
|
|
2564
2637
|
isActive: () => this.isCurrentRequest(sessionId, requestId),
|
|
2565
2638
|
onStdout: (text) => {
|
package/dist/system-ai-call.d.ts
CHANGED
|
@@ -21,9 +21,13 @@ export declare const SYSTEM_AI_PURPOSES: {
|
|
|
21
21
|
readonly "user-memory": "短期习惯分析";
|
|
22
22
|
readonly "employee-draft": "员工招聘草稿";
|
|
23
23
|
readonly decision: "有界决策";
|
|
24
|
-
readonly "speech-polish": "口述文字整理";
|
|
25
24
|
};
|
|
26
25
|
export type SystemAiPurpose = keyof typeof SYSTEM_AI_PURPOSES;
|
|
26
|
+
export declare const SYSTEM_AI_AUXILIARY_PURPOSES: ("decision" | "system-text" | "session-title" | "task-title" | "task-organize" | "conversation-organize-job" | "conversation-organize" | "conversation-summary" | "prompt-optimize" | "git-commit" | "git-tag" | "git-fallback" | "model-auto-assign" | "user-memory" | "employee-draft")[];
|
|
27
|
+
/** Unknown/unspecified history and agent decisions are protected, even with system-ai provenance. */
|
|
28
|
+
export declare function isRetainableSystemAiCall(session: {
|
|
29
|
+
automationId?: unknown;
|
|
30
|
+
} | null | undefined): boolean;
|
|
27
31
|
export interface AiCallTrace {
|
|
28
32
|
readonly sessionId: string;
|
|
29
33
|
candidateStarted(candidate: AiCliCandidate): void;
|