pi-durable-subagents 1.0.25 → 1.0.27
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 +32 -0
- package/README.md +15 -6
- package/dist/cli/hold.js +17 -6
- package/dist/cli/main.js +1 -1
- package/dist/cli/requests.js +3 -1
- package/dist/events/derive.js +7 -3
- package/dist/orchestrator/executor/session.js +4 -2
- package/dist/platform/lease.js +16 -0
- package/dist/requests.js +4 -0
- package/dist/ui/session.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.27
|
|
4
|
+
|
|
5
|
+
- `hold --no-wait` (or `--max-wait 0`) takes the lease at once or exits 75
|
|
6
|
+
without ever being queued: the decision is made under the resource's lock
|
|
7
|
+
and a refused request writes nothing, so `leases`, `status` and other
|
|
8
|
+
waiters never see it. Before, `--max-wait 0` queued a ticket, checked, and
|
|
9
|
+
removed it. A no-wait request also no longer ends processes left by a
|
|
10
|
+
killed holder (a queued waiter still does); it is refused while they remain.
|
|
11
|
+
- Release check: `pack:smoke` waits for its orchestrator to exit before
|
|
12
|
+
removing its temporary directory, and a failed removal is a warning. The
|
|
13
|
+
1.0.26 release stopped at this step on macOS (`ENOTEMPTY` after
|
|
14
|
+
`pack-smoke ok`), so 1.0.26 was never published; 1.0.27 includes its
|
|
15
|
+
changes.
|
|
16
|
+
|
|
17
|
+
## 1.0.26
|
|
18
|
+
|
|
19
|
+
- A daily quota message in Chinese ("remaining quota is 0, resets at 00:00 the
|
|
20
|
+
next day") is a used-up usage window: a pool call moves to its next candidate
|
|
21
|
+
and a single-model call waits, instead of failing at once as a balance error
|
|
22
|
+
(the word for "balance" occurs inside "remaining quota"). Checked against the 7,941 provider errors
|
|
23
|
+
recorded on a working machine: this was the only usage-window text misread,
|
|
24
|
+
and no rate limit or transient error is read as one.
|
|
25
|
+
- `answered.by` is `call:<wid>/<key>` for a subagent answering through the CLI
|
|
26
|
+
(the CLI now sends its `DSA_CALL` as `caller`). The caller is provenance and
|
|
27
|
+
not part of `spec_digest`: retrying a request id with or without it is the
|
|
28
|
+
same request. A pi session still running an older extension computes the
|
|
29
|
+
digest with the caller included and would see such a retry as a conflict.
|
|
30
|
+
- Tests: an end-to-end pool failover through the CLI and a detached
|
|
31
|
+
orchestrator; the effects fixtures use a unique workflow id, since gate
|
|
32
|
+
processes are found by tag across the whole machine and parallel test files
|
|
33
|
+
shared one; the CI runner also reruns files reported in nested tests.
|
|
34
|
+
|
|
3
35
|
## 1.0.25
|
|
4
36
|
|
|
5
37
|
- `restart` refusals show what a fence would cut short: each lease a running
|
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ the npx cache, so `install-service` refuses to run from there.
|
|
|
55
55
|
| You steer a subagent while it is asking you a question | Your message reaches it, in order. Nothing is rejected or lost. |
|
|
56
56
|
| Two steers arrive out of order and the second replaces the first | Only the second one applies. |
|
|
57
57
|
| A step is refused, or a dependency fails | The workflow stops that branch cleanly. Nothing is retried in vain. |
|
|
58
|
-
| A provider's usage window runs out (`No available accounts`, usage limit, quota exceeded) | Found at the second refusal in a row, while pi is still retrying. A call in a pool continues **in the same session** on the pool's next model (within pi's next retry or two); new calls skip that provider. After 15 minutes the next call that wants it tries it once; when it answers, new calls and new generations use it again. A call with a single model waits for it instead of failing. Billing errors (402, insufficient balance) still fail at once. |
|
|
58
|
+
| A provider's usage window runs out (`No available accounts`, usage limit, quota exceeded, a daily quota at 0) | Found at the second refusal in a row, while pi is still retrying. A call in a pool continues **in the same session** on the pool's next model (within pi's next retry or two); new calls skip that provider. After 15 minutes the next call that wants it tries it once; when it answers, new calls and new generations use it again. A call with a single model waits for it instead of failing. Billing errors (402, insufficient balance) still fail at once. |
|
|
59
59
|
| Two subagents would write in the same worktree | Only one runs there at a time. A call that can write (its tools include `edit` or `write`, which pi's default tools do) holds its git worktree's writer lock from its launch until it ends, also while it waits for an answer. Another writer for that worktree waits in order, and status shows `waiting for writer lock: <root> held by <wid>/<key>`. `writer: false` (a call that does not write there), `isolation: "worktree"` and `"writerLock": "off"` opt out. |
|
|
60
60
|
| A subagent waits for an answer for a long time | It releases its model slot and memory, then resumes exactly once when you answer. The question survives orchestrator restarts (also forced ones) and crashes, including one that hits before the subagent released its slot. |
|
|
61
61
|
| A subagent's work ends (finished, stopped, or cut off) | Every process its tools started ends with that execution, also ones started with `nohup`, `setsid` or `&`: they carry the execution's tag (see the limit below). Anything that must outlive the subagent has to be started by you or the parent session. A command run under `hold` is no exception: a forced restart stops it and its lease is released. |
|
|
@@ -254,7 +254,7 @@ pi-durable-subagents stop-all pause every existing workflow now; journ
|
|
|
254
254
|
pi-durable-subagents prune [wid] [--older-than <days>]
|
|
255
255
|
delete finished workflows (done, failed, stopped); prints count and bytes freed
|
|
256
256
|
pi-durable-subagents restart [--force <token> --reason <text>] switch to the installed version (see "Updating Durable Subagents")
|
|
257
|
-
pi-durable-subagents hold <resource> [--shared] [--max-wait <s>] [--note <text>] -- <command…>
|
|
257
|
+
pi-durable-subagents hold <resource> [--shared] [--max-wait <s> | --no-wait] [--note <text>] -- <command…>
|
|
258
258
|
run one command while holding a resource lease (see below)
|
|
259
259
|
pi-durable-subagents leases [--json] who holds and who waits for each resource
|
|
260
260
|
pi-durable-subagents doctor [--json] read-only health check; exits 1 when something needs you
|
|
@@ -361,9 +361,9 @@ Every event has `id`, `cursor`, `ts` (when the milestone happened), `type`,
|
|
|
361
361
|
|
|
362
362
|
Readers must ignore types they do not know (`waiting`/`moving` follow).
|
|
363
363
|
`by` is the sender of the answer: `session:<id>` for a pi session (with
|
|
364
|
-
`via: "ui"` when it came from the subagent list), `
|
|
365
|
-
|
|
366
|
-
`unknown`. `fenced` is emitted when the call's next execution begins (right
|
|
364
|
+
`via: "ui"` when it came from the subagent list), `call:<wid>/<key>` for a
|
|
365
|
+
subagent answering through the CLI (its `DSA_CALL`; provenance, not
|
|
366
|
+
authority), `cli:<user>@<host>` for any other CLI use, else `unknown`. `fenced` is emitted when the call's next execution begins (right
|
|
367
367
|
after recovery, before it waits for a slot) and only when the fence
|
|
368
368
|
interrupted work, exactly as `describe`'s `lastFence`: a turn that had ended,
|
|
369
369
|
a hibernated question, an answer's resume or a seal are no `fenced`. A `once`
|
|
@@ -473,6 +473,14 @@ pi-durable-subagents hold machine --max-wait 600 --note "profile" -- ./measure.s
|
|
|
473
473
|
everything before it, and keeps later shared requests out (no starvation).
|
|
474
474
|
A waiting `hold` prints who holds the resource; `--max-wait` gives up with
|
|
475
475
|
exit 75 without running the command.
|
|
476
|
+
- `--no-wait` (same as `--max-wait 0`) takes the lease now or not at all.
|
|
477
|
+
It decides under the resource's lock. If it can run now, its request is
|
|
478
|
+
written already granted. Otherwise nothing is written, and it exits 75
|
|
479
|
+
naming who holds or waits, without running the command. It is never
|
|
480
|
+
listed as a waiter, even for a moment, so it can probe a resource whose
|
|
481
|
+
owner treats any queued request as interference. It also leaves the
|
|
482
|
+
resource alone: unlike a queued waiter, it does not end processes left
|
|
483
|
+
by a holder whose `hold` was killed, and is refused while they remain.
|
|
476
484
|
- The command runs without a shell (write `-- sh -c '…'` for one) in its
|
|
477
485
|
own process group; signals to `hold` go to it and its exit status is
|
|
478
486
|
returned. When it exits, whatever it left in its process group is ended
|
|
@@ -514,7 +522,8 @@ State lives in `~/.pi/durable-subagents`; set `DSA_HOME` to move it.
|
|
|
514
522
|
}
|
|
515
523
|
```
|
|
516
524
|
|
|
517
|
-
- **Pools:** a model can name a pool
|
|
525
|
+
- **Pools:** a model can name a pool (the `model` of a call, a `run --spec`
|
|
526
|
+
file or a model send). The first candidate with a free slot is
|
|
518
527
|
used, and a candidate that keeps failing is skipped for 10 minutes. The
|
|
519
528
|
order is the preference: list the provider you want to use first.
|
|
520
529
|
- **A used-up provider** is not sent new calls until its next try, 15 minutes
|
package/dist/cli/hold.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// `pi-durable-subagents hold <resource> [--shared] [--max-wait <s>] [--note <text>] -- <command> [args…]`
|
|
2
2
|
// Waits for the lease (strict FIFO), runs the command in its own process group, ends what the command left in that
|
|
3
|
-
// group, then releases.
|
|
3
|
+
// group, then releases. `--max-wait 0` (or `--no-wait`) takes the lease at once or exits 75 without ever being queued.
|
|
4
|
+
// See src/platform/lease.ts for the ticket protocol.
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
5
6
|
import { watch } from "node:fs";
|
|
6
7
|
import { constants } from "node:os";
|
|
7
8
|
import { dsaHome } from "../paths.js";
|
|
8
9
|
import { captureStart } from "../platform/proctable.js";
|
|
9
|
-
import { blockers, enqueue, groupAlive, leaseDir, liveTickets, orphaned, removeTicket, RESOURCE, who, writeTicket } from "../platform/lease.js";
|
|
10
|
-
export const HOLD_USAGE = "usage: pi-durable-subagents hold <resource> [--shared] [--max-wait <seconds>] [--note <text>] -- <command> [args…]";
|
|
10
|
+
import { blockers, enqueue, groupAlive, tryGrant, leaseDir, liveTickets, orphaned, removeTicket, RESOURCE, who, writeTicket } from "../platform/lease.js";
|
|
11
|
+
export const HOLD_USAGE = "usage: pi-durable-subagents hold <resource> [--shared] [--max-wait <seconds> | --no-wait] [--note <text>] -- <command> [args…]";
|
|
11
12
|
/** Exit status when --max-wait expires before the lease is granted (EX_TEMPFAIL). */
|
|
12
13
|
export const WAIT_EXPIRED = 75;
|
|
13
14
|
export function parseHold(args) {
|
|
@@ -20,6 +21,8 @@ export function parseHold(args) {
|
|
|
20
21
|
const a = opts[i];
|
|
21
22
|
if (a === "--shared")
|
|
22
23
|
mode = "shared";
|
|
24
|
+
else if (a === "--no-wait")
|
|
25
|
+
maxWaitMs = 0;
|
|
23
26
|
else if (a === "--max-wait" || a === "--note") {
|
|
24
27
|
const v = opts[++i];
|
|
25
28
|
if (v === undefined)
|
|
@@ -54,13 +57,21 @@ const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
|
54
57
|
export async function hold(args, options = {}) {
|
|
55
58
|
const env = options.env ?? process.env, home = dsaHome(env), say = options.stderr ?? (l => process.stderr.write(`${l}\n`));
|
|
56
59
|
const pollMs = options.pollMs ?? 250, graceMs = options.graceMs ?? 2000;
|
|
57
|
-
const
|
|
60
|
+
const request = {
|
|
58
61
|
resource: args.resource, mode: args.mode,
|
|
59
62
|
wrapper: { pid: process.pid, start: (await captureStart(process.pid)) || undefined },
|
|
60
63
|
argv: args.argv.map(a => a.length > 200 ? `${a.slice(0, 199)}…` : a).slice(0, 32), cwd: process.cwd(),
|
|
61
64
|
...(args.note ? { note: args.note.slice(0, 300) } : {}), since: Date.now(),
|
|
62
65
|
...(env.DSA_EXEC ? { exec: env.DSA_EXEC } : {}), ...(env.DSA_CALL ? { call: env.DSA_CALL } : {}),
|
|
63
|
-
}
|
|
66
|
+
};
|
|
67
|
+
// No wait: granted now, or refused without a ticket — never listed as a waiter, even for a moment.
|
|
68
|
+
const granted = args.maxWaitMs === 0 ? await tryGrant(home, request) : undefined;
|
|
69
|
+
if (granted && "busy" in granted) {
|
|
70
|
+
const now = Date.now(), by = granted.busy.map(t => `${who(t)} (${t.mode}${t.grantedAt === undefined ? ", waiting" : ""}, ${age(now - (t.grantedAt ?? t.since))})`).join(", ");
|
|
71
|
+
say(`hold: ${args.resource} is not free now (${by}); not running the command (exit ${WAIT_EXPIRED})`);
|
|
72
|
+
return WAIT_EXPIRED;
|
|
73
|
+
}
|
|
74
|
+
const ticket = granted ?? await enqueue(home, request);
|
|
64
75
|
// Signals: while waiting they withdraw the request; while the command runs they go to its process group.
|
|
65
76
|
let child, interrupted, wake = () => { };
|
|
66
77
|
const onSignal = (signal) => {
|
|
@@ -125,7 +136,7 @@ export async function hold(args, options = {}) {
|
|
|
125
136
|
}
|
|
126
137
|
watcher?.close();
|
|
127
138
|
watcher = undefined;
|
|
128
|
-
ticket.grantedAt
|
|
139
|
+
ticket.grantedAt ??= Date.now();
|
|
129
140
|
await writeTicket(home, ticket);
|
|
130
141
|
if (interrupted) {
|
|
131
142
|
removeTicket(home, ticket);
|
package/dist/cli/main.js
CHANGED
|
@@ -132,7 +132,7 @@ export function serviceEntryError(entry) {
|
|
|
132
132
|
return `install-service refuses to run from an npx cache (${entry}); the cache can be pruned and the service would break. Install the CLI with \`npm i -g pi-durable-subagents\` and run \`pi-durable-subagents install-service\` again.`;
|
|
133
133
|
return undefined;
|
|
134
134
|
}
|
|
135
|
-
export const HELP = "pi-durable-subagents: smoke | status [wid] [--json] | events <wid> [--json] | events --all [--since <cursor>] [--limit <n>] [--json] [--wait-ms <n>] | tail [wid] [--json] | start | resume [wid] | drain | stop <wid|callId> | stop-all | run --request <id> --spec <file|-> [--labels <json>] [--cwd <dir>] [--json] [--wait-ms <n>] | send --request <id> --to <run-id|wid/key> [--call <key>] --kind follow-up|answer|steer|model [--qid <qid> --rev <n>] [--message <text|@file>] [--model <m>] [--json] [--wait-ms <n>] | stop --request <id> <run-id|wid|wid/key> [--json] [--wait-ms <n>] | describe --key <id> | describe <wid> [--json] | prune [wid] [--older-than <days>] | restart [--force <token> --reason <text>] | hold <resource> [--shared] [--max-wait <s>] [--note <text>] -- <command…> | leases [--json] | doctor [--json] | install-service [--dry-run] | uninstall-service [--dry-run] | chaos [--scenario <1-9>] [--keep] [--json]";
|
|
135
|
+
export const HELP = "pi-durable-subagents: smoke | status [wid] [--json] | events <wid> [--json] | events --all [--since <cursor>] [--limit <n>] [--json] [--wait-ms <n>] | tail [wid] [--json] | start | resume [wid] | drain | stop <wid|callId> | stop-all | run --request <id> --spec <file|-> [--labels <json>] [--cwd <dir>] [--json] [--wait-ms <n>] | send --request <id> --to <run-id|wid/key> [--call <key>] --kind follow-up|answer|steer|model [--qid <qid> --rev <n>] [--message <text|@file>] [--model <m>] [--json] [--wait-ms <n>] | stop --request <id> <run-id|wid|wid/key> [--json] [--wait-ms <n>] | describe --key <id> | describe <wid> [--json] | prune [wid] [--older-than <days>] | restart [--force <token> --reason <text>] | hold <resource> [--shared] [--max-wait <s> | --no-wait] [--note <text>] -- <command…> | leases [--json] | doctor [--json] | install-service [--dry-run] | uninstall-service [--dry-run] | chaos [--scenario <1-9>] [--keep] [--json]";
|
|
136
136
|
/** Restart: the orchestrator exits when no execution runs (or `force`) and the installed version takes over. */
|
|
137
137
|
async function restartCommand(home, env, write, options) {
|
|
138
138
|
const body = { ...(typeof options.force === "string" ? { token: options.force } : options.force === true ? { force: true } : {}), ...(options.reason !== undefined ? { reason: options.reason } : {}), initiator: cliInitiator(env) };
|
package/dist/cli/requests.js
CHANGED
|
@@ -373,7 +373,9 @@ async function sendRequest(args, ctx, seen) {
|
|
|
373
373
|
const normalized = request({ action: "send", to: where.to, kind, ...(message !== undefined ? { message } : {}), ...(text(values, "model") !== undefined ? { model: text(values, "model") } : {}),
|
|
374
374
|
...(qid !== undefined ? { qid } : {}), ...(revision !== undefined ? { rev: revision } : {}) }, ctx.cwd ?? process.cwd());
|
|
375
375
|
seen.digest = specDigest({ kind: "send", body: normalized.body, cond: normalized.cond });
|
|
376
|
-
|
|
376
|
+
// Inside a subagent the CLI names its call (provenance for `answered.by`; spec_digest ignores it).
|
|
377
|
+
const body = ctx.env.DSA_CALL ? { ...normalized.body, caller: ctx.env.DSA_CALL } : normalized.body;
|
|
378
|
+
const done = await submitAndWait(ctx, seen, id, "send", body, normalized.cond, wait, json);
|
|
377
379
|
if ("code" in done)
|
|
378
380
|
return done.code;
|
|
379
381
|
return decided(ctx, id, done, json);
|
package/dist/events/derive.js
CHANGED
|
@@ -23,11 +23,15 @@ export function labelsOf(body) {
|
|
|
23
23
|
return entries.length && entries.every(([, v]) => typeof v === "string") ? Object.fromEntries(entries) : undefined;
|
|
24
24
|
}
|
|
25
25
|
/** `answered.by` from the answer request's sender: a pi session `main:<id>` → `session:<id>` (+ via "ui" when the
|
|
26
|
-
* subagent list sent it, SendBody.by "user"); the CLI
|
|
26
|
+
* subagent list sent it, SendBody.by "user"); the CLI run inside a subagent (SendBody.caller `<wid>@<rev>/<key>@<gen>`)
|
|
27
|
+
* → `call:<wid>/<key>`; any other CLI sender `cli:<user>@<host>` as is; anything else `unknown`. */
|
|
27
28
|
export function answeredBy(req) {
|
|
28
|
-
const from = req?.from ?? "";
|
|
29
|
+
const from = req?.from ?? "", body = req?.body;
|
|
29
30
|
if (from.startsWith("main:"))
|
|
30
|
-
return { by: `session:${from.slice(5)}`, ...(
|
|
31
|
+
return { by: `session:${from.slice(5)}`, ...(body?.by === "user" ? { via: "ui" } : {}) };
|
|
32
|
+
const caller = typeof body?.caller === "string" ? /^([^/@]+)@\d+\/(.+)@\d+$/.exec(body.caller) : null;
|
|
33
|
+
if (from.startsWith("cli:") && caller)
|
|
34
|
+
return { by: `call:${caller[1]}/${caller[2]}` };
|
|
31
35
|
if (/^cli:[^@]+@.+$/.test(from))
|
|
32
36
|
return { by: from };
|
|
33
37
|
return { by: "unknown" };
|
|
@@ -136,7 +136,9 @@ export function evidence(entries, exec) {
|
|
|
136
136
|
/** Only explicit payment failures are terminal; rate limits, overload and transport errors still retry, and a used-up
|
|
137
137
|
* usage window (`quotaExhausted`) waits for the provider or moves to another one. */
|
|
138
138
|
export function fatalProviderError(text) {
|
|
139
|
-
|
|
139
|
+
// Chinese "balance", but not inside "remaining quota": "remaining quota is 0, resets at 00:00 the next day" is a daily
|
|
140
|
+
// window. CJK text is written as \u escapes (the repository is ASCII-only English).
|
|
141
|
+
return /\b402\b|insufficient[_ ]?(quota|balance|funds)|billing|credit balance|(?<!\u5269)\u4f59\u989d/i.test(text);
|
|
140
142
|
}
|
|
141
143
|
/** A provider's usage window is used up: its requests are refused (and not counted) until the window resets, hours
|
|
142
144
|
* later. Seen as a gateway's `503 No available accounts` once pi's own retries are spent, or a usage-limit message.
|
|
@@ -150,7 +152,7 @@ export function quotaExhausted(text) {
|
|
|
150
152
|
// per minute"): pi's retries and the lost-execution path handle it; it must not take the provider out for minutes.
|
|
151
153
|
if (/rate.?limit|too many requests|request limit|per (second|minute)|\b[RT]PM\b|resets? in \d+ ?(ms|s|secs?|seconds?|minutes?)\b/i.test(text))
|
|
152
154
|
return false;
|
|
153
|
-
return /usage limit|quota (exceeded|exhausted)|exceeded your (current )?(usage|quota)|limit (reached|exceeded)[^.]*resets?\b
|
|
155
|
+
return /usage limit|quota (exceeded|exhausted)|exceeded your (current )?(usage|quota)|limit (reached|exceeded)[^.]*resets?\b|\u989d\u5ea6/i.test(text);
|
|
154
156
|
}
|
|
155
157
|
/** A refusal of the request's content (terms of service, usage or content policy): the same request is refused again,
|
|
156
158
|
* on this provider and usually on another, so it is reported at once instead of retried as a lost execution. */
|
package/dist/platform/lease.js
CHANGED
|
@@ -159,6 +159,16 @@ export function removeTicket(home, t) {
|
|
|
159
159
|
/** Queue a request: number it under the resource's kernel lock and write its ticket before the lock is released, so a
|
|
160
160
|
* later request always sees it. */
|
|
161
161
|
export async function enqueue(home, ticket, lock = new OsLock()) {
|
|
162
|
+
return (await numbered(home, ticket, false, lock));
|
|
163
|
+
}
|
|
164
|
+
/** Take the lease now or not at all (`hold --max-wait 0`): under the resource's kernel lock, either no live ticket
|
|
165
|
+
* blocks it and its ticket is written already granted, or nothing is written and the blockers are returned. A refused
|
|
166
|
+
* request is never visible as a waiter, and a granted one never was one. Granting needs no more than the lock: a later
|
|
167
|
+
* request gets a higher number and waits behind this ticket, and earlier tickets can only disappear. */
|
|
168
|
+
export async function tryGrant(home, ticket, lock = new OsLock()) {
|
|
169
|
+
return numbered(home, ticket, true, lock);
|
|
170
|
+
}
|
|
171
|
+
async function numbered(home, ticket, now, lock) {
|
|
162
172
|
const dir = leaseDir(home, ticket.resource);
|
|
163
173
|
mkdirSync(dir, { recursive: true });
|
|
164
174
|
let handle = await lock.tryAcquire(path.join(dir, ".lock"));
|
|
@@ -171,6 +181,12 @@ export async function enqueue(home, ticket, lock = new OsLock()) {
|
|
|
171
181
|
const last = Number.parseInt(await readFile(counter, "utf8").catch(() => "0"), 10) || 0;
|
|
172
182
|
const highest = readTickets(home, ticket.resource).reduce((m, t) => Math.max(m, t.seq), last);
|
|
173
183
|
const t = { ...ticket, seq: highest + 1 };
|
|
184
|
+
if (now) {
|
|
185
|
+
const busy = blockers(t, liveTickets(home, ticket.resource));
|
|
186
|
+
if (busy.length)
|
|
187
|
+
return { busy };
|
|
188
|
+
t.grantedAt = Date.now();
|
|
189
|
+
}
|
|
174
190
|
await writeFile(`${counter}.tmp`, String(t.seq));
|
|
175
191
|
renameSync(`${counter}.tmp`, counter);
|
|
176
192
|
writeTicketSync(home, t);
|
package/dist/requests.js
CHANGED
|
@@ -29,6 +29,10 @@ export function specDigest(req) {
|
|
|
29
29
|
const { origin: _, ...rest } = body;
|
|
30
30
|
body = rest;
|
|
31
31
|
}
|
|
32
|
+
if (req.kind === 'send' && body && typeof body === 'object' && !Array.isArray(body)) {
|
|
33
|
+
const { caller: _, ...rest } = body;
|
|
34
|
+
body = rest;
|
|
35
|
+
}
|
|
32
36
|
return contentHash({ kind: req.kind, body, cond: req.cond });
|
|
33
37
|
}
|
|
34
38
|
/** The envelope recorded for `rid`: the orchestrator's admitted copy (ledger `request`, kept after prune), else any
|
package/dist/ui/session.js
CHANGED
|
@@ -86,8 +86,8 @@ export function thoughtSummary(text) {
|
|
|
86
86
|
return (headings.at(-1)[1] ?? headings.at(-1)[2]).trim();
|
|
87
87
|
return lastSentence(text);
|
|
88
88
|
}
|
|
89
|
-
const STOPS = new Set([..."
|
|
90
|
-
/** The last match of /[
|
|
89
|
+
const STOPS = new Set([...".!?\u3002\uff01\uff1f"]);
|
|
90
|
+
/** The last match of /[^.!?\u3002\uff01\uff1f]+[.!?\u3002\uff01\uff1f](?=\s|$)/gu without the regex: a long thought with no sentence end made
|
|
91
91
|
* that regex retry from every position (quadratic), which stalled pi's startup on long histories. Such a match is a
|
|
92
92
|
* whole run of non-stop characters followed by one stop that ends the text or precedes whitespace. */
|
|
93
93
|
function lastSentence(text) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-durable-subagents",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.27",
|
|
4
4
|
"description": "Subagents for pi that never lose work and never do it twice. Crash-safe workflows, automatic recovery, and a live view just like the main agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|