kimi-plugin-cc 2.4.0 → 2.4.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +9 -0
- package/README.md +14 -6
- package/dist/cli-client.js +2 -2
- package/dist/commands/cli-helpers.js +5 -1
- package/dist/commands/swarm.js +191 -34
- package/dist/hooks/config-safety.js +4 -3
- package/dist/kimi-engine.js +1 -0
- package/dist/process-tree.js +48 -17
- package/dist/swarm-activation.js +812 -0
- package/dist/version.js +1 -1
- package/docs/migration.md +12 -2
- package/docs/registry-releases.md +6 -2
- package/docs/safety.md +17 -5
- package/package.json +2 -2
- package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/kimi-codex/dist/cli-client.js +2 -2
- package/plugins/kimi-codex/dist/commands/cli-helpers.js +5 -1
- package/plugins/kimi-codex/dist/commands/swarm.js +191 -34
- package/plugins/kimi-codex/dist/hooks/config-safety.js +4 -3
- package/plugins/kimi-codex/dist/kimi-engine.js +1 -0
- package/plugins/kimi-codex/dist/process-tree.js +48 -17
- package/plugins/kimi-codex/dist/swarm-activation.js +812 -0
- package/plugins/kimi-codex/dist/version.js +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimi",
|
|
3
3
|
"description": "Kimi for Claude Code: code questions, independent reviews, bounded fixes, autonomous goals, parallel tasks, and MCP tasks through the local Kimi Code CLI, with PreToolUse safety hooks.",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.1",
|
|
5
5
|
"homepage": "https://github.com/linxule/kimi-plugin-cc",
|
|
6
6
|
"repository": "https://github.com/linxule/kimi-plugin-cc",
|
|
7
7
|
"license": "Apache-2.0",
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
> **Post-1.0 release history (v1.0.1 -> present) lives in [ROADMAP-TO-GA.md § Post-GA audit log](./ROADMAP-TO-GA.md#post-ga-audit-log)** and the "Version" / "Upstream compat" lines of [AGENTS.md](./AGENTS.md). Docs-only kimi-code compat checkups that don't bump the plugin version (e.g. the 0.14.2 / 0.14.3 patches) are recorded there, not here. Notable releases are summarized below; the GA entry and full pre-GA detail follow.
|
|
4
4
|
|
|
5
|
+
## 2.4.1 — 2026-10-25
|
|
6
|
+
|
|
7
|
+
- Certify exact kimi-code 2.3.2 for all eight operations using the 15-test/92-assertion source scan, plan-ON/OFF controls, and the final 19-test/125-assertion production smoke. Keep the no-plan construction and legacy-v1 boundary at 0.41.x. See [evidence](docs/upstream-2.3.2-certification.md).
|
|
8
|
+
- Preserve swarm support after upstream removed model self-activation: exact-version, authenticated loopback activation creates a same-job session with the selected model, auto permission, and plan mode off, then resumes through ordinary `kimi -p`. `runSwarm` solely owns bounded deactivation and verifies the journal; older certified versions stay on direct print.
|
|
9
|
+
- Bind activation to exact provenance, finite budgets, process identity, and session identity (`preactivated`). Failure recovery is cancellation-aware and preserves primary print errors. Unknown late-fork or changed-PGID survivors remain unverified, never false quiescence or unproven signal targets. Cleanup failure blocks completion and retains raw output, structured diagnostics, and the write worktree. MCP uses the same implementation and existing task/job ownership.
|
|
10
|
+
- Close a certification blind spot found by adversarial review: AgentSwarm call records, aggregate denial text, and coordinator-only patches no longer count as fan-out. Smoke requires distinct child journals and correlates each assigned Write/Edit path and toolCallId with its own denial or successful result; the final production run passed 19/19 with 125 assertions in 296.67 seconds.
|
|
11
|
+
|
|
12
|
+
Contributors: **Kimi** (source audit, design, implementation, documentation, and verification), **Claude** and **Codex** (adversarial review and re-review).
|
|
13
|
+
|
|
5
14
|
## 2.4.0 — 2026-10-24
|
|
6
15
|
|
|
7
16
|
- Add one dependency-free MCP stdio server for Claude Code and Codex, with typed schemas for seven model operations plus job status, result, and cancellation. The existing slash commands, skills, safety hooks, and review gate remain intact.
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ You need:
|
|
|
40
40
|
- macOS or Linux
|
|
41
41
|
- Claude Code or Codex with plugin support
|
|
42
42
|
- Node.js 22.5 or newer
|
|
43
|
-
- Kimi Code CLI 2.3.
|
|
43
|
+
- Kimi Code CLI 2.3.2 (certified; 2.3.1 and 2.3.0 are also certified)
|
|
44
44
|
|
|
45
45
|
Windows is not currently supported. Check your versions in a terminal:
|
|
46
46
|
|
|
@@ -100,12 +100,12 @@ If you use both Claude Code and Codex, run setup in each. Each app has its own h
|
|
|
100
100
|
## npm distribution (optional)
|
|
101
101
|
|
|
102
102
|
The existing marketplace commands above remain the default for Claude Code and
|
|
103
|
-
Codex.
|
|
103
|
+
Codex. Published releases are also available as
|
|
104
104
|
[`kimi-plugin-cc` on npm](https://www.npmjs.com/package/kimi-plugin-cc), including
|
|
105
105
|
both compiled plugin distributions. No Bun, TypeScript, or install scripts are
|
|
106
106
|
needed to use the package. Kimi Code CLI and Node are still prerequisites.
|
|
107
107
|
|
|
108
|
-
For an explicitly versioned local Claude Code plugin:
|
|
108
|
+
For an explicitly versioned local Claude Code plugin, this example pins the published 2.4.0 package; 2.4.1 is not yet published:
|
|
109
109
|
|
|
110
110
|
```sh
|
|
111
111
|
npm install --prefix ~/.local/share/kimi-plugin-cc kimi-plugin-cc@2.4.0
|
|
@@ -228,6 +228,8 @@ All refusal codes and their remedies are listed in the [refusal-code catalog](./
|
|
|
228
228
|
|
|
229
229
|
The tool rules for ask, review, challenge, and read-only swarm deny file writes and shell commands. Rescue and pursue can write files in your workspace. Swarm with `--write` works in a temporary Git worktree. It starts from your latest commit, so it does not include uncommitted changes. It returns a patch without applying it to your project.
|
|
230
230
|
|
|
231
|
+
For swarm on CLI 2.3.2, the plugin briefly starts an authenticated loopback `kimi web` process to activate a session and another to deactivate swarm mode afterward; one ordinary `kimi -p -r` process still performs the task. Activation counts against the time limit, the access token stays in memory and out of logs, and cleanup failure prevents success. Older certified versions use `kimi -p` directly.
|
|
232
|
+
|
|
231
233
|
The plugin does not commit changes. Review changes before you keep or apply them.
|
|
232
234
|
|
|
233
235
|
Only use tasks that can write files in repositories you trust. Rescue, pursue, and swarm with `--write` can run tests and similar commands. These commands execute code from your repository. The hook restricts tool use; it is not an operating-system sandbox.
|
|
@@ -262,7 +264,7 @@ Contributors need Bun to run `bun run check`. The detailed engineering documents
|
|
|
262
264
|
- macOS 或 Linux
|
|
263
265
|
- 支持插件的 Claude Code 或 Codex
|
|
264
266
|
- Node.js 22.5 或更高版本
|
|
265
|
-
- Kimi Code CLI 2.3.
|
|
267
|
+
- Kimi Code CLI 2.3.2(已认证;2.3.1 和 2.3.0 也已认证)
|
|
266
268
|
|
|
267
269
|
目前不支持 Windows。在终端检查版本:
|
|
268
270
|
|
|
@@ -425,6 +427,8 @@ codex plugin add kimi@kimi-marketplace
|
|
|
425
427
|
|
|
426
428
|
Ask、review、challenge 和只读 swarm 的工具规则禁止写入文件和执行 shell 命令。Rescue 和 pursue 可以修改工作区内的文件。带 `--write` 的 swarm 在临时 Git 工作树中操作。它从最新提交开始,不包含尚未提交的修改。任务结束后返回补丁,不会自动应用到项目。
|
|
427
429
|
|
|
430
|
+
在 CLI 2.3.2 上运行 swarm 时,插件会短暂启动需要认证的本地回环 `kimi web` 进程来激活会话,并在结束后再次启动以停用 swarm 模式;任务本身仍由一个普通的 `kimi -p -r` 进程执行。激活计入任务时限,访问令牌只保存在内存且不会写入日志;无法确认清理完成时,任务不会报告成功。较早的已认证版本仍直接通过 `kimi -p` 运行。
|
|
431
|
+
|
|
428
432
|
插件不会提交修改。保留或应用修改前,请先审查。
|
|
429
433
|
|
|
430
434
|
只在你信任的仓库中运行能修改文件的任务。Rescue、pursue 和带 `--write` 的 swarm 可以运行测试等命令。这些命令会执行仓库中的代码。钩子限制的是工具调用,它不是操作系统沙箱。
|
|
@@ -459,7 +463,7 @@ Il vous faut :
|
|
|
459
463
|
- macOS ou Linux
|
|
460
464
|
- une version de Claude Code ou de Codex prenant en charge les plugins
|
|
461
465
|
- Node.js 22.5 ou une version ultérieure
|
|
462
|
-
- Kimi Code CLI 2.3.
|
|
466
|
+
- Kimi Code CLI 2.3.2 (certifiée ; les versions 2.3.1 et 2.3.0 sont également certifiées)
|
|
463
467
|
|
|
464
468
|
Windows n'est pas pris en charge actuellement. Vérifiez les versions dans un terminal :
|
|
465
469
|
|
|
@@ -622,6 +626,8 @@ Tous les codes de refus et leurs remèdes sont répertoriés dans le [catalogue
|
|
|
622
626
|
|
|
623
627
|
Les règles des modes ask, review, challenge et swarm en lecture seule interdisent les écritures et les commandes shell. Rescue et pursue peuvent modifier les fichiers de votre espace de travail. Swarm avec `--write` travaille dans un worktree Git temporaire. Il part du dernier commit et n'inclut donc pas les modifications non commitées. Il renvoie un patch sans l'appliquer à votre projet.
|
|
624
628
|
|
|
629
|
+
Avec le CLI 2.3.2, les tâches swarm lancent brièvement un processus `kimi web` authentifié sur l'interface de boucle locale pour activer la session, puis un second pour désactiver le mode swarm ; un seul processus `kimi -p -r` ordinaire exécute toujours la tâche. L'activation est comptée dans la limite de temps, le jeton reste en mémoire et hors des journaux, et l'échec du nettoyage empêche de déclarer la réussite. Les versions certifiées antérieures utilisent directement `kimi -p`.
|
|
630
|
+
|
|
625
631
|
Le plugin ne crée aucun commit. Examinez les modifications avant de les conserver ou de les appliquer.
|
|
626
632
|
|
|
627
633
|
N'utilisez les tâches pouvant écrire des fichiers que dans des dépôts auxquels vous faites confiance. Rescue, pursue et swarm avec `--write` peuvent lancer des tests et des commandes similaires. Ces commandes exécutent du code du dépôt. Le hook limite l'utilisation des outils ; ce n'est pas un bac à sable du système d'exploitation.
|
|
@@ -656,7 +662,7 @@ Les contributeurs ont besoin de Bun pour exécuter `bun run check`. La documenta
|
|
|
656
662
|
- macOS または Linux
|
|
657
663
|
- プラグインに対応した Claude Code または Codex
|
|
658
664
|
- Node.js 22.5 以降
|
|
659
|
-
- Kimi Code CLI 2.3.
|
|
665
|
+
- Kimi Code CLI 2.3.2(認証済み。2.3.1 と 2.3.0 も認証済み)
|
|
660
666
|
|
|
661
667
|
現在、Windows には対応していません。ターミナルでバージョンを確認します。
|
|
662
668
|
|
|
@@ -819,6 +825,8 @@ codex plugin add kimi@kimi-marketplace
|
|
|
819
825
|
|
|
820
826
|
Ask、review、challenge、読み取り専用 swarm のツールルールでは、ファイルへの書き込みとシェルコマンドを禁止しています。Rescue と pursue はワークスペース内のファイルを変更できます。`--write` を指定した swarm は、一時的な Git ワークツリーで作業します。最新のコミットから開始するため、未コミットの変更は含まれません。パッチを返すだけで、プロジェクトには自動適用しません。
|
|
821
827
|
|
|
828
|
+
CLI 2.3.2 の swarm では、セッションを有効化するため、そして終了後に swarm モードを無効化するため、認証付きのローカルループバック `kimi web` プロセスをそれぞれ短時間だけ起動します。タスク自体は従来どおり 1 つの `kimi -p -r` プロセスが実行します。有効化も制限時間に含まれ、トークンはメモリー内にのみ保持されてログには出ません。後処理を確認できない場合、タスクは成功と報告されません。以前の認証済みバージョンでは、引き続き `kimi -p` を直接使います。
|
|
829
|
+
|
|
822
830
|
プラグインは変更をコミットしません。変更を残す、または適用する前に内容を確認してください。
|
|
823
831
|
|
|
824
832
|
ファイルを書き換えるタスクは、信頼できるリポジトリでのみ実行してください。Rescue、pursue、`--write` 付きの swarm は、テストなどのコマンドを実行できます。これらのコマンドはリポジトリ内のコードを実行します。フックはツールの使用を制限しますが、OS のサンドボックスではありません。
|
package/dist/cli-client.js
CHANGED
|
@@ -60,7 +60,7 @@ export async function runCliPrompt(opts) {
|
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
const args = buildArgs(opts);
|
|
63
|
-
const env =
|
|
63
|
+
const env = buildKimiCliEnvironment(opts);
|
|
64
64
|
if (opts.logPath !== undefined) {
|
|
65
65
|
await mkdir(path.dirname(opts.logPath), { recursive: true }).catch(() => {
|
|
66
66
|
// Best-effort; if mkdir fails the appendFile below will error and be
|
|
@@ -788,7 +788,7 @@ function buildArgs(opts) {
|
|
|
788
788
|
args.push("-p", opts.prompt);
|
|
789
789
|
return args;
|
|
790
790
|
}
|
|
791
|
-
function
|
|
791
|
+
export function buildKimiCliEnvironment(opts) {
|
|
792
792
|
const env = { ...opts.env };
|
|
793
793
|
if (opts.thinkingEffort !== undefined)
|
|
794
794
|
env.KIMI_MODEL_THINKING_EFFORT = opts.thinkingEffort;
|
|
@@ -117,7 +117,11 @@ export async function captureTimeoutPartialOutput(classified, store, job, env, c
|
|
|
117
117
|
return undefined;
|
|
118
118
|
const result = timeout.partialResult;
|
|
119
119
|
let sessionId = result.sessionId;
|
|
120
|
-
let source = sessionId
|
|
120
|
+
let source = sessionId
|
|
121
|
+
? sessionId === job.kimi_session_id
|
|
122
|
+
? job.kimi_session_id_source
|
|
123
|
+
: "announced"
|
|
124
|
+
: null;
|
|
121
125
|
const details = classified instanceof RuntimeError ? classified.details : timeout.details;
|
|
122
126
|
details.kimi_session_id_source = job.kimi_session_id_source;
|
|
123
127
|
// The persisted plan is plugin-owned. Recovery never runs on cancellation,
|
package/dist/commands/swarm.js
CHANGED
|
@@ -2,7 +2,8 @@ import { allocateJobId } from "../job-id.js";
|
|
|
2
2
|
import { mkdir, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createCliCancellationHandlers } from "../cli-cancellation.js";
|
|
5
|
-
import { runCliPromptWithBudget } from "../cli-client.js";
|
|
5
|
+
import { ResponseTimeoutError, runCliPromptWithBudget } from "../cli-client.js";
|
|
6
|
+
import { deactivateSwarmSession, preactivateSwarmSession, requiresSwarmPreactivation, } from "../swarm-activation.js";
|
|
6
7
|
import { executionPlanFromPersisted, observedExecutionFields, persistedExecutionPlanFields, prepareKimiExecutionPlan, } from "../kimi-engine.js";
|
|
7
8
|
import { RuntimeError } from "../errors.js";
|
|
8
9
|
import { captureWorktreePatch, createEphemeralWorktree, hasBornHead, isWorkingTreeDirty, pruneWorktrees, removeWorktree, resolveRepoIdentity, } from "../git.js";
|
|
@@ -24,7 +25,8 @@ import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js
|
|
|
24
25
|
// only the wall-clock hard bound. Read/write modes reuse review/rescue lineage,
|
|
25
26
|
// and the mandatory PreToolUse hook enforces read-only policy or trusted-worktree
|
|
26
27
|
// write confinement on the coordinator and every subagent. Write mode requires
|
|
27
|
-
// kimi-code 0.18.0+.
|
|
28
|
+
// kimi-code 0.18.0+. Exact 2.3.2 additionally activates a same-job session through
|
|
29
|
+
// a bounded loopback web lifecycle, then resumes it through the normal print path.
|
|
28
30
|
const SWARM_SUMMARY_MAX = 120;
|
|
29
31
|
const SWARM_AGENT_PROFILE = "<swarm>";
|
|
30
32
|
const SWARM_WRITE_AGENT_PROFILE = "<swarm-write>";
|
|
@@ -87,7 +89,7 @@ export function resolveSwarmWriteMaxConcurrency(requested) {
|
|
|
87
89
|
* subagent, so slimming this text does not move the safety boundary. The optional
|
|
88
90
|
* `cap` is a SOFT subagent-count hint (the hook cannot enforce a count). The HARD
|
|
89
91
|
* concurrency ceiling is separate: --max-concurrency →
|
|
90
|
-
* KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY (see executeSwarmJob / cli-client
|
|
92
|
+
* KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY (see executeSwarmJob / cli-client buildKimiCliEnvironment).
|
|
91
93
|
* The --budget wall-clock ceiling is the always-on hard bound on the whole run.
|
|
92
94
|
*/
|
|
93
95
|
export function buildSwarmPrompt(objective, cap) {
|
|
@@ -343,7 +345,7 @@ async function runWriteSwarm(parsed, objective, context) {
|
|
|
343
345
|
}
|
|
344
346
|
finally {
|
|
345
347
|
if (preserveWorktree) {
|
|
346
|
-
context.stderr.write(`[kimi-plugin-cc] swarm-write patch capture failed; worktree retained at ${worktreePath}.\n`);
|
|
348
|
+
context.stderr.write(`[kimi-plugin-cc] swarm-write patch capture or session cleanup failed; worktree retained at ${worktreePath}.\n`);
|
|
347
349
|
}
|
|
348
350
|
else {
|
|
349
351
|
await removeWorktree(repoIdentity.repoRoot, worktreePath);
|
|
@@ -465,44 +467,163 @@ writeMode) {
|
|
|
465
467
|
}
|
|
466
468
|
}
|
|
467
469
|
const handlers = createCliCancellationHandlers();
|
|
470
|
+
const budgetDeadline = Date.now() + budgetMs;
|
|
471
|
+
let activationOptions;
|
|
472
|
+
let preactivated;
|
|
473
|
+
let deactivationRequired = false;
|
|
474
|
+
let cleanupAttempted = false;
|
|
475
|
+
let cleanupFailure;
|
|
476
|
+
let finalText;
|
|
477
|
+
const commandLabel = writeMode ? "swarm-write" : "swarm";
|
|
478
|
+
const recoveryCleanupTimeoutMs = 2_000;
|
|
479
|
+
const cleanupSignal = (timeoutMs) => AbortSignal.any([
|
|
480
|
+
handlers.signal,
|
|
481
|
+
AbortSignal.timeout(Math.max(1, timeoutMs)),
|
|
482
|
+
]);
|
|
483
|
+
const attemptDeactivation = async (identity, fallbackStage, timeoutMs = Math.max(1, budgetDeadline - Date.now())) => {
|
|
484
|
+
cleanupAttempted = true;
|
|
485
|
+
try {
|
|
486
|
+
await deactivateSwarmSession({
|
|
487
|
+
...activationOptions,
|
|
488
|
+
...identity,
|
|
489
|
+
signal: cleanupSignal(timeoutMs),
|
|
490
|
+
});
|
|
491
|
+
deactivationRequired = false;
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
return error instanceof RuntimeError
|
|
496
|
+
? error
|
|
497
|
+
: new RuntimeError("SWARM_SESSION_CLEANUP_FAILED", "Swarm session cleanup failed without a safe diagnostic payload.", fallbackStage);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
468
500
|
try {
|
|
469
501
|
const executionPlan = executionPlanFromPersisted(job);
|
|
502
|
+
activationOptions = {
|
|
503
|
+
cwd: writeMode?.spawnCwd ?? job.cwd,
|
|
504
|
+
env: context.env,
|
|
505
|
+
command: executionPlan.command,
|
|
506
|
+
prefixArgs: executionPlan.prefixArgs,
|
|
507
|
+
executionPlan,
|
|
508
|
+
commandLabel,
|
|
509
|
+
model: job.model ?? undefined,
|
|
510
|
+
thinkingEffort: job.thinking_effort ?? undefined,
|
|
511
|
+
swarmMaxConcurrency: maxConcurrency,
|
|
512
|
+
trustedWorkspaceRoot: writeMode?.workspaceRoot,
|
|
513
|
+
expectedVersion: executionPlan.kimiVersion ?? undefined,
|
|
514
|
+
};
|
|
515
|
+
if (requiresSwarmPreactivation(executionPlan.kimiVersion)) {
|
|
516
|
+
store.updateRunningJob(job.job_id, { phase: "turn-running" });
|
|
517
|
+
const activationController = new AbortController();
|
|
518
|
+
const onParentAbort = () => activationController.abort();
|
|
519
|
+
let activationTimedOut = false;
|
|
520
|
+
handlers.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
521
|
+
if (handlers.signal.aborted)
|
|
522
|
+
onParentAbort();
|
|
523
|
+
const activationTimer = setTimeout(() => {
|
|
524
|
+
activationTimedOut = true;
|
|
525
|
+
activationController.abort();
|
|
526
|
+
}, Math.max(1, budgetDeadline - Date.now()));
|
|
527
|
+
activationTimer.unref();
|
|
528
|
+
try {
|
|
529
|
+
preactivated = await preactivateSwarmSession({
|
|
530
|
+
...activationOptions,
|
|
531
|
+
signal: activationController.signal,
|
|
532
|
+
});
|
|
533
|
+
deactivationRequired = true;
|
|
534
|
+
store.updateRunningJob(job.job_id, {
|
|
535
|
+
kimi_session_id: preactivated.sessionId,
|
|
536
|
+
kimi_session_id_source: "preactivated",
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
const activationDidTimeOut = activationTimedOut;
|
|
541
|
+
clearTimeout(activationTimer);
|
|
542
|
+
handlers.signal.removeEventListener("abort", onParentAbort);
|
|
543
|
+
const details = error instanceof RuntimeError ? error.details : {};
|
|
544
|
+
const sessionId = typeof details.session_id === "string" ? details.session_id : undefined;
|
|
545
|
+
const workspaceId = typeof details.workspace_id === "string" ? details.workspace_id : undefined;
|
|
546
|
+
const hasSessionIdentity = sessionId !== undefined && workspaceId !== undefined;
|
|
547
|
+
if (error instanceof RuntimeError &&
|
|
548
|
+
error.code === "SWARM_SESSION_CLEANUP_FAILED" &&
|
|
549
|
+
details.process_cleanup_unverified === true) {
|
|
550
|
+
cleanupFailure = error;
|
|
551
|
+
}
|
|
552
|
+
if (hasSessionIdentity) {
|
|
553
|
+
store.updateRunningJob(job.job_id, {
|
|
554
|
+
kimi_session_id: sessionId,
|
|
555
|
+
kimi_session_id_source: "preactivated",
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
if (hasSessionIdentity && !handlers.cancelling) {
|
|
559
|
+
const sessionCleanupFailure = await attemptDeactivation({ sessionId, workspaceId }, "swarm.activation-cleanup", recoveryCleanupTimeoutMs);
|
|
560
|
+
if (sessionCleanupFailure)
|
|
561
|
+
cleanupFailure = sessionCleanupFailure;
|
|
562
|
+
}
|
|
563
|
+
else if (hasSessionIdentity && cleanupFailure === undefined) {
|
|
564
|
+
cleanupFailure = new RuntimeError("SWARM_SESSION_CLEANUP_FAILED", "Swarm session cleanup was not completed after activation was cancelled.", "swarm.activation-cleanup", { details: { cancelled: true, cleanup_unverified: true } });
|
|
565
|
+
}
|
|
566
|
+
if (activationDidTimeOut && !handlers.cancelling) {
|
|
567
|
+
throw new ResponseTimeoutError(budgetMs, "swarm.activation", commandLabel, undefined, error);
|
|
568
|
+
}
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
571
|
+
finally {
|
|
572
|
+
clearTimeout(activationTimer);
|
|
573
|
+
handlers.signal.removeEventListener("abort", onParentAbort);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const promptBudgetMs = budgetDeadline - Date.now();
|
|
577
|
+
if (promptBudgetMs <= 0)
|
|
578
|
+
throw new ResponseTimeoutError(budgetMs, "swarm.prompt", commandLabel);
|
|
470
579
|
store.updateRunningJob(job.job_id, { phase: "turn-running" });
|
|
471
580
|
const result = await runCliPromptWithBudget({
|
|
472
|
-
|
|
473
|
-
// worktree so coder subagents edit there (job.cwd stays the user's real
|
|
474
|
-
// cwd for lineage). This is the load-bearing isolation seam.
|
|
475
|
-
cwd: writeMode?.spawnCwd ?? job.cwd,
|
|
581
|
+
cwd: activationOptions.cwd,
|
|
476
582
|
env: context.env,
|
|
477
583
|
command: executionPlan.command,
|
|
478
584
|
prefixArgs: [...executionPlan.prefixArgs],
|
|
479
585
|
executionPlan,
|
|
480
586
|
prompt,
|
|
481
|
-
|
|
482
|
-
// PLUS rescue-grade write confinement to the trusted worktree root. The
|
|
483
|
-
// label drives the hook for the coordinator AND every spawned subagent.
|
|
484
|
-
commandLabel: writeMode ? "swarm-write" : "swarm",
|
|
485
|
-
// Always exported as KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY. kimi-code
|
|
486
|
-
// 0.18.0+ enforces this hard peak ceiling; older read-only binaries ignore
|
|
487
|
-
// it and retain only the wall-clock hard bound. Write mode requires
|
|
488
|
-
// 0.18.0+. Distinct from --cap, the soft total-count prompt hint.
|
|
587
|
+
commandLabel,
|
|
489
588
|
swarmMaxConcurrency: maxConcurrency,
|
|
490
|
-
// Trusted allowlist root for the swarm-write hook (forge-proof; ignored
|
|
491
|
-
// by other labels). Only set in write mode.
|
|
492
589
|
trustedWorkspaceRoot: writeMode?.workspaceRoot,
|
|
493
590
|
model: job.model ?? undefined,
|
|
494
591
|
thinkingEffort: job.thinking_effort ?? undefined,
|
|
592
|
+
resumeSessionId: preactivated?.sessionId,
|
|
495
593
|
logPath: job.stream_log_path,
|
|
496
594
|
onSpawn: (pid) => { store.updateRunningJob(job.job_id, { kimi_pid: pid }); },
|
|
497
595
|
signal: handlers.signal,
|
|
498
|
-
},
|
|
596
|
+
}, promptBudgetMs, "swarm.prompt");
|
|
499
597
|
store.updateRunningJob(job.job_id, observedExecutionFields(result));
|
|
598
|
+
finalText = reassembleProseFromRecords(result.records);
|
|
599
|
+
let resultError;
|
|
500
600
|
if (handlers.cancelling) {
|
|
501
|
-
|
|
601
|
+
resultError = new RuntimeError("SWARM_CANCELLED", "Swarm cancelled by user request.", "swarm.runtime");
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
try {
|
|
605
|
+
assertCliResultSuccess(result, "swarm.runtime");
|
|
606
|
+
if (preactivated && result.sessionId !== preactivated.sessionId) {
|
|
607
|
+
throw new RuntimeError("SWARM_SESSION_MISMATCH", "The resumed print session differs from the preactivated swarm session.", "swarm.session-provenance", { details: { expected_session_id: preactivated.sessionId, actual_session_id: result.sessionId ?? null } });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
resultError = error;
|
|
612
|
+
}
|
|
502
613
|
}
|
|
503
|
-
assertCliResultSuccess(result, "swarm.runtime");
|
|
504
614
|
warnHookEscapes(result, "swarm", context.stderr);
|
|
505
|
-
if (
|
|
615
|
+
if (preactivated) {
|
|
616
|
+
cleanupFailure = await attemptDeactivation(preactivated, "swarm.cleanup");
|
|
617
|
+
}
|
|
618
|
+
if (resultError !== undefined)
|
|
619
|
+
throw resultError;
|
|
620
|
+
if (handlers.cancelling) {
|
|
621
|
+
throw new RuntimeError("SWARM_CANCELLED", "Swarm cancelled by user request.", "swarm.runtime");
|
|
622
|
+
}
|
|
623
|
+
if (cleanupFailure)
|
|
624
|
+
throw cleanupFailure;
|
|
625
|
+
if (!preactivated &&
|
|
626
|
+
result.sessionId !== undefined &&
|
|
506
627
|
result.sessionId.length > 0 &&
|
|
507
628
|
result.sessionId !== job.kimi_session_id) {
|
|
508
629
|
store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
|
|
@@ -517,7 +638,7 @@ writeMode) {
|
|
|
517
638
|
});
|
|
518
639
|
warnIfSessionIdMissing(result, "swarm", job.job_id, context.stderr);
|
|
519
640
|
const prose = reassembleProseFromRecords(result.records);
|
|
520
|
-
|
|
641
|
+
finalText = prose;
|
|
521
642
|
if (writeMode) {
|
|
522
643
|
const captured = await capturePatchArtifact(paths, job.job_id, writeMode.workspaceRoot, context.stderr, writeMode.onPatchCaptureFailure);
|
|
523
644
|
if (captured === null) {
|
|
@@ -549,6 +670,21 @@ writeMode) {
|
|
|
549
670
|
}) ?? job);
|
|
550
671
|
}
|
|
551
672
|
catch (error) {
|
|
673
|
+
if (preactivated && deactivationRequired && activationOptions) {
|
|
674
|
+
if (handlers.cancelling) {
|
|
675
|
+
cleanupFailure = new RuntimeError("SWARM_SESSION_CLEANUP_FAILED", "Swarm session cleanup was not completed after cancellation.", "swarm.cleanup", { details: { cancelled: true, cleanup_unverified: true } });
|
|
676
|
+
}
|
|
677
|
+
else if (!cleanupAttempted) {
|
|
678
|
+
cleanupFailure = await attemptDeactivation(preactivated, "swarm.cleanup", recoveryCleanupTimeoutMs);
|
|
679
|
+
}
|
|
680
|
+
else if (error instanceof RuntimeError && error.code === "SWARM_SESSION_CLEANUP_FAILED") {
|
|
681
|
+
cleanupFailure ??= error;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (cleanupFailure) {
|
|
685
|
+
writeMode?.onPatchCaptureFailure?.();
|
|
686
|
+
context.stderr.write(`[kimi-plugin-cc] swarm session cleanup also failed: ${cleanupFailure.message}\n`);
|
|
687
|
+
}
|
|
552
688
|
let captureFailed = writeMode !== undefined && error instanceof RuntimeError && error.code === "SWARM_PATCH_CAPTURE_FAILED";
|
|
553
689
|
if (writeMode && !(error instanceof RuntimeError && error.code === "SWARM_PATCH_CAPTURE_FAILED")) {
|
|
554
690
|
const captured = await capturePatchArtifact(paths, job.job_id, writeMode.workspaceRoot, context.stderr, writeMode.onPatchCaptureFailure);
|
|
@@ -557,34 +693,55 @@ writeMode) {
|
|
|
557
693
|
context.stderr.write(`[kimi-plugin-cc] swarm-write run ended early; partial patch captured at ${captured.path}.\n`);
|
|
558
694
|
}
|
|
559
695
|
}
|
|
560
|
-
const recoveryDetails =
|
|
696
|
+
const recoveryDetails = {
|
|
697
|
+
...(error instanceof RuntimeError && typeof error.details.exit_code === "number"
|
|
698
|
+
? { exit_code: error.details.exit_code }
|
|
699
|
+
: {}),
|
|
700
|
+
...((captureFailed || cleanupFailure) && writeMode ? { worktreePath: writeMode.workspaceRoot } : {}),
|
|
701
|
+
...(cleanupFailure
|
|
702
|
+
? {
|
|
703
|
+
cleanup_error: {
|
|
704
|
+
code: cleanupFailure.code,
|
|
705
|
+
stage: cleanupFailure.stage,
|
|
706
|
+
cancelled: handlers.cancelling,
|
|
707
|
+
unverified: true,
|
|
708
|
+
},
|
|
709
|
+
...(finalText !== undefined ? { rawOutput: finalText } : {}),
|
|
710
|
+
}
|
|
711
|
+
: {}),
|
|
712
|
+
};
|
|
561
713
|
if (handlers.cancelling) {
|
|
714
|
+
const cancellationCause = classifyManagedCommandFailure(error, writeMode ? "rescue" : "review", job.job_id, {
|
|
715
|
+
preserveStage: true,
|
|
716
|
+
});
|
|
562
717
|
const cancelledError = new RuntimeError("SWARM_CANCELLED", captureFailed && writeMode
|
|
563
718
|
? `Swarm cancelled; patch capture failed and worktree retained at ${writeMode.workspaceRoot}.`
|
|
564
719
|
: "Swarm cancelled by user request.", "swarm.runtime", {
|
|
565
720
|
...(error instanceof Error ? { cause: error } : {}),
|
|
566
|
-
details:
|
|
721
|
+
details: {
|
|
722
|
+
...(cancellationCause instanceof RuntimeError ? cancellationCause.details : {}),
|
|
723
|
+
...recoveryDetails,
|
|
724
|
+
},
|
|
567
725
|
});
|
|
568
726
|
return await markJobCancelled(store, paths, job, "Swarm cancelled by user request.", cancelledError, {
|
|
569
727
|
phase: "cancelled",
|
|
570
728
|
});
|
|
571
729
|
}
|
|
730
|
+
if (error === cleanupFailure && budgetDeadline <= Date.now() && !(error instanceof ResponseTimeoutError)) {
|
|
731
|
+
error = new ResponseTimeoutError(budgetMs, "swarm.cleanup", commandLabel, undefined, cleanupFailure);
|
|
732
|
+
}
|
|
572
733
|
const classified = classifyManagedCommandFailure(error, writeMode ? "rescue" : "review", job.job_id, {
|
|
573
734
|
preserveStage: true,
|
|
574
735
|
});
|
|
575
|
-
|
|
576
|
-
|
|
736
|
+
const withRecovery = captureFailed || cleanupFailure
|
|
737
|
+
? classified instanceof RuntimeError
|
|
577
738
|
? new RuntimeError(classified.code, classified.message, classified.stage, {
|
|
578
739
|
cause: classified,
|
|
579
740
|
details: { ...classified.details, ...recoveryDetails },
|
|
580
741
|
})
|
|
581
|
-
: new RuntimeError("SWARM_PATCH_CAPTURE_FAILED", classified.message, "swarm.patch-capture", {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
});
|
|
585
|
-
return await markJobFailed(store, paths, job, withRecovery, "Swarm failed.", { phase: "failed" });
|
|
586
|
-
}
|
|
587
|
-
return await markJobFailed(store, paths, job, classified, "Swarm failed.", { phase: "failed" });
|
|
742
|
+
: new RuntimeError(cleanupFailure?.code ?? "SWARM_PATCH_CAPTURE_FAILED", classified.message, cleanupFailure?.stage ?? "swarm.patch-capture", { cause: classified, details: recoveryDetails })
|
|
743
|
+
: classified;
|
|
744
|
+
return await markJobFailed(store, paths, job, withRecovery, "Swarm failed.", { phase: "failed" });
|
|
588
745
|
}
|
|
589
746
|
finally {
|
|
590
747
|
handlers.dispose();
|
|
@@ -594,8 +594,8 @@ function validateVersionSensitiveEvents(events, version) {
|
|
|
594
594
|
}
|
|
595
595
|
// Exact schemas reviewed at 2.0.0 (1b89e4b0), 2.0.1 (caf7d4e2),
|
|
596
596
|
// 2.0.2 (9d07f634), 2.1.0 (52437299), 2.1.1 (f67e6398), 2.2.0 (472192c3),
|
|
597
|
-
// 2.3.0 (b5e28fd6)
|
|
598
|
-
// are unchanged. Require the full version string AND matching components;
|
|
597
|
+
// 2.3.0 (b5e28fd6), 2.3.1 (684bae9e) and 2.3.2 (b8dd3a13): the additive
|
|
598
|
+
// events and HookDefSchema are unchanged. Require the full version string AND matching components;
|
|
599
599
|
// no inferred 2.x compatibility.
|
|
600
600
|
if (version.major === 2 && ((version.minor === 0 && ((version.version === "2.0.0" && version.patch === 0) ||
|
|
601
601
|
(version.version === "2.0.1" && version.patch === 1) ||
|
|
@@ -604,7 +604,8 @@ function validateVersionSensitiveEvents(events, version) {
|
|
|
604
604
|
(version.version === "2.1.1" && version.patch === 1))) ||
|
|
605
605
|
(version.minor === 2 && ((version.version === "2.2.0" && version.patch === 0))) ||
|
|
606
606
|
(version.minor === 3 && ((version.version === "2.3.0" && version.patch === 0) ||
|
|
607
|
-
(version.version === "2.3.1" && version.patch === 1)
|
|
607
|
+
(version.version === "2.3.1" && version.patch === 1) ||
|
|
608
|
+
(version.version === "2.3.2" && version.patch === 2))))) {
|
|
608
609
|
return { valid: true };
|
|
609
610
|
}
|
|
610
611
|
if (version.major !== 0) {
|
package/dist/kimi-engine.js
CHANGED
package/dist/process-tree.js
CHANGED
|
@@ -6,7 +6,7 @@ const MAX_DESCENDANT_DEPTH = 8;
|
|
|
6
6
|
const MAX_DESCENDANT_PIDS = 512;
|
|
7
7
|
const PROCESS_EXIT_POLL_MS = 10;
|
|
8
8
|
const PROCESS_SNAPSHOT_TIMEOUT_MS = 1_000;
|
|
9
|
-
const PROCESS_IDENTITY_PROBE_TIMEOUT_MS =
|
|
9
|
+
const PROCESS_IDENTITY_PROBE_TIMEOUT_MS = 1_000;
|
|
10
10
|
const PROBE_TIMEOUT = Symbol("process-probe-timeout");
|
|
11
11
|
/**
|
|
12
12
|
* Capture descendant ancestry and reusable identities in the same bounded
|
|
@@ -23,6 +23,18 @@ export async function collectDescendantIdentities(rootPid) {
|
|
|
23
23
|
}
|
|
24
24
|
return snapshot;
|
|
25
25
|
}
|
|
26
|
+
/** Return current members of the selected process groups, or undefined when unreadable. */
|
|
27
|
+
export async function collectProcessGroupsIdentities(processGroupIds) {
|
|
28
|
+
if (process.platform === "win32")
|
|
29
|
+
return undefined;
|
|
30
|
+
const groups = new Set(processGroupIds.filter((id) => Number.isInteger(id) && id > 0));
|
|
31
|
+
if (groups.size === 0)
|
|
32
|
+
return [];
|
|
33
|
+
const rows = await raceValueWithTimeout(collectProcessSnapshotRows(), PROCESS_SNAPSHOT_TIMEOUT_MS);
|
|
34
|
+
if (rows === PROBE_TIMEOUT || rows === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
return rows.filter((row) => groups.has(row.processGroupId)).map(toProcessIdentity);
|
|
37
|
+
}
|
|
26
38
|
/**
|
|
27
39
|
* Compatibility surface for callers that only need numeric process listings.
|
|
28
40
|
* Cancellation must use collectDescendantIdentities so ancestry and identity
|
|
@@ -36,15 +48,21 @@ export async function collectDescendants(rootPid) {
|
|
|
36
48
|
return [];
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
|
-
/**
|
|
40
|
-
export async function
|
|
51
|
+
/** Distinguish a missing process from a failed or timed-out identity probe. */
|
|
52
|
+
export async function probeProcessIdentity(pid) {
|
|
41
53
|
if (!Number.isInteger(pid) || pid <= 0 || process.platform === "win32")
|
|
42
|
-
return
|
|
54
|
+
return { complete: true };
|
|
43
55
|
const read = process.platform === "linux"
|
|
44
56
|
? readProcessIdentityBatchFromProc([pid])
|
|
45
57
|
: readProcessIdentityBatchWithPs([pid]);
|
|
46
58
|
const batch = await raceValueWithTimeout(read, PROCESS_IDENTITY_PROBE_TIMEOUT_MS);
|
|
47
|
-
|
|
59
|
+
if (batch === PROBE_TIMEOUT)
|
|
60
|
+
return { complete: false };
|
|
61
|
+
return { identity: batch.identities.get(pid), complete: batch.complete };
|
|
62
|
+
}
|
|
63
|
+
/** Read one reusable process identity. PID alone is not stable across teardown. */
|
|
64
|
+
export async function readProcessIdentity(pid) {
|
|
65
|
+
return (await probeProcessIdentity(pid)).identity;
|
|
48
66
|
}
|
|
49
67
|
/**
|
|
50
68
|
* Revalidate all captured identities concurrently under one deadline. The
|
|
@@ -63,16 +81,26 @@ export async function revalidateProcessIdentities(expectedIdentities, options =
|
|
|
63
81
|
if (batch === PROBE_TIMEOUT)
|
|
64
82
|
return { identities: [], complete: false };
|
|
65
83
|
const identities = [];
|
|
84
|
+
const groupChanged = [];
|
|
66
85
|
for (const expectedIdentity of expected) {
|
|
67
86
|
const current = batch.identities.get(expectedIdentity.pid);
|
|
68
|
-
if (current
|
|
69
|
-
current.startToken
|
|
70
|
-
current.
|
|
71
|
-
|
|
87
|
+
if (current === undefined ||
|
|
88
|
+
current.startToken !== expectedIdentity.startToken ||
|
|
89
|
+
isQuiescentState(current.state)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (current.processGroupId !== expectedIdentity.processGroupId) {
|
|
93
|
+
groupChanged.push(current);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
72
96
|
identities.push(current);
|
|
73
97
|
}
|
|
74
98
|
}
|
|
75
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
identities,
|
|
101
|
+
complete: batch.complete,
|
|
102
|
+
...(groupChanged.length > 0 ? { groupChanged } : {}),
|
|
103
|
+
};
|
|
76
104
|
}
|
|
77
105
|
/** Return the current identity only when pid, start token, and PGID still match. */
|
|
78
106
|
export async function revalidateProcessIdentity(expected, identityReader) {
|
|
@@ -104,7 +132,7 @@ export async function waitForProcessTreeExit(identities, timeoutMs, options = {}
|
|
|
104
132
|
identityReader: options.identityReader,
|
|
105
133
|
timeoutMs: probeTimeoutMs,
|
|
106
134
|
});
|
|
107
|
-
active = !validation.complete || validation.identities.length > 0;
|
|
135
|
+
active = !validation.complete || validation.identities.length > 0 || (validation.groupChanged?.length ?? 0) > 0;
|
|
108
136
|
}
|
|
109
137
|
if (!active)
|
|
110
138
|
return true;
|
|
@@ -117,13 +145,13 @@ export async function waitForProcessTreeExit(identities, timeoutMs, options = {}
|
|
|
117
145
|
async function collectVerifiedDescendantIdentities(rootPid) {
|
|
118
146
|
const initialRows = await collectProcessSnapshotRows();
|
|
119
147
|
if (initialRows === undefined)
|
|
120
|
-
|
|
148
|
+
throw new Error("process snapshot unavailable");
|
|
121
149
|
const candidateRows = descendantRowsFromSnapshot(rootPid, initialRows);
|
|
122
150
|
const initialRoot = initialRows.find((row) => row.pid === rootPid);
|
|
123
151
|
if (initialRoot === undefined || candidateRows.length === 0)
|
|
124
152
|
return [];
|
|
125
|
-
// A second, targeted snapshot closes the cross-row /proc race:
|
|
126
|
-
//
|
|
153
|
+
// A second, targeted snapshot closes the cross-row /proc race: each child
|
|
154
|
+
// must retain its PID and start token. Reparenting and PGID changes are not exit.
|
|
127
155
|
const expectedRows = [initialRoot, ...candidateRows];
|
|
128
156
|
const expectedByPid = new Map(expectedRows.map((row) => [row.pid, row]));
|
|
129
157
|
const verificationRows = await readCurrentSnapshotRows(expectedRows);
|
|
@@ -131,11 +159,9 @@ async function collectVerifiedDescendantIdentities(rootPid) {
|
|
|
131
159
|
const expected = expectedByPid.get(current.pid);
|
|
132
160
|
return (expected !== undefined &&
|
|
133
161
|
current.startToken === expected.startToken &&
|
|
134
|
-
current.processGroupId === expected.processGroupId &&
|
|
135
|
-
current.parentPid === expected.parentPid &&
|
|
136
162
|
!isQuiescentState(current.state));
|
|
137
163
|
});
|
|
138
|
-
return
|
|
164
|
+
return stableRows.map(toProcessIdentity);
|
|
139
165
|
}
|
|
140
166
|
function descendantRowsFromSnapshot(rootPid, rows) {
|
|
141
167
|
const root = rows.find((row) => row.pid === rootPid);
|
|
@@ -186,6 +212,9 @@ async function readCurrentSnapshotRows(expectedRows) {
|
|
|
186
212
|
Promise.all(procPids.map((pid) => readProcRow(pid))),
|
|
187
213
|
collectProcessSnapshotRowsFromPs(psPids),
|
|
188
214
|
]);
|
|
215
|
+
if (procOutcomes.some((outcome) => outcome.status === "error") || (psPids.length > 0 && psRows === undefined)) {
|
|
216
|
+
throw new Error("process identity verification failed");
|
|
217
|
+
}
|
|
189
218
|
return [
|
|
190
219
|
...procOutcomes.flatMap((outcome) => outcome.status === "found" ? [outcome.row] : []),
|
|
191
220
|
...(psRows ?? []),
|
|
@@ -212,6 +241,8 @@ async function collectProcessSnapshotRowsFromProc() {
|
|
|
212
241
|
const outcomes = await Promise.all(entries
|
|
213
242
|
.filter((entry) => /^\d+$/.test(entry))
|
|
214
243
|
.map((entry) => readProcRow(Number.parseInt(entry, 10))));
|
|
244
|
+
if (outcomes.some((outcome) => outcome.status === "error"))
|
|
245
|
+
return undefined;
|
|
215
246
|
return outcomes.flatMap((outcome) => outcome.status === "found" ? [outcome.row] : []);
|
|
216
247
|
}
|
|
217
248
|
async function collectProcessSnapshotRowsFromPs(pids) {
|