@zhushanwen/subagent-core 0.2.0 → 0.4.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 +45 -2
- package/agents/analyst.md +60 -0
- package/agents/coder.md +69 -0
- package/agents/debugger.md +66 -0
- package/agents/doc-reviewer.md +49 -0
- package/agents/explorer.md +63 -0
- package/agents/general-purpose.md +32 -0
- package/agents/orchestrator.md +61 -0
- package/agents/planner.md +53 -0
- package/agents/researcher.md +67 -0
- package/agents/reviewer.md +73 -0
- package/dist/chunk-4KN4TTG7.js +240 -0
- package/dist/chunk-APZY4IME.js +27 -0
- package/dist/chunk-X7SZ5HLQ.js +66 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
- package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
- package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
- package/dist/execution/engine/engines/zcode/constants.js +43 -1
- package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
- package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
- package/dist/execution/engine/engines/zcode/reader.js +4 -234
- package/dist/execution/engine/paths.js +7 -19
- package/dist/index.cjs +14709 -100
- package/dist/index.d.cts +4053 -200
- package/dist/index.d.ts +4053 -200
- package/dist/index.js +14358 -102
- package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
- package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
- package/package.json +6 -3
- package/src/__tests__/review-fix-loop-script.test.ts +90 -2
- package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
- package/src/__tests__/smoke.test.ts +9 -2
- package/src/execution/__tests__/agent-profile.test.ts +232 -0
- package/src/execution/__tests__/agents-assembly.test.ts +219 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
- package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
- package/src/execution/__tests__/delivery-methods.test.ts +2 -0
- package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
- package/src/execution/__tests__/dialog-queue.test.ts +199 -1
- package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
- package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
- package/src/execution/__tests__/finalize-record.test.ts +106 -0
- package/src/execution/__tests__/gc-timer.test.ts +2 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
- package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
- package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
- package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
- package/src/execution/__tests__/notify-ledger.test.ts +294 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
- package/src/execution/__tests__/record-store.test.ts +83 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
- package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
- package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
- package/src/execution/__tests__/session-pending.test.ts +228 -0
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
- package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
- package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
- package/src/execution/__tests__/spawned-children.test.ts +153 -10
- package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
- package/src/execution/__tests__/timeout-integration.test.ts +7 -7
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
- package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
- package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
- package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
- package/src/execution/agent-registry.ts +225 -1
- package/src/execution/agents-assembly.ts +88 -0
- package/src/execution/concurrency-pool.ts +71 -9
- package/src/execution/dialog-queue.ts +101 -3
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
- package/src/execution/engine/__tests__/registry.test.ts +90 -1
- package/src/execution/engine/engine-discovery.ts +12 -16
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
- package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
- package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
- package/src/execution/engine/engines/zcode/connection.ts +585 -0
- package/src/execution/engine/engines/zcode/constants.ts +129 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
- package/src/execution/engine/engines/zcode/preparer.ts +25 -22
- package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
- package/src/execution/engine/host-task-spec.ts +3 -3
- package/src/execution/engine/port.ts +41 -1
- package/src/execution/engine/registry.ts +53 -0
- package/src/execution/finalize-record.ts +63 -5
- package/src/execution/get-state-handshake.ts +85 -10
- package/src/execution/lifecycle-manager.ts +27 -3
- package/src/execution/manifest-store.ts +53 -62
- package/src/execution/notifier.ts +10 -10
- package/src/execution/notify-ledger.ts +117 -16
- package/src/execution/record-entry.ts +8 -2
- package/src/execution/record-store.ts +29 -15
- package/src/execution/session-pending.ts +213 -62
- package/src/execution/session-runner.ts +974 -128
- package/src/execution/sessions-index.ts +10 -55
- package/src/execution/settled-watchdog.ts +99 -0
- package/src/execution/subagent-actions-core.ts +686 -0
- package/src/execution/subagent-service.ts +396 -49
- package/src/execution/ui-request-handler-factory.ts +27 -4
- package/src/execution/worktree-git-ops.ts +397 -0
- package/src/execution/worktree-manager.ts +72 -10
- package/src/execution/worktree-registry.ts +6 -12
- package/src/index.ts +413 -6
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
- package/src/orchestration/__tests__/args-meta.test.ts +358 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
- package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
- package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
- package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
- package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
- package/src/orchestration/__tests__/script-generate.test.ts +314 -0
- package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
- package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
- package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
- package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
- package/src/orchestration/agent-opts-resolver.ts +7 -7
- package/src/orchestration/args-meta.ts +198 -0
- package/src/orchestration/error-recovery.ts +416 -146
- package/src/orchestration/execute-agent-call.ts +9 -9
- package/src/orchestration/file-run-store.ts +327 -0
- package/src/orchestration/launcher.ts +35 -26
- package/src/orchestration/lifecycle.ts +355 -79
- package/src/orchestration/models/agent-call.ts +7 -7
- package/src/orchestration/models/budget.ts +5 -5
- package/src/orchestration/models/run-runtime.ts +13 -13
- package/src/orchestration/models/trace.ts +8 -8
- package/src/orchestration/models/workflow-run.ts +27 -27
- package/src/orchestration/models/workflow-script.ts +4 -4
- package/src/orchestration/run-snapshot.ts +266 -0
- package/src/orchestration/script-generate.ts +154 -0
- package/src/orchestration/script-lint.ts +33 -33
- package/src/orchestration/worker-handle.ts +10 -10
- package/src/orchestration/worker-host.ts +10 -10
- package/src/orchestration/worker-script-builder.ts +401 -346
- package/src/orchestration/workflow-files.ts +37 -11
- package/src/orchestration/workflow-run-summary.ts +69 -0
- package/src/orchestration/workflow-script-registry-impl.ts +31 -9
- package/src/shared/__tests__/agent-ref.test.ts +209 -2
- package/src/shared/__tests__/atomic-write.test.ts +267 -0
- package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
- package/src/shared/__tests__/injection-render.test.ts +518 -0
- package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
- package/src/shared/__tests__/resource-discovery.test.ts +3 -2
- package/src/shared/agent-ref.ts +143 -2
- package/src/shared/atomic-write.ts +320 -0
- package/src/shared/bounded-serialize.ts +154 -0
- package/src/shared/injection-render.ts +279 -0
- package/src/shared/meta-parser.ts +41 -1
- package/src/shared/resource-discovery.ts +104 -37
- package/src/shared/resource-meta.ts +14 -0
- package/src/shared/xml-injection.ts +9 -9
- package/workflows/README.md +9 -9
- package/workflows/chain.js +4 -2
- package/workflows/map-reduce.js +5 -3
- package/workflows/parallel.js +5 -3
- package/workflows/review-fix-loop-utils.cjs +14 -3
- package/workflows/review-fix-loop.js +18 -7
- package/workflows/scatter-gather.js +4 -2
- package/dist/chunk-3VOERJPJ.js +0 -22
|
@@ -40,17 +40,27 @@ export interface ActivePendingResult {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* [perf] per-file 增量游标:session 文件 append-only,同文件重复判定(层主被多个
|
|
43
|
-
* 后代唤醒 N 次 → N 次 agent_end)只需读上次 offset
|
|
44
|
-
*
|
|
45
|
-
* 「首读全量 + 后续增量」。
|
|
43
|
+
* 后代唤醒 N 次 → N 次 agent_end)只需读上次 offset 之后的新增行。fork 继承的大
|
|
44
|
+
* session(数十 MB)从「每次整读」降为「首读全量 + 后续增量」。
|
|
46
45
|
* truncate/重建防御:size < offset → 重置全读。EOF 半行(写入竞态)不入账,
|
|
47
46
|
* offset 只推进到完整行边界,下次补读。
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
*
|
|
48
|
+
* [LC-6/T6②] entries 只留 register−unregister 差集的**活跃**条目(activeRegisters
|
|
49
|
+
* Map,id → 原始 register entry),不累积全量历史 pending 行——长寿命 orchestrator
|
|
50
|
+
* 的内存从「随 pending 行总数无界涨」收敛为「随活跃后代数有界」(差集化同时把每次
|
|
51
|
+
* 判定的端口/list 重扫从 O(历史行数) 降为 O(活跃数))。unregister 抵消已内联完成,
|
|
52
|
+
* 下游 countActiveFromEntries 端口(TTL/跨 session 过滤作用于 register entry 本体)
|
|
53
|
+
* 与 list 差集口径的语义与全量读完全一致;latestUnregisterMs(60s 唤醒窗口判据)
|
|
54
|
+
* 独立在 cursor 字段上,不受差集化影响。缺 data.id 的畸形行丢弃(对齐坏行跳过层级,
|
|
55
|
+
* 契约上 register/unregister 必带 data.id)。
|
|
56
|
+
*
|
|
57
|
+
* 剪枝:文件 stat/read 失败(含删除)即删 cursor(下次成功读时全量重建,增量只是
|
|
58
|
+
* 优化不是事实源);进程 close 后由调用方调 prunePendingCursor 显式回收。
|
|
59
|
+
* 测试隔离用 clearPendingCursors()。
|
|
50
60
|
*/
|
|
51
61
|
interface PendingReadCursor {
|
|
52
62
|
offset: number;
|
|
53
|
-
|
|
63
|
+
activeRegisters: Map<string, unknown>;
|
|
54
64
|
latestUnregisterMs: number;
|
|
55
65
|
}
|
|
56
66
|
|
|
@@ -61,6 +71,17 @@ export function clearPendingCursors(): void {
|
|
|
61
71
|
cursors.clear();
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
/**
|
|
75
|
+
* [LC-6/T6②] 剪枝单个 sessionFile 的增量游标(子进程 close / session 终态化时调)。
|
|
76
|
+
*
|
|
77
|
+
* 进程死后其 sessionFile 不再有 agent_end 判定,cursor 条目(offset + 活跃后代 Map)
|
|
78
|
+
* 只会滞留不再更新——进程退出路径调用本函数回收。文件删除侧的自动剪枝在
|
|
79
|
+
* accumulatePendingEntries(stat/read 失败即删)。不存在时 no-op。
|
|
80
|
+
*/
|
|
81
|
+
export function prunePendingCursor(sessionFile: string): void {
|
|
82
|
+
cursors.delete(sessionFile);
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
/** [taste/no-unsafe-cast] pending 行的最小结构守卫:非 null object 即可(字段访问
|
|
65
86
|
* 侧均做了 undefined 检查,全可选类型断言无校验意义,改守卫带运行时检查)。 */
|
|
66
87
|
function isPendingLineLike(v: unknown): v is { customType?: string; timestamp?: string } {
|
|
@@ -68,7 +89,10 @@ function isPendingLineLike(v: unknown): v is { customType?: string; timestamp?:
|
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
/**
|
|
71
|
-
* 读 session
|
|
92
|
+
* [内部共享] 读 session 文件并把 pending register/unregister 行累积进 per-file 增量
|
|
93
|
+
* 游标的**活跃差集**(见 PendingReadCursor)。readActivePendingFromSessionFile(count
|
|
94
|
+
* 口径)与 listActivePendingFromSessionFile(清单口径,T2-②)共用同一 cursor——两个
|
|
95
|
+
* 口径交错调用同一文件时差集不错位、不重复读已消费区间。
|
|
72
96
|
*
|
|
73
97
|
* 快速路径:行内含 pending 值(`"pending:register"` / `"pending:unregister"`)才解析,
|
|
74
98
|
* 大量 message 行只付 includes 扫描跳过 JSON.parse。
|
|
@@ -77,58 +101,31 @@ function isPendingLineLike(v: unknown): v is { customType?: string; timestamp?:
|
|
|
77
101
|
* 静默失效 → recursive tree 被杀、steer 丢失。值字符串本身不受序列化空格影响。
|
|
78
102
|
*
|
|
79
103
|
* 文件不存在(sessionFile 未回填/首次 assistant 前)→ error(调用方保守不 kill)。
|
|
80
|
-
*
|
|
104
|
+
* [LC-6] stat/read 失败(含文件被删)剪枝 cursor——下次成功读时全量重建(增量只是
|
|
105
|
+
* 优化不是事实源),防「已删文件 cursor 永久滞留」。坏行跳过(append 中途崩溃的截断行)。
|
|
81
106
|
*/
|
|
82
|
-
|
|
107
|
+
function accumulatePendingEntries(
|
|
83
108
|
sessionFile: string | undefined,
|
|
84
|
-
):
|
|
109
|
+
): { activeRegisters: Map<string, unknown>; latestUnregisterMs: number; error?: string } {
|
|
110
|
+
const emptyAcc = { activeRegisters: new Map<string, unknown>(), latestUnregisterMs: 0 };
|
|
85
111
|
if (!sessionFile) {
|
|
86
|
-
return {
|
|
112
|
+
return { ...emptyAcc, error: "no sessionFile (handshake not settled)" };
|
|
87
113
|
}
|
|
88
114
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
} catch (err) {
|
|
93
|
-
return {
|
|
94
|
-
count: 0,
|
|
95
|
-
recentUnregister: false,
|
|
96
|
-
error: `session file unreadable: ${err instanceof Error ? err.message : String(err)}`,
|
|
97
|
-
};
|
|
115
|
+
const size = statPendingFileSize(sessionFile);
|
|
116
|
+
if (typeof size !== "number") {
|
|
117
|
+
return { ...emptyAcc, error: size.error };
|
|
98
118
|
}
|
|
99
119
|
|
|
100
120
|
let cursor = cursors.get(sessionFile);
|
|
101
121
|
if (cursor === undefined || size < cursor.offset) {
|
|
102
122
|
// 首次(全量)或文件被 truncate/重建(offset 越界)→ 重置从头读
|
|
103
|
-
cursor = { offset: 0,
|
|
123
|
+
cursor = { offset: 0, activeRegisters: new Map<string, unknown>(), latestUnregisterMs: 0 };
|
|
104
124
|
}
|
|
105
125
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
chunk = fs.readFileSync(sessionFile, "utf-8");
|
|
110
|
-
} else {
|
|
111
|
-
const fd = fs.openSync(sessionFile, "r");
|
|
112
|
-
try {
|
|
113
|
-
const len = size - cursor.offset;
|
|
114
|
-
const buf = Buffer.alloc(len);
|
|
115
|
-
let total = 0;
|
|
116
|
-
while (total < len) {
|
|
117
|
-
const n = fs.readSync(fd, buf, total, len - total, cursor.offset + total);
|
|
118
|
-
if (n <= 0) break;
|
|
119
|
-
total += n;
|
|
120
|
-
}
|
|
121
|
-
chunk = buf.toString("utf-8", 0, total);
|
|
122
|
-
} finally {
|
|
123
|
-
fs.closeSync(fd);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
} catch (err) {
|
|
127
|
-
return {
|
|
128
|
-
count: 0,
|
|
129
|
-
recentUnregister: false,
|
|
130
|
-
error: `session file unreadable: ${err instanceof Error ? err.message : String(err)}`,
|
|
131
|
-
};
|
|
126
|
+
const chunk = readPendingChunk(sessionFile, cursor, size);
|
|
127
|
+
if (typeof chunk !== "string") {
|
|
128
|
+
return { ...emptyAcc, error: chunk.error };
|
|
132
129
|
}
|
|
133
130
|
|
|
134
131
|
// 只消费到最后一个完整行:EOF 半行(append 写入竞态)不入账,offset 不推进,
|
|
@@ -137,34 +134,188 @@ export function readActivePendingFromSessionFile(
|
|
|
137
134
|
const complete = lastNl === -1 ? "" : chunk.slice(0, lastNl);
|
|
138
135
|
cursor.offset += Buffer.byteLength(complete, "utf-8");
|
|
139
136
|
cursors.set(sessionFile, cursor);
|
|
137
|
+
consumePendingLines(complete, cursor, sessionFile);
|
|
138
|
+
|
|
139
|
+
return { activeRegisters: cursor.activeRegisters, latestUnregisterMs: cursor.latestUnregisterMs };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** stat + 失败剪枝的统一文案(stat/read 两路 [LC-6] 剪枝共用)。 */
|
|
143
|
+
function pendingFileUnreadableMessage(err: unknown): string {
|
|
144
|
+
return `session file unreadable: ${err instanceof Error ? err.message : String(err)}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** stat 文件大小;失败(含 ENOENT 文件被删)剪枝 cursor 并返回错误(防已删文件 cursor 永久滞留)。 */
|
|
148
|
+
function statPendingFileSize(sessionFile: string): number | { error: string } {
|
|
149
|
+
try {
|
|
150
|
+
return fs.statSync(sessionFile).size;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
cursors.delete(sessionFile);
|
|
153
|
+
return { error: pendingFileUnreadableMessage(err) };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 按 cursor.offset 增量读 chunk(首次全量);读失败剪枝 cursor(同 stat 失败路径)。 */
|
|
158
|
+
function readPendingChunk(
|
|
159
|
+
sessionFile: string,
|
|
160
|
+
cursor: PendingReadCursor,
|
|
161
|
+
size: number,
|
|
162
|
+
): string | { error: string } {
|
|
163
|
+
try {
|
|
164
|
+
if (cursor.offset === 0) {
|
|
165
|
+
return fs.readFileSync(sessionFile, "utf-8");
|
|
166
|
+
}
|
|
167
|
+
return readPendingChunkFrom(sessionFile, cursor.offset, size - cursor.offset);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
cursors.delete(sessionFile);
|
|
170
|
+
return { error: pendingFileUnreadableMessage(err) };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** offset 起的定长读(短读循环补齐到 EOF);异常由 readPendingChunk 的 catch 统一接。 */
|
|
175
|
+
function readPendingChunkFrom(sessionFile: string, offset: number, len: number): string {
|
|
176
|
+
const buf = Buffer.alloc(len);
|
|
177
|
+
const fd = fs.openSync(sessionFile, "r");
|
|
178
|
+
try {
|
|
179
|
+
let total = 0;
|
|
180
|
+
while (total < len) {
|
|
181
|
+
const n = fs.readSync(fd, buf, total, len - total, offset + total);
|
|
182
|
+
if (n <= 0) break;
|
|
183
|
+
total += n;
|
|
184
|
+
}
|
|
185
|
+
return buf.toString("utf-8", 0, total);
|
|
186
|
+
} finally {
|
|
187
|
+
fs.closeSync(fd);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
140
190
|
|
|
191
|
+
/** 完整行逐条入账差集(值匹配快速路径 + 坏行跳过,见 accumulatePendingEntries 的 [S-4] 注)。 */
|
|
192
|
+
function consumePendingLines(complete: string, cursor: PendingReadCursor, sessionFile: string): void {
|
|
141
193
|
for (const line of complete.split("\n")) {
|
|
142
194
|
// [S-4] 按值匹配:countActiveFromEntries 只消费 register/unregister 两种 customType,
|
|
143
195
|
// 故只需检测这两个值字符串;冒号前后空格变化不影响(值始终是连续子串)。
|
|
144
196
|
if (!line.includes('"pending:register"') && !line.includes('"pending:unregister"')) continue;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
197
|
+
applyPendingLine(line, cursor, sessionFile);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 单行 register/unregister 入账(差集内联 + latestUnregister 窗口判据;坏行 catch 跳过)。 */
|
|
202
|
+
function applyPendingLine(line: string, cursor: PendingReadCursor, sessionFile: string): void {
|
|
203
|
+
try {
|
|
204
|
+
const entry: unknown = JSON.parse(line);
|
|
205
|
+
if (!isPendingLineLike(entry)) return;
|
|
206
|
+
// [LC-6] 差集内联:register 入活跃 Map(同 id 重 register 覆盖 = 差集语义),
|
|
207
|
+
// unregister 抵消移除。缺 data.id 的畸形行丢弃(契约必带 id,对齐坏行跳过层级)。
|
|
208
|
+
const customType = entry.customType;
|
|
209
|
+
const id = extractPendingEntryId((entry as { data?: unknown }).data);
|
|
210
|
+
if (customType === "pending:register" && id !== undefined) {
|
|
211
|
+
cursor.activeRegisters.set(id, entry);
|
|
212
|
+
} else if (customType === "pending:unregister" && id !== undefined) {
|
|
213
|
+
cursor.activeRegisters.delete(id);
|
|
156
214
|
}
|
|
215
|
+
if (customType === "pending:unregister" && entry.timestamp) {
|
|
216
|
+
const ts = Date.parse(entry.timestamp);
|
|
217
|
+
if (Number.isFinite(ts) && ts > cursor.latestUnregisterMs) cursor.latestUnregisterMs = ts;
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
// 截断行/坏行跳过——不影响其余 entry 的差集判定(罕见:append 中途崩溃)
|
|
221
|
+
logger.debug("skipped malformed pending line", { sessionFile });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** entry.data.id 的运行时守卫提取([taste/no-unsafe-cast] 字段访问前校验)。 */
|
|
226
|
+
function extractPendingEntryId(data: unknown): string | undefined {
|
|
227
|
+
return typeof data === "object" && data !== null && typeof (data as { id?: unknown }).id === "string"
|
|
228
|
+
? (data as { id: string }).id
|
|
229
|
+
: undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 读 session 文件计算活跃 pending 数(增量读 + 差集,见 PendingReadCursor)。
|
|
234
|
+
*/
|
|
235
|
+
export function readActivePendingFromSessionFile(
|
|
236
|
+
sessionFile: string | undefined,
|
|
237
|
+
): ActivePendingResult {
|
|
238
|
+
const acc = accumulatePendingEntries(sessionFile);
|
|
239
|
+
if (acc.error) {
|
|
240
|
+
return { count: 0, recentUnregister: false, error: acc.error };
|
|
157
241
|
}
|
|
158
242
|
|
|
159
243
|
// 计数器经通知域窄端口解析(缺省实现恒 0 = 零活跃,pending 门全开——安全侧缺省
|
|
160
244
|
// 收敛在端口层,见 core/notify-ports.ts DEFAULT_NOTIFY_PORTS;此处 `?? 0` 仅防御
|
|
161
|
-
// 宿主注入部分端口对象的形态)。
|
|
245
|
+
// 宿主注入部分端口对象的形态)。[LC-6] 入参是差集后的活跃 register 集合(unregister
|
|
246
|
+
// 抵消已内联),端口语义(TTL/跨 session 过滤作用于 register entry 本体)不受影响。
|
|
162
247
|
const countActive = getNotifyDomainPorts().countActiveFromEntries;
|
|
163
|
-
const active = countActive ? countActive(
|
|
248
|
+
const active = countActive ? countActive([...acc.activeRegisters.values()]) : 0;
|
|
164
249
|
return {
|
|
165
250
|
count: active,
|
|
166
251
|
recentUnregister:
|
|
167
|
-
|
|
168
|
-
Date.now() -
|
|
252
|
+
acc.latestUnregisterMs > 0 &&
|
|
253
|
+
Date.now() - acc.latestUnregisterMs < RECENT_UNREGISTER_WINDOW_MS,
|
|
169
254
|
};
|
|
170
255
|
}
|
|
256
|
+
|
|
257
|
+
/** 活跃 pending 清单条目(T2-② 后代补杀的 id/sessionId 线索)。 */
|
|
258
|
+
export interface ActivePendingItem {
|
|
259
|
+
/** pending 操作 id(register − unregister 差集的键,id 全局唯一)。 */
|
|
260
|
+
id: string;
|
|
261
|
+
/**
|
|
262
|
+
* 注册时的后代 pi session id(pending:register entry data.sessionId)——
|
|
263
|
+
* 反查后代 sessionFile 的线索。缺失(异常形态)时 undefined,调用方降级处理。
|
|
264
|
+
*/
|
|
265
|
+
sessionId: string | undefined;
|
|
266
|
+
/** 操作来源类型(workflow / session / process),诊断用。 */
|
|
267
|
+
type: string | undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** listActivePendingFromSessionFile 的结果(error 非空时 items 为空)。 */
|
|
271
|
+
export interface ActivePendingListResult {
|
|
272
|
+
items: ActivePendingItem[];
|
|
273
|
+
/** 读取/解析失败的原因(undefined = 成功)。 */
|
|
274
|
+
error?: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** pending:register/unregister entry 的 data 最小形状(对齐 pending-notifications
|
|
278
|
+
* extension 的 RegisterEntryData/UnregisterEntryData:id 均在 data.id)。 */
|
|
279
|
+
interface PendingEntryDataLike {
|
|
280
|
+
id?: unknown;
|
|
281
|
+
sessionId?: unknown;
|
|
282
|
+
type?: unknown;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** entry.data 的运行时守卫([taste/no-unsafe-cast]:字段访问前校验)。 */
|
|
286
|
+
function isPendingEntryDataLike(v: unknown): v is PendingEntryDataLike {
|
|
287
|
+
return typeof v === "object" && v !== null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* [T2-② / P-T2b 主路径] 读 session 文件列出活跃 pending 清单(register − unregister
|
|
292
|
+
* 差集,按 data.id)。与 readActivePendingFromSessionFile 共享 per-file 增量游标的
|
|
293
|
+
* 活跃差集(count 与 list 交错调用不错位)。
|
|
294
|
+
*
|
|
295
|
+
* 与 count 口径的两点刻意差异(后代补杀语境):
|
|
296
|
+
* 1. 不经 countActiveFromEntries 端口——补杀需要「谁还活着」的 id/sessionId 线索,
|
|
297
|
+
* 端口只给数量(差集本身已由游标内联完成,此处直接遍历活跃 register)。
|
|
298
|
+
* 2. 不套用端口的 TTL/跨 session 过期语义——层主死后,TTL 过期或跨 session 的后代
|
|
299
|
+
* 同样是无人管理的孤儿进程,列入补杀正是本函数的目的而非误杀。
|
|
300
|
+
*/
|
|
301
|
+
export function listActivePendingFromSessionFile(
|
|
302
|
+
sessionFile: string | undefined,
|
|
303
|
+
): ActivePendingListResult {
|
|
304
|
+
const acc = accumulatePendingEntries(sessionFile);
|
|
305
|
+
if (acc.error) {
|
|
306
|
+
return { items: [], error: acc.error };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const items: ActivePendingItem[] = [];
|
|
310
|
+
for (const [id, raw] of acc.activeRegisters) {
|
|
311
|
+
if (!isPendingLineLike(raw)) continue;
|
|
312
|
+
const data = (raw as { data?: unknown }).data;
|
|
313
|
+
if (!isPendingEntryDataLike(data)) continue;
|
|
314
|
+
items.push({
|
|
315
|
+
id,
|
|
316
|
+
sessionId: typeof data.sessionId === "string" ? data.sessionId : undefined,
|
|
317
|
+
type: typeof data.type === "string" ? data.type : undefined,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return { items };
|
|
321
|
+
}
|