@co0ontty/wand 4.83.1 → 4.84.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 +4 -0
- package/dist/agent-dispatch.js +5 -1
- package/dist/ai-team-availability.d.ts +1 -1
- package/dist/ai-team-prompts.js +13 -4
- package/dist/ai-team-runner.d.ts +80 -5
- package/dist/ai-team-runner.js +332 -55
- package/dist/ai-team-types.d.ts +92 -5
- package/dist/ai-team-types.js +9 -2
- package/dist/build-info.json +3 -3
- package/dist/cli-api.d.ts +1 -0
- package/dist/cli-api.js +3 -0
- package/dist/cli.js +18 -1
- package/dist/commit-task-archive.d.ts +3 -1
- package/dist/commit-task-archive.js +21 -10
- package/dist/config.d.ts +1 -1
- package/dist/config.js +73 -0
- package/dist/conversation-mentions.d.ts +16 -0
- package/dist/conversation-mentions.js +32 -0
- package/dist/conversation-service.d.ts +90 -0
- package/dist/conversation-service.js +759 -0
- package/dist/conversation-session-preview.d.ts +9 -0
- package/dist/conversation-session-preview.js +47 -0
- package/dist/conversation-task-preview.d.ts +5 -0
- package/dist/conversation-task-preview.js +8 -0
- package/dist/conversation-types.d.ts +80 -0
- package/dist/conversation-types.js +7 -0
- package/dist/core-extension-host.d.ts +27 -0
- package/dist/core-extension-host.js +138 -0
- package/dist/core-runner.d.ts +86 -0
- package/dist/core-runner.js +757 -0
- package/dist/core-status-cli.d.ts +9 -0
- package/dist/core-status-cli.js +280 -0
- package/dist/core-turn-tracker.d.ts +12 -0
- package/dist/core-turn-tracker.js +36 -0
- package/dist/decision-runner.d.ts +6 -0
- package/dist/decision-runner.js +3 -1
- package/dist/decision-tool.d.ts +2 -0
- package/dist/decision-tool.js +49 -22
- package/dist/env-utils.d.ts +7 -3
- package/dist/env-utils.js +30 -5
- package/dist/git-quick-commit.d.ts +5 -0
- package/dist/git-quick-commit.js +9 -3
- package/dist/git-utils.js +2 -1
- package/dist/harness-engine.d.ts +110 -0
- package/dist/harness-engine.js +331 -0
- package/dist/mission-diff.js +2 -1
- package/dist/model-auto-assign.d.ts +50 -0
- package/dist/model-auto-assign.js +163 -0
- package/dist/model-group-runner.d.ts +4 -0
- package/dist/model-group-runner.js +72 -0
- package/dist/model-groups.d.ts +38 -0
- package/dist/model-groups.js +138 -0
- package/dist/models.d.ts +12 -0
- package/dist/models.js +102 -8
- package/dist/npm-update-utils.js +2 -1
- package/dist/openrouter-free-cli.d.ts +25 -0
- package/dist/openrouter-free-cli.js +109 -0
- package/dist/openrouter-free-models.d.ts +85 -0
- package/dist/openrouter-free-models.js +458 -0
- package/dist/openrouter-free-selection.d.ts +5 -0
- package/dist/openrouter-free-selection.js +7 -0
- package/dist/path-repair.d.ts +10 -0
- package/dist/path-repair.js +50 -5
- package/dist/pi-auto-resources.d.ts +12 -0
- package/dist/pi-auto-resources.js +151 -0
- package/dist/pi-execution-types.d.ts +48 -0
- package/dist/pi-execution-types.js +1 -0
- package/dist/pi-execution.d.ts +9 -0
- package/dist/pi-execution.js +187 -0
- package/dist/pi-extension-resources.d.ts +13 -0
- package/dist/pi-extension-resources.js +71 -0
- package/dist/pi-openrouter-free-bridge.d.ts +2 -0
- package/dist/pi-openrouter-free-bridge.js +80 -0
- package/dist/pi-resource-bridge.d.ts +2 -0
- package/dist/pi-resource-bridge.js +73 -0
- package/dist/pi-resource-catalog.d.ts +18 -0
- package/dist/pi-resource-catalog.js +82 -0
- package/dist/pi-resource-recommendation.d.ts +29 -0
- package/dist/pi-resource-recommendation.js +169 -0
- package/dist/pi-resource-run.d.ts +8 -0
- package/dist/pi-resource-run.js +28 -0
- package/dist/pi-session-settings.d.ts +157 -0
- package/dist/pi-session-settings.js +166 -0
- package/dist/platform-action-catalog.d.ts +24 -0
- package/dist/platform-action-catalog.js +141 -0
- package/dist/platform-action-service.d.ts +37 -0
- package/dist/platform-action-service.js +138 -0
- package/dist/platform-action-types.d.ts +125 -0
- package/dist/platform-action-types.js +7 -0
- package/dist/process-manager.d.ts +21 -1
- package/dist/process-manager.js +168 -15
- package/dist/provider-catalog.js +1 -1
- package/dist/provider-cli-updater.js +29 -8
- package/dist/provider-history-scanner.d.ts +9 -0
- package/dist/provider-history-scanner.js +107 -1
- package/dist/retention.d.ts +19 -7
- package/dist/retention.js +171 -28
- package/dist/server-ai-team-routes.js +6 -2
- package/dist/server-conversation-routes.d.ts +5 -0
- package/dist/server-conversation-routes.js +91 -0
- package/dist/server-employee-routes.d.ts +2 -0
- package/dist/server-employee-routes.js +85 -11
- package/dist/server-pi-recommendation-routes.d.ts +13 -0
- package/dist/server-pi-recommendation-routes.js +67 -0
- package/dist/server-resume-routes.js +1 -1
- package/dist/server-session-routes.d.ts +4 -3
- package/dist/server-session-routes.js +182 -37
- package/dist/server-settings-routes.d.ts +5 -0
- package/dist/server-settings-routes.js +46 -0
- package/dist/server-task-routes.js +49 -4
- package/dist/server-team-dispatch-routes.d.ts +19 -0
- package/dist/server-team-dispatch-routes.js +71 -0
- package/dist/server-update-routes.d.ts +1 -1
- package/dist/server-update-routes.js +10 -5
- package/dist/server-workspace-routes.js +44 -24
- package/dist/server.d.ts +1 -0
- package/dist/server.js +148 -30
- package/dist/session-ai-context.d.ts +5 -1
- package/dist/session-ai-context.js +9 -4
- package/dist/session-registry.d.ts +4 -0
- package/dist/session-registry.js +47 -13
- package/dist/session-transport.js +8 -1
- package/dist/settings-web-access.d.ts +15 -0
- package/dist/settings-web-access.js +37 -0
- package/dist/silicon-employee-defaults.d.ts +29 -0
- package/dist/silicon-employee-defaults.js +41 -0
- package/dist/silicon-employee-dispatch.d.ts +5 -1
- package/dist/silicon-employee-dispatch.js +10 -4
- package/dist/silicon-employee-draft.d.ts +21 -4
- package/dist/silicon-employee-draft.js +103 -16
- package/dist/storage.d.ts +73 -2
- package/dist/storage.js +362 -26
- package/dist/structured-claude-protocol.js +1 -1
- package/dist/structured-client-protocol.js +11 -33
- package/dist/structured-content.d.ts +23 -0
- package/dist/structured-content.js +72 -0
- package/dist/structured-failure.d.ts +16 -0
- package/dist/structured-failure.js +36 -0
- package/dist/structured-pi-adapter.d.ts +8 -3
- package/dist/structured-pi-adapter.js +131 -30
- package/dist/structured-provider-common.d.ts +6 -0
- package/dist/structured-provider-common.js +23 -1
- package/dist/structured-runner.d.ts +45 -0
- package/dist/structured-session-manager.d.ts +78 -4
- package/dist/structured-session-manager.js +513 -129
- package/dist/structured-thinking.d.ts +15 -0
- package/dist/structured-thinking.js +46 -0
- package/dist/subagent-dispatch.d.ts +56 -0
- package/dist/subagent-dispatch.js +102 -0
- package/dist/system-employee.d.ts +4 -3
- package/dist/system-employee.js +7 -4
- package/dist/task-retention.d.ts +23 -0
- package/dist/task-retention.js +69 -0
- package/dist/task-types.d.ts +4 -0
- package/dist/task-types.js +3 -0
- package/dist/team-dispatch.d.ts +101 -0
- package/dist/team-dispatch.js +269 -0
- package/dist/tool-preview.js +7 -1
- package/dist/tui/commands.js +50 -1
- package/dist/tui/ipc-protocol.d.ts +1 -1
- package/dist/tui/ipc-server.d.ts +4 -0
- package/dist/tui/ipc-server.js +23 -0
- package/dist/turn-heartbeat.d.ts +35 -0
- package/dist/turn-heartbeat.js +64 -0
- package/dist/types.d.ts +147 -1
- package/dist/user-profile.d.ts +33 -0
- package/dist/user-profile.js +70 -0
- package/dist/web-ui/chat-message-meta.d.ts +11 -0
- package/dist/web-ui/chat-message-meta.js +47 -0
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/scripts.js +795 -95
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -6808
- package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +5 -5
- package/dist/web-ui/index.d.ts +1 -1
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/markdown.d.ts +2 -0
- package/dist/web-ui/markdown.js +111 -0
- package/dist/web-ui/page.d.ts +4 -0
- package/dist/web-ui/page.js +8 -0
- package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
- package/dist/web-ui/react/local-preview/controller.js +159 -0
- package/dist/web-ui/running-activity.d.ts +63 -0
- package/dist/web-ui/running-activity.js +134 -0
- package/dist/web-ui/styles.js +2 -11
- package/package.json +7 -2
|
@@ -264,6 +264,9 @@ export class ProviderHistoryScanner {
|
|
|
264
264
|
codexSessionsDir;
|
|
265
265
|
openCodeDatabasePath;
|
|
266
266
|
qoderProjectsDirs;
|
|
267
|
+
piSessionsDir;
|
|
268
|
+
grokSessionsDir;
|
|
269
|
+
geminiHome;
|
|
267
270
|
claudeIndex = new Map();
|
|
268
271
|
codexIndex = new Map();
|
|
269
272
|
qoderIndex = new Map();
|
|
@@ -280,6 +283,9 @@ export class ProviderHistoryScanner {
|
|
|
280
283
|
path.join(os.homedir(), ".qoder", "projects"),
|
|
281
284
|
path.join(os.homedir(), ".qoder-cn", "projects"),
|
|
282
285
|
];
|
|
286
|
+
this.piSessionsDir = options.piSessionsDir ?? path.join(os.homedir(), ".pi", "agent", "sessions");
|
|
287
|
+
this.grokSessionsDir = options.grokSessionsDir ?? path.join(os.homedir(), ".grok", "sessions");
|
|
288
|
+
this.geminiHome = options.geminiHome ?? path.join(os.homedir(), ".gemini");
|
|
283
289
|
}
|
|
284
290
|
getDiagnostics() {
|
|
285
291
|
return {
|
|
@@ -611,11 +617,25 @@ export class ProviderHistoryScanner {
|
|
|
611
617
|
let database = null;
|
|
612
618
|
try {
|
|
613
619
|
database = new DatabaseSync(this.openCodeDatabasePath);
|
|
620
|
+
// This is OpenCode's own database, shared with every live `opencode run`. Default
|
|
621
|
+
// busy_timeout is 0, so one concurrent writer turned a delete into an instant
|
|
622
|
+
// SQLITE_BUSY that we swallowed as "0 deleted".
|
|
623
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
614
624
|
database.exec("PRAGMA foreign_keys = ON");
|
|
625
|
+
// `session` has no foreign key to `event_sequence`, so deleting a session row alone
|
|
626
|
+
// orphans every event row it ever wrote (measured: 425MB across 28 deleted sessions).
|
|
627
|
+
// Removing the sequence row first cascades to `event` and actually reclaims the space.
|
|
628
|
+
// The event tables only exist on newer schemas, so their absence must not fail the delete.
|
|
629
|
+
const hasEventSequence = database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'event_sequence'").get() !== undefined;
|
|
630
|
+
const dropSequence = hasEventSequence
|
|
631
|
+
? database.prepare("DELETE FROM event_sequence WHERE aggregate_id = ?")
|
|
632
|
+
: null;
|
|
615
633
|
const remove = database.prepare("DELETE FROM session WHERE id = ?");
|
|
616
634
|
let deleted = 0;
|
|
617
|
-
for (const id of ids)
|
|
635
|
+
for (const id of ids) {
|
|
636
|
+
dropSequence?.run(id);
|
|
618
637
|
deleted += Number(remove.run(id).changes ?? 0);
|
|
638
|
+
}
|
|
619
639
|
this.invalidate("opencode");
|
|
620
640
|
return deleted;
|
|
621
641
|
}
|
|
@@ -647,4 +667,90 @@ export class ProviderHistoryScanner {
|
|
|
647
667
|
}
|
|
648
668
|
return deleted;
|
|
649
669
|
}
|
|
670
|
+
deletePiHistoryFiles(sessionIds) {
|
|
671
|
+
const ids = new Set(sessionIds.filter((id) => PROVIDER_SESSION_ID_PATTERN.test(id)));
|
|
672
|
+
if (ids.size === 0 || !existsSync(this.piSessionsDir))
|
|
673
|
+
return 0;
|
|
674
|
+
let deleted = 0;
|
|
675
|
+
try {
|
|
676
|
+
const dirs = readdirSync(this.piSessionsDir, { withFileTypes: true });
|
|
677
|
+
for (const dir of dirs) {
|
|
678
|
+
if (!dir.isDirectory())
|
|
679
|
+
continue;
|
|
680
|
+
const projectDir = path.join(this.piSessionsDir, dir.name);
|
|
681
|
+
try {
|
|
682
|
+
const files = readdirSync(projectDir, { withFileTypes: true });
|
|
683
|
+
for (const file of files) {
|
|
684
|
+
if (!file.isFile() || !file.name.endsWith(".jsonl"))
|
|
685
|
+
continue;
|
|
686
|
+
const matched = Array.from(ids).some((id) => file.name.endsWith(`_${id}.jsonl`) || file.name === `${id}.jsonl`);
|
|
687
|
+
if (matched) {
|
|
688
|
+
try {
|
|
689
|
+
unlinkSync(path.join(projectDir, file.name));
|
|
690
|
+
deleted += 1;
|
|
691
|
+
}
|
|
692
|
+
catch { /* already absent */ }
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
catch { /* ignore per-project read error */ }
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
return 0;
|
|
701
|
+
}
|
|
702
|
+
return deleted;
|
|
703
|
+
}
|
|
704
|
+
deleteGrokHistoryFiles(sessionIds) {
|
|
705
|
+
const ids = new Set(sessionIds.filter((id) => PROVIDER_SESSION_ID_PATTERN.test(id)));
|
|
706
|
+
if (ids.size === 0 || !existsSync(this.grokSessionsDir))
|
|
707
|
+
return 0;
|
|
708
|
+
let deleted = 0;
|
|
709
|
+
try {
|
|
710
|
+
const dirs = readdirSync(this.grokSessionsDir, { withFileTypes: true });
|
|
711
|
+
for (const dir of dirs) {
|
|
712
|
+
if (!dir.isDirectory())
|
|
713
|
+
continue;
|
|
714
|
+
const projectDir = path.join(this.grokSessionsDir, dir.name);
|
|
715
|
+
for (const id of ids) {
|
|
716
|
+
const targetDir = path.join(projectDir, id);
|
|
717
|
+
if (existsSync(targetDir)) {
|
|
718
|
+
try {
|
|
719
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
720
|
+
deleted += 1;
|
|
721
|
+
}
|
|
722
|
+
catch { /* best effort */ }
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
return 0;
|
|
729
|
+
}
|
|
730
|
+
return deleted;
|
|
731
|
+
}
|
|
732
|
+
deleteGeminiHistoryFiles(sessionIds) {
|
|
733
|
+
const ids = new Set(sessionIds.filter((id) => PROVIDER_SESSION_ID_PATTERN.test(id)));
|
|
734
|
+
if (ids.size === 0 || !existsSync(this.geminiHome))
|
|
735
|
+
return 0;
|
|
736
|
+
let deleted = 0;
|
|
737
|
+
for (const id of ids) {
|
|
738
|
+
const dbPath = path.join(this.geminiHome, "antigravity-cli", "conversations", `${id}.db`);
|
|
739
|
+
if (existsSync(dbPath)) {
|
|
740
|
+
try {
|
|
741
|
+
unlinkSync(dbPath);
|
|
742
|
+
deleted += 1;
|
|
743
|
+
}
|
|
744
|
+
catch { /* already absent */ }
|
|
745
|
+
}
|
|
746
|
+
const lockPath = path.join(this.geminiHome, "antigravity-cli", "presence", `${id}.lock`);
|
|
747
|
+
if (existsSync(lockPath)) {
|
|
748
|
+
try {
|
|
749
|
+
unlinkSync(lockPath);
|
|
750
|
+
}
|
|
751
|
+
catch { /* best effort */ }
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return deleted;
|
|
755
|
+
}
|
|
650
756
|
}
|
package/dist/retention.d.ts
CHANGED
|
@@ -1,29 +1,41 @@
|
|
|
1
1
|
import type { SessionRegistry } from "./session-registry.js";
|
|
2
2
|
import type { WandStorage } from "./storage.js";
|
|
3
|
+
import { type TaskRetentionSettings } from "./task-retention.js";
|
|
4
|
+
import type { SessionSnapshot } from "./types.js";
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 会话的自动归档此前散在两个 runner 里(24h),这里收敛成同一份常量,避免
|
|
9
|
-
* 「会话 24h、任务 7 天」两套口径。运行中的会话/任务永不归档或清理。
|
|
6
|
+
* 看板任务、会话和已结束的团队运行共用 `taskRetention`。
|
|
7
|
+
* 没配过时默认空闲 7 天归档,再过 7 天清理。
|
|
8
|
+
* 结构化会话看运行状态;PTY 只有正在回复或等待确认才跳过,空闲壳不算在处理。
|
|
10
9
|
*/
|
|
11
10
|
export declare const RETENTION_IDLE_MS: number;
|
|
12
11
|
export declare const RETENTION_ARCHIVED_MS: number;
|
|
13
12
|
export interface RetentionDeps {
|
|
14
13
|
storage: WandStorage;
|
|
15
14
|
sessions: SessionRegistry;
|
|
15
|
+
/** 每次扫描读取。传入函数才能在设置保存后立刻用新窗口,而不是启动时的快照。 */
|
|
16
|
+
taskRetention?: TaskRetentionSettings | (() => TaskRetentionSettings | undefined);
|
|
16
17
|
}
|
|
17
18
|
export interface RetentionResult {
|
|
18
19
|
archivedSessions: number;
|
|
19
20
|
purgedSessions: number;
|
|
20
21
|
archivedTasks: number;
|
|
21
22
|
purgedTasks: number;
|
|
23
|
+
purgedTeamRuns: number;
|
|
22
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* 保留期里的「还在处理」:结构化会话看运行状态;PTY 的 `running` 只表示壳进程还在,
|
|
27
|
+
* 空闲 zsh 不算。真正挡住清理的是正在回复、或停在权限/确认上。
|
|
28
|
+
*/
|
|
29
|
+
export declare function isRetentionBusy(session: SessionSnapshot): boolean;
|
|
23
30
|
/**
|
|
24
31
|
* 跑一遍保留期清理。返回本轮的实际动作计数,便于日志与测试断言。
|
|
25
32
|
* 纯函数式编排:只依赖 storage / registry,方便在单测里注入固定时间。
|
|
26
33
|
*/
|
|
27
34
|
export declare function runRetentionSweep(deps: RetentionDeps, now?: number): RetentionResult;
|
|
35
|
+
/**
|
|
36
|
+
* 可重复调用的扫描。同一时刻只跑一轮;进行中又被请求时,本轮结束后再跑一轮,
|
|
37
|
+
* 这样保存设置时不会和定时扫描互相覆盖。
|
|
38
|
+
*/
|
|
39
|
+
export declare function createRetentionSweep(deps: RetentionDeps): () => RetentionResult;
|
|
28
40
|
/** 每小时的保留期扫描;返回清理定时器,调用方负责 clearInterval。 */
|
|
29
|
-
export declare function startRetentionTimer(deps: RetentionDeps, intervalMs?: number): NodeJS.Timeout;
|
|
41
|
+
export declare function startRetentionTimer(deps: RetentionDeps, intervalMs?: number, sweep?: () => RetentionResult): NodeJS.Timeout;
|
package/dist/retention.js
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
import { cleanupWorktreeSync } from "./git-worktree.js";
|
|
2
|
+
import { taskRetentionWindows } from "./task-retention.js";
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 会话的自动归档此前散在两个 runner 里(24h),这里收敛成同一份常量,避免
|
|
8
|
-
* 「会话 24h、任务 7 天」两套口径。运行中的会话/任务永不归档或清理。
|
|
4
|
+
* 看板任务、会话和已结束的团队运行共用 `taskRetention`。
|
|
5
|
+
* 没配过时默认空闲 7 天归档,再过 7 天清理。
|
|
6
|
+
* 结构化会话看运行状态;PTY 只有正在回复或等待确认才跳过,空闲壳不算在处理。
|
|
9
7
|
*/
|
|
10
8
|
export const RETENTION_IDLE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
9
|
export const RETENTION_ARCHIVED_MS = 7 * 24 * 60 * 60 * 1000;
|
|
10
|
+
/**
|
|
11
|
+
* 保留期里的「还在处理」:结构化会话看运行状态;PTY 的 `running` 只表示壳进程还在,
|
|
12
|
+
* 空闲 zsh 不算。真正挡住清理的是正在回复、或停在权限/确认上。
|
|
13
|
+
*/
|
|
14
|
+
export function isRetentionBusy(session) {
|
|
15
|
+
if (session.ptyBusy === true || session.structuredState?.inFlight === true)
|
|
16
|
+
return true;
|
|
17
|
+
if (session.permissionBlocked || session.pendingEscalation)
|
|
18
|
+
return true;
|
|
19
|
+
if ((session.sessionKind ?? "pty") === "pty" || session.runner === "pty")
|
|
20
|
+
return false;
|
|
21
|
+
return session.status === "running";
|
|
22
|
+
}
|
|
23
|
+
function isIdleLivePty(session) {
|
|
24
|
+
return session.status === "running" && !isRetentionBusy(session)
|
|
25
|
+
&& ((session.sessionKind ?? "pty") === "pty" || session.runner === "pty");
|
|
26
|
+
}
|
|
12
27
|
function timestamp(value) {
|
|
13
28
|
if (!value)
|
|
14
29
|
return null;
|
|
@@ -40,23 +55,28 @@ function taskLastActivity(storage, task, bound) {
|
|
|
40
55
|
* 纯函数式编排:只依赖 storage / registry,方便在单测里注入固定时间。
|
|
41
56
|
*/
|
|
42
57
|
export function runRetentionSweep(deps, now = Date.now()) {
|
|
43
|
-
const result = {
|
|
58
|
+
const result = {
|
|
59
|
+
archivedSessions: 0, purgedSessions: 0, archivedTasks: 0, purgedTasks: 0, purgedTeamRuns: 0,
|
|
60
|
+
};
|
|
61
|
+
const taskWindows = taskRetentionWindows(typeof deps.taskRetention === "function" ? deps.taskRetention() : deps.taskRetention);
|
|
44
62
|
const snapshots = deps.sessions.listSlim();
|
|
45
63
|
for (const session of snapshots) {
|
|
46
|
-
//
|
|
47
|
-
if (session
|
|
64
|
+
// 真正还在处理的会话不能归档或杀掉。空闲 PTY 壳不算。
|
|
65
|
+
if (isRetentionBusy(session))
|
|
48
66
|
continue;
|
|
49
67
|
if (session.archived) {
|
|
68
|
+
if (taskWindows.archivedMs === null)
|
|
69
|
+
continue;
|
|
50
70
|
const archivedAt = timestamp(session.archivedAt);
|
|
51
71
|
if (archivedAt === null) {
|
|
52
|
-
//
|
|
72
|
+
// 历史归档行没有时间戳:先补上,让删除窗口从这一刻起算,而不是立刻删除。
|
|
53
73
|
try {
|
|
54
74
|
deps.sessions.setArchived(session.id, true);
|
|
55
75
|
}
|
|
56
76
|
catch { /* already gone */ }
|
|
57
77
|
continue;
|
|
58
78
|
}
|
|
59
|
-
if (now - archivedAt <
|
|
79
|
+
if (now - archivedAt < taskWindows.archivedMs)
|
|
60
80
|
continue;
|
|
61
81
|
try {
|
|
62
82
|
deps.sessions.delete(session.id);
|
|
@@ -65,9 +85,17 @@ export function runRetentionSweep(deps, now = Date.now()) {
|
|
|
65
85
|
catch { /* best-effort: a session may disappear between the read and the purge */ }
|
|
66
86
|
continue;
|
|
67
87
|
}
|
|
88
|
+
if (taskWindows.idleMs === null)
|
|
89
|
+
continue;
|
|
68
90
|
const reference = timestamp(session.endedAt ?? session.startedAt);
|
|
69
|
-
if (reference === null || now - reference <
|
|
91
|
+
if (reference === null || now - reference < taskWindows.idleMs)
|
|
70
92
|
continue;
|
|
93
|
+
if (isIdleLivePty(session)) {
|
|
94
|
+
try {
|
|
95
|
+
deps.sessions.stop(session.id);
|
|
96
|
+
}
|
|
97
|
+
catch { /* shell may already be gone */ }
|
|
98
|
+
}
|
|
71
99
|
try {
|
|
72
100
|
deps.sessions.setArchived(session.id, true);
|
|
73
101
|
result.archivedSessions += 1;
|
|
@@ -87,9 +115,9 @@ export function runRetentionSweep(deps, now = Date.now()) {
|
|
|
87
115
|
}
|
|
88
116
|
for (const task of deps.storage.listWandTasks()) {
|
|
89
117
|
const bound = task.workspaceTaskId ? sessionsByTask.get(task.workspaceTaskId) ?? [] : [];
|
|
90
|
-
const running = bound.some((session) => session
|
|
118
|
+
const running = bound.some((session) => isRetentionBusy(session));
|
|
91
119
|
if (task.status === "archived") {
|
|
92
|
-
if (running)
|
|
120
|
+
if (running || taskWindows.archivedMs === null)
|
|
93
121
|
continue;
|
|
94
122
|
const archivedAt = timestamp(task.archivedAt);
|
|
95
123
|
if (archivedAt === null) {
|
|
@@ -97,7 +125,7 @@ export function runRetentionSweep(deps, now = Date.now()) {
|
|
|
97
125
|
deps.storage.updateWandTask(task.id, { status: "archived" });
|
|
98
126
|
continue;
|
|
99
127
|
}
|
|
100
|
-
if (now - archivedAt <
|
|
128
|
+
if (now - archivedAt < taskWindows.archivedMs)
|
|
101
129
|
continue;
|
|
102
130
|
const container = task.workspaceTaskId ? deps.storage.getWorkspaceTask(task.workspaceTaskId) : null;
|
|
103
131
|
if (container?.worktree) {
|
|
@@ -110,33 +138,148 @@ export function runRetentionSweep(deps, now = Date.now()) {
|
|
|
110
138
|
result.purgedTasks += 1;
|
|
111
139
|
continue;
|
|
112
140
|
}
|
|
113
|
-
if (running)
|
|
141
|
+
if (running || taskWindows.idleMs === null)
|
|
114
142
|
continue;
|
|
115
143
|
const lastActivity = taskLastActivity(deps.storage, task, bound);
|
|
116
|
-
if (lastActivity === null || now - lastActivity <
|
|
144
|
+
if (lastActivity === null || now - lastActivity < taskWindows.idleMs)
|
|
117
145
|
continue;
|
|
118
146
|
deps.storage.updateWandTask(task.id, { status: "archived" });
|
|
119
147
|
result.archivedTasks += 1;
|
|
120
148
|
}
|
|
149
|
+
sweepTeamRuns(deps, taskWindows, snapshots, now, result);
|
|
121
150
|
return result;
|
|
122
151
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
152
|
+
function sweepTeamRuns(deps, windows, snapshots, now, result) {
|
|
153
|
+
const sessionById = new Map(snapshots.map((session) => [session.id, session]));
|
|
154
|
+
for (const run of deps.storage.listAiTeamRuns()) {
|
|
155
|
+
if (run.status === "running")
|
|
156
|
+
continue;
|
|
157
|
+
const linked = teamRunSessionIds(deps.storage, run.id, run.chatSessionId);
|
|
158
|
+
if (linked.some((id) => {
|
|
159
|
+
const session = sessionById.get(id);
|
|
160
|
+
return session ? isRetentionBusy(session) : false;
|
|
161
|
+
}))
|
|
162
|
+
continue;
|
|
163
|
+
const activity = teamRunActivity(run.updatedAt, linked, sessionById);
|
|
164
|
+
if (activity === null)
|
|
165
|
+
continue;
|
|
166
|
+
if (windows.archivedMs !== null && now - activity >= windows.archivedMs) {
|
|
167
|
+
for (const id of linked) {
|
|
168
|
+
const session = sessionById.get(id);
|
|
169
|
+
if (!session || isRetentionBusy(session))
|
|
170
|
+
continue;
|
|
171
|
+
if (isIdleLivePty(session)) {
|
|
172
|
+
try {
|
|
173
|
+
deps.sessions.stop(id);
|
|
174
|
+
}
|
|
175
|
+
catch { /* already gone */ }
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
deps.sessions.delete(id);
|
|
179
|
+
result.purgedSessions += 1;
|
|
180
|
+
sessionById.delete(id);
|
|
181
|
+
}
|
|
182
|
+
catch { /* already gone */ }
|
|
132
183
|
}
|
|
184
|
+
deps.storage.deleteAiTeamRun(run.id);
|
|
185
|
+
result.purgedTeamRuns += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (windows.idleMs === null || now - activity < windows.idleMs)
|
|
189
|
+
continue;
|
|
190
|
+
for (const id of linked) {
|
|
191
|
+
const session = sessionById.get(id);
|
|
192
|
+
if (!session || session.archived || isRetentionBusy(session))
|
|
193
|
+
continue;
|
|
194
|
+
if (isIdleLivePty(session)) {
|
|
195
|
+
try {
|
|
196
|
+
deps.sessions.stop(id);
|
|
197
|
+
}
|
|
198
|
+
catch { /* already gone */ }
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
deps.sessions.setArchived(id, true);
|
|
202
|
+
session.archived = true;
|
|
203
|
+
result.archivedSessions += 1;
|
|
204
|
+
}
|
|
205
|
+
catch { /* best-effort */ }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function teamRunSessionIds(storage, runId, chatSessionId) {
|
|
210
|
+
const ids = new Set();
|
|
211
|
+
if (chatSessionId)
|
|
212
|
+
ids.add(chatSessionId);
|
|
213
|
+
for (const step of storage.listAiTeamSteps(runId)) {
|
|
214
|
+
if (step.sessionId)
|
|
215
|
+
ids.add(step.sessionId);
|
|
216
|
+
}
|
|
217
|
+
return [...ids];
|
|
218
|
+
}
|
|
219
|
+
function teamRunActivity(updatedAt, sessionIds, sessionById) {
|
|
220
|
+
const times = [];
|
|
221
|
+
const updated = timestamp(updatedAt);
|
|
222
|
+
if (updated !== null)
|
|
223
|
+
times.push(updated);
|
|
224
|
+
for (const id of sessionIds) {
|
|
225
|
+
const session = sessionById.get(id);
|
|
226
|
+
const reference = timestamp(session?.endedAt ?? session?.startedAt);
|
|
227
|
+
if (reference !== null)
|
|
228
|
+
times.push(reference);
|
|
229
|
+
}
|
|
230
|
+
return times.length > 0 ? Math.max(...times) : null;
|
|
231
|
+
}
|
|
232
|
+
function logRetentionSweep(result) {
|
|
233
|
+
const acted = result.archivedSessions + result.purgedSessions + result.archivedTasks + result.purgedTasks + result.purgedTeamRuns;
|
|
234
|
+
if (acted > 0) {
|
|
235
|
+
console.log(`[retention] archived sessions=${result.archivedSessions} tasks=${result.archivedTasks}; `
|
|
236
|
+
+ `purged sessions=${result.purgedSessions} tasks=${result.purgedTasks} teamRuns=${result.purgedTeamRuns}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* 可重复调用的扫描。同一时刻只跑一轮;进行中又被请求时,本轮结束后再跑一轮,
|
|
241
|
+
* 这样保存设置时不会和定时扫描互相覆盖。
|
|
242
|
+
*/
|
|
243
|
+
export function createRetentionSweep(deps) {
|
|
244
|
+
const empty = {
|
|
245
|
+
archivedSessions: 0, purgedSessions: 0, archivedTasks: 0, purgedTasks: 0, purgedTeamRuns: 0,
|
|
246
|
+
};
|
|
247
|
+
let running = false;
|
|
248
|
+
let again = false;
|
|
249
|
+
let last = empty;
|
|
250
|
+
return () => {
|
|
251
|
+
if (running) {
|
|
252
|
+
again = true;
|
|
253
|
+
return last;
|
|
254
|
+
}
|
|
255
|
+
running = true;
|
|
256
|
+
try {
|
|
257
|
+
do {
|
|
258
|
+
again = false;
|
|
259
|
+
last = runRetentionSweep(deps);
|
|
260
|
+
logRetentionSweep(last);
|
|
261
|
+
} while (again);
|
|
262
|
+
return last;
|
|
133
263
|
}
|
|
134
264
|
catch (error) {
|
|
135
265
|
console.error(`[retention] sweep failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
running = false;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/** 每小时的保留期扫描;返回清理定时器,调用方负责 clearInterval。 */
|
|
274
|
+
export function startRetentionTimer(deps, intervalMs = 60 * 60 * 1000, sweep = createRetentionSweep(deps)) {
|
|
275
|
+
const run = () => {
|
|
276
|
+
try {
|
|
277
|
+
sweep();
|
|
136
278
|
}
|
|
279
|
+
catch { /* createRetentionSweep 已记录 */ }
|
|
137
280
|
};
|
|
138
|
-
|
|
139
|
-
const timer = setInterval(
|
|
281
|
+
run();
|
|
282
|
+
const timer = setInterval(run, intervalMs);
|
|
140
283
|
timer.unref?.();
|
|
141
284
|
return timer;
|
|
142
285
|
}
|
|
@@ -290,10 +290,14 @@ export function registerAiTeamRoutes(app, deps) {
|
|
|
290
290
|
sendTeamError(res, error);
|
|
291
291
|
}
|
|
292
292
|
});
|
|
293
|
-
/**
|
|
293
|
+
/**
|
|
294
|
+
* 运行中步骤的 live 文本(§4.9.1):Web 走 ai-team-step-live 推送,移动端轻量轮询这个端点。
|
|
295
|
+
* 除步骤快照外还回 run 级活动时间(startedAt / lastActivityAt / observedAt),轮询端不必
|
|
296
|
+
* 再拉一次 detail 才知道「还在跑、只是这一轮安静」。
|
|
297
|
+
*/
|
|
294
298
|
app.get("/api/ai-team-runs/:id/live", (req, res) => {
|
|
295
299
|
try {
|
|
296
|
-
res.json(
|
|
300
|
+
res.json(runner.liveSnapshot(req.params.id));
|
|
297
301
|
}
|
|
298
302
|
catch (error) {
|
|
299
303
|
sendTeamError(res, error);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import type { ConversationService } from "./conversation-service.js";
|
|
3
|
+
export declare function registerConversationRoutes(app: Express, service: ConversationService): void;
|
|
4
|
+
/** Compatibility relay inputs are ordinary talk; never infer the newest task or approve a short phrase. */
|
|
5
|
+
export declare function forwardConversationRelay(service: ConversationService, sessionId: string, input: string): Promise<void>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { AiTeamConflictError } from "./ai-team-runner.js";
|
|
3
|
+
import { asyncRoute } from "./express-async.js";
|
|
4
|
+
import { bodyObject, sendRouteError, text } from "./server-request.js";
|
|
5
|
+
function targetOf(value) {
|
|
6
|
+
if (value === null || value === undefined)
|
|
7
|
+
return null;
|
|
8
|
+
const body = bodyObject(value);
|
|
9
|
+
if (!text(body.taskId) || !text(body.runId))
|
|
10
|
+
throw new Error("发送目标需要明确 taskId 和 runId。");
|
|
11
|
+
return { taskId: text(body.taskId), runId: text(body.runId) };
|
|
12
|
+
}
|
|
13
|
+
function result(res, receipt) {
|
|
14
|
+
if (receipt.state === "rejected")
|
|
15
|
+
res.status(400).json({ error: receipt.error ?? "请求未接受。", receipt });
|
|
16
|
+
else
|
|
17
|
+
res.status(202).json(receipt);
|
|
18
|
+
}
|
|
19
|
+
export function registerConversationRoutes(app, service) {
|
|
20
|
+
app.get("/api/conversations", (req, res) => res.json({ conversations: service.list() }));
|
|
21
|
+
app.patch("/api/conversations/:id/list-state", (req, res) => {
|
|
22
|
+
try {
|
|
23
|
+
const body = bodyObject(req.body);
|
|
24
|
+
if ((body.pinned !== undefined && typeof body.pinned !== "boolean") || (body.dissolved !== undefined && typeof body.dissolved !== "boolean")
|
|
25
|
+
|| (body.pinned === undefined && body.dissolved === undefined))
|
|
26
|
+
throw new Error("请提供有效的置顶或列表显示状态。");
|
|
27
|
+
res.json(service.updateListState(req.params.id, { pinned: body.pinned, dissolved: body.dissolved }));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
sendRouteError(res, error, "对话列表更新失败。", 400);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
app.delete("/api/conversations/:id", asyncRoute(async (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
await service.remove(req.params.id);
|
|
36
|
+
res.json({ ok: true });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
sendRouteError(res, error, "删除对话失败。", error instanceof AiTeamConflictError ? 409 : 400);
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
42
|
+
app.get("/api/conversations/requests/:requestId", (req, res) => {
|
|
43
|
+
const receipt = service.receipt(req.params.requestId);
|
|
44
|
+
if (!receipt)
|
|
45
|
+
res.status(404).json({ error: "未找到回执;不能据此断言请求未接受。" });
|
|
46
|
+
else
|
|
47
|
+
res.json(receipt);
|
|
48
|
+
});
|
|
49
|
+
app.get("/api/conversations/:id", (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
res.json(service.detail(req.params.id));
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
sendRouteError(res, error, "对话读取失败。", 404);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const post = (path, operation) => {
|
|
58
|
+
app.post(path, asyncRoute(async (req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
result(res, await operation(req.params.id ?? "", bodyObject(req.body)));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
sendRouteError(res, error, "对话操作失败。", error instanceof AiTeamConflictError ? 409 : 400);
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
};
|
|
67
|
+
post("/api/conversations", (_id, body) => service.createGroup(text(body.requestId), bodyObject(body.group)));
|
|
68
|
+
post("/api/conversations/:id/invitations", (id, body) => service.invite(id, text(body.requestId), Number(body.memberVersion), bodyObject(body.group)));
|
|
69
|
+
post("/api/conversations/:id/channel", (id, body) => service.prepare(id, text(body.requestId), text(body.cwd) || undefined));
|
|
70
|
+
post("/api/conversations/:id/messages", (id, body) => service.send(id, text(body.requestId), text(body.input), targetOf(body.target), text(body.cwd) || undefined));
|
|
71
|
+
post("/api/conversations/:id/tasks", (id, body) => service.dispatch(id, text(body.requestId), {
|
|
72
|
+
title: text(body.title), description: text(body.description), workspaceId: text(body.workspaceId), cwd: text(body.cwd) || undefined,
|
|
73
|
+
memberVersion: body.memberVersion === undefined ? undefined : Number(body.memberVersion),
|
|
74
|
+
continueTaskId: text(body.continueTaskId) || undefined,
|
|
75
|
+
}));
|
|
76
|
+
post("/api/conversations/:id/actions", (id, body) => {
|
|
77
|
+
const target = targetOf(body.target);
|
|
78
|
+
if (!target)
|
|
79
|
+
throw new Error("运行操作需要明确任务与轮次。");
|
|
80
|
+
return service.act(id, text(body.requestId), target, text(body.action), body.extraSteps === undefined ? undefined : Number(body.extraSteps));
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Compatibility relay inputs are ordinary talk; never infer the newest task or approve a short phrase. */
|
|
84
|
+
export async function forwardConversationRelay(service, sessionId, input) {
|
|
85
|
+
const group = service.list().find((item) => item.sessionId === sessionId);
|
|
86
|
+
if (!group)
|
|
87
|
+
throw new Error("群实例不存在,请从对话列表核对。");
|
|
88
|
+
const receipt = await service.send(group.id, randomUUID(), input, null, undefined, false);
|
|
89
|
+
if (receipt.error)
|
|
90
|
+
throw new Error(receipt.error);
|
|
91
|
+
}
|
|
@@ -19,4 +19,6 @@ export declare function registerSiliconEmployeeRoutes(app: Express, deps: {
|
|
|
19
19
|
config?: WandConfig;
|
|
20
20
|
/** 测试注入点:默认按系统 AI 配置真实调用模型。 */
|
|
21
21
|
generateDraft?: (request: EmployeeDraftRequest) => Promise<SiliconEmployeeDraft>;
|
|
22
|
+
/** 测试注入点:CLI 可用性探测。 */
|
|
23
|
+
isProviderAvailable?: (agent: WandTaskAgent) => boolean;
|
|
22
24
|
}): void;
|