@dtlabs/ai-toolkit 0.17.2 → 0.17.3
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/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.17.3](https://github.com/dtlab-tech/ai-toolkit/compare/v0.17.2...v0.17.3) (2026-10-12)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
* **execution-ledger:** close PID-reuse gap in lock-staleness liveness check ([4a9a365](https://github.com/dtlab-tech/ai-toolkit/commit/4a9a3652bf8e7844accff46851b2e1286e24c0d7))
|
|
8
|
+
* **task-executor:** session-list clutter and PID-reuse lock-liveness gap ([e000a6f](https://github.com/dtlab-tech/ai-toolkit/commit/e000a6faa2e75eac5bb65069eb9b37b102a14cf8))
|
|
9
|
+
* **task-executor:** stop every dispatch from cluttering the Claude Code session list ([8676d66](https://github.com/dtlab-tech/ai-toolkit/commit/8676d66157bceeafb7fa225cabeb8a666dbac955))
|
|
10
|
+
|
|
2
11
|
## [0.17.2](https://github.com/dtlab-tech/ai-toolkit/compare/v0.17.1...v0.17.2) (2026-10-12)
|
|
3
12
|
|
|
4
13
|
|
package/lib/execution-ledger.js
CHANGED
|
@@ -4,6 +4,10 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const crypto = require('crypto');
|
|
7
|
+
// Namespaced (not destructured) so tests can jest.spyOn(ownership, '_queryProcessStartTimeSync')
|
|
8
|
+
// — a destructured reference would capture the original function at require
|
|
9
|
+
// time and bypass any later spy on the module's own export.
|
|
10
|
+
const ownership = require('./task-executor/ownership');
|
|
7
11
|
|
|
8
12
|
// ── Internal I/O helpers (stubs — implemented by INFRA-TASK-BE-03) ──────────
|
|
9
13
|
|
|
@@ -131,6 +135,78 @@ function _sleepSync(ms) {
|
|
|
131
135
|
while (Date.now() < end) { /* busy-wait for synchronous cross-process backoff */ }
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
// This process's own OS-level start time, probed via ownership.js's
|
|
139
|
+
// host:pid:startTime technique exactly ONCE per process lifetime (not once
|
|
140
|
+
// per lock acquisition) and cached forever after — a process's own start
|
|
141
|
+
// time never changes. Recording this in every lock token we write is what
|
|
142
|
+
// later lets a CONTENDED acquisition attempt tell "the recorded owner is
|
|
143
|
+
// still running" apart from "a recycled pid now belongs to someone else"
|
|
144
|
+
// (see _confirmSameOwnerProcess). `undefined` means not yet probed;
|
|
145
|
+
// `null` means the probe was inconclusive (non-Windows, or it failed) —
|
|
146
|
+
// callers must treat that the same as "no evidence either way", never as
|
|
147
|
+
// proof of death.
|
|
148
|
+
let _selfProcessStartTime;
|
|
149
|
+
function _getSelfProcessStartTime() {
|
|
150
|
+
if (_selfProcessStartTime === undefined) {
|
|
151
|
+
const probe = ownership._queryProcessStartTimeSync(process.pid);
|
|
152
|
+
_selfProcessStartTime = (probe && probe.found) ? probe.startTime : null;
|
|
153
|
+
}
|
|
154
|
+
return _selfProcessStartTime;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Per-nonce verdict cache for the PID-reuse check below. A lock's nonce is
|
|
158
|
+
// unique per acquisition (crypto.randomBytes at write time), so a verdict
|
|
159
|
+
// for a given nonce never goes stale: either the recorded owner is
|
|
160
|
+
// confirmed still the same OS process (forever, until it exits), or it was
|
|
161
|
+
// already confirmed to be a different process that reused the pid
|
|
162
|
+
// (forever — that fact doesn't change either). Caching here is what keeps
|
|
163
|
+
// the expensive OS probe to once per contended lock episode instead of once
|
|
164
|
+
// per ~100ms poll in _acquireLock's retry loop. Bounded defensively; a
|
|
165
|
+
// single process is not expected to ever accumulate more than a handful of
|
|
166
|
+
// distinct contended nonces in practice.
|
|
167
|
+
const _ownerProcessVerdictCache = new Map();
|
|
168
|
+
|
|
169
|
+
// Resolves whether `token.pid` is still the SAME OS process that wrote this
|
|
170
|
+
// lock, using the host:pid:startTime identity technique (same one
|
|
171
|
+
// ownership.js already uses for task leases) — `process.kill(pid, 0)` alone
|
|
172
|
+
// cannot tell a still-alive original owner apart from an unrelated process
|
|
173
|
+
// that reused a recycled pid after the owner crashed.
|
|
174
|
+
//
|
|
175
|
+
// Returns:
|
|
176
|
+
// true — confirmed same process (startTime probes match)
|
|
177
|
+
// false — confirmed a DIFFERENT process now holds this pid (mismatch)
|
|
178
|
+
// null — inconclusive (non-Windows, probe failed, or token predates this
|
|
179
|
+
// field) — caller must fall back to trusting process.kill() alone,
|
|
180
|
+
// exactly like before this check existed; never treated as death.
|
|
181
|
+
function _confirmSameOwnerProcess(token) {
|
|
182
|
+
if (typeof token.processStartTime !== 'string' || !token.nonce) return null;
|
|
183
|
+
|
|
184
|
+
const cached = _ownerProcessVerdictCache.get(token.nonce);
|
|
185
|
+
if (cached !== undefined) return cached;
|
|
186
|
+
|
|
187
|
+
const probe = ownership._queryProcessStartTimeSync(token.pid);
|
|
188
|
+
// Inconclusive probe (non-Windows, or the query failed) or the OS
|
|
189
|
+
// confirms no such pid despite process.kill() saying alive (a narrow
|
|
190
|
+
// race): do not cache — there is nothing stable to remember, and a
|
|
191
|
+
// later retry might get a conclusive answer.
|
|
192
|
+
if (!probe) return null;
|
|
193
|
+
if (!probe.found) return null;
|
|
194
|
+
|
|
195
|
+
const verdict = probe.startTime === token.processStartTime;
|
|
196
|
+
if (_ownerProcessVerdictCache.size > 500) _ownerProcessVerdictCache.clear();
|
|
197
|
+
_ownerProcessVerdictCache.set(token.nonce, verdict);
|
|
198
|
+
return verdict;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Test-only reset: both caches are intentionally process-lifetime-scoped in
|
|
202
|
+
// production (see their own comments for why), which would otherwise leak
|
|
203
|
+
// a mocked _queryProcessStartTimeSync's behavior across unrelated test cases
|
|
204
|
+
// sharing the same Jest worker process.
|
|
205
|
+
function _resetOwnerProcessCaches() {
|
|
206
|
+
_selfProcessStartTime = undefined;
|
|
207
|
+
_ownerProcessVerdictCache.clear();
|
|
208
|
+
}
|
|
209
|
+
|
|
134
210
|
function _acquireLock(lockPath, opts) {
|
|
135
211
|
const deadlineMs = (opts && opts.deadlineMs != null) ? opts.deadlineMs : 5000;
|
|
136
212
|
const retryIntervalMs = (opts && opts.retryIntervalMs != null) ? opts.retryIntervalMs : 100;
|
|
@@ -142,7 +218,12 @@ function _acquireLock(lockPath, opts) {
|
|
|
142
218
|
fd = fs.openSync(lockPath, 'wx');
|
|
143
219
|
const nonce = crypto.randomBytes(16).toString('hex');
|
|
144
220
|
const startedAt = new Date().toISOString();
|
|
145
|
-
|
|
221
|
+
// processStartTime: this process's own OS start time (cached once per
|
|
222
|
+
// process lifetime, see _getSelfProcessStartTime) — null on non-Windows
|
|
223
|
+
// or when the probe is unavailable, matching ownership.js's own
|
|
224
|
+
// null-means-inconclusive convention. Lets a later contended acquirer
|
|
225
|
+
// tell "still us" apart from "a recycled pid, now someone else".
|
|
226
|
+
const ownerToken = { pid: process.pid, startedAt, nonce, processStartTime: _getSelfProcessStartTime() };
|
|
146
227
|
fs.writeSync(fd, JSON.stringify(ownerToken));
|
|
147
228
|
fs.fsyncSync(fd);
|
|
148
229
|
fs.closeSync(fd);
|
|
@@ -270,7 +351,15 @@ function _isLockStale(content, stat) {
|
|
|
270
351
|
}
|
|
271
352
|
}
|
|
272
353
|
|
|
273
|
-
|
|
354
|
+
// process.kill(pid, 0) alone cannot tell a still-alive original owner
|
|
355
|
+
// apart from an unrelated process that reused a recycled pid after the
|
|
356
|
+
// owner crashed. _confirmSameOwnerProcess returns false only when it has
|
|
357
|
+
// positive OS evidence that today's occupant of this pid started at a
|
|
358
|
+
// different time than the token recorded — a confirmed PID reuse, not a
|
|
359
|
+
// guess. true or null (inconclusive) both fall through to the original
|
|
360
|
+
// "trust process.kill()" behavior, so nothing regresses on non-Windows
|
|
361
|
+
// or for locks written before this field existed.
|
|
362
|
+
if (ownerAlive && _confirmSameOwnerProcess(token) !== false) {
|
|
274
363
|
return 'live';
|
|
275
364
|
}
|
|
276
365
|
|
|
@@ -827,6 +916,9 @@ module.exports = {
|
|
|
827
916
|
_acquireLock,
|
|
828
917
|
_releaseLock,
|
|
829
918
|
_isLockStale,
|
|
919
|
+
_confirmSameOwnerProcess,
|
|
920
|
+
_getSelfProcessStartTime,
|
|
921
|
+
_resetOwnerProcessCaches,
|
|
830
922
|
_validateMetadata,
|
|
831
923
|
_extractStoredMetadata,
|
|
832
924
|
_metadataEqual,
|
|
@@ -600,9 +600,16 @@ async function dispatchTaskAttempt(args) {
|
|
|
600
600
|
|
|
601
601
|
// 4. Only now: dispatch the verified agent.
|
|
602
602
|
const prompt = typeof args.prompt === 'string' ? args.prompt : _buildTaskPrompt(task);
|
|
603
|
+
// --no-session-persistence: this dispatch is a one-shot --print invocation
|
|
604
|
+
// that is never resumed (no code path in this codebase passes --resume /
|
|
605
|
+
// --continue / --session-id for a task-executor spawn; replan/resume
|
|
606
|
+
// re-dispatch with a fresh prompt, never reattach to a prior Claude
|
|
607
|
+
// conversation), so a saved, resumable session transcript has no purpose
|
|
608
|
+
// here — only cost: every dispatch would otherwise accumulate as its own
|
|
609
|
+
// entry in the user's Claude Code session list.
|
|
603
610
|
const baseSpawnArgs = Array.isArray(args.spawnArgs)
|
|
604
611
|
? args.spawnArgs
|
|
605
|
-
: ['--print', '--output-format', 'json', '--agent', identity.nativeName].concat(EXECUTOR_PERMISSION_BYPASS_ARGS);
|
|
612
|
+
: ['--print', '--output-format', 'json', '--agent', identity.nativeName, '--no-session-persistence'].concat(EXECUTOR_PERMISSION_BYPASS_ARGS);
|
|
606
613
|
// Append the same processIdentity tag already persisted above as one extra
|
|
607
614
|
// trailing argv element, so it appears verbatim in the spawned process's
|
|
608
615
|
// own command line — this is what makes _findLiveTaggedProcess's later OS
|
|
@@ -905,7 +912,7 @@ async function runReview(args) {
|
|
|
905
912
|
const prompt = _buildReviewPrompt(task, diff);
|
|
906
913
|
const spawnArgs = Array.isArray(args.spawnArgs)
|
|
907
914
|
? args.spawnArgs
|
|
908
|
-
: ['--print', '--output-format', 'json', '--agent', identity.nativeName].concat(EXECUTOR_PERMISSION_BYPASS_ARGS);
|
|
915
|
+
: ['--print', '--output-format', 'json', '--agent', identity.nativeName, '--no-session-persistence'].concat(EXECUTOR_PERMISSION_BYPASS_ARGS);
|
|
909
916
|
const spawnResult = await claudeProcess.spawnClaudeAgent({
|
|
910
917
|
claudePath: args.claudePath,
|
|
911
918
|
args: spawnArgs,
|
|
@@ -587,4 +587,10 @@ module.exports = {
|
|
|
587
587
|
reclaimLease,
|
|
588
588
|
checkOwnerLiveness,
|
|
589
589
|
createOwnershipGuard,
|
|
590
|
+
// Exported for execution-ledger.js's lock-staleness check (US-xx liveness
|
|
591
|
+
// parity): the same host:pid:startTime identity technique proven here is
|
|
592
|
+
// the only reliable defense against a recycled PID being misread as the
|
|
593
|
+
// original lock owner. See execution-ledger.js's own comment at its call
|
|
594
|
+
// site for why this is only invoked lazily/cached there, never per-poll.
|
|
595
|
+
_queryProcessStartTimeSync,
|
|
590
596
|
};
|
package/lib/workflow-control.js
CHANGED
|
@@ -139,7 +139,12 @@ function createControl(options) {
|
|
|
139
139
|
const identity = await resolve(id);
|
|
140
140
|
const definition = definitionFields(read(identity.path));
|
|
141
141
|
return activity(opts.label || id, async () => {
|
|
142
|
-
|
|
142
|
+
// --no-session-persistence: this worker call is a one-shot --print
|
|
143
|
+
// invocation that is never resumed, so a saved session transcript has
|
|
144
|
+
// no purpose — only cost, accumulating as a stray entry in the user's
|
|
145
|
+
// Claude Code session list (see lib/task-executor/index.js's dispatch
|
|
146
|
+
// call sites for the same reasoning).
|
|
147
|
+
const cliArgs = ['--print', '--output-format', 'json', '--agent', identity.nativeName, '--no-session-persistence'];
|
|
143
148
|
if (opts.schema) cliArgs.push('--json-schema', JSON.stringify(opts.schema));
|
|
144
149
|
if (options.workerMaxBudgetUsd != null) cliArgs.push('--max-budget-usd', String(options.workerMaxBudgetUsd));
|
|
145
150
|
const outputs = opts.outputs || [];
|