pi-session-mcp 0.8.0 → 0.8.2
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/.pi-session-mcp-cli/doctor.js +14 -7
- package/.pi-session-mcp-cli/override-lock.js +125 -0
- package/.pi-session-mcp-cli/setup-platform.js +195 -0
- package/.pi-session-mcp-cli/skill-customize.js +131 -46
- package/.pi-session-mcp-cli/skill-library.js +72 -4
- package/CHANGELOG.md +159 -0
- package/CONTRIBUTING.md +5 -3
- package/README.md +5 -3
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +14 -7
- package/dist/override-lock.d.ts +34 -0
- package/dist/override-lock.js +125 -0
- package/dist/pi-adapter.d.ts +6 -4
- package/dist/sdk-pi-adapter.js +28 -5
- package/dist/session-registry.d.ts +8 -6
- package/dist/session-registry.js +3 -2
- package/dist/setup-ownership.d.ts +4 -1
- package/dist/setup-ownership.js +6 -3
- package/dist/setup-skills.d.ts +46 -1
- package/dist/setup-skills.js +349 -78
- package/dist/setup.js +21 -4
- package/dist/skill-customize.d.ts +19 -1
- package/dist/skill-customize.js +131 -46
- package/dist/skill-library.d.ts +46 -1
- package/dist/skill-library.js +72 -4
- package/docs/acceptance.md +747 -26
- package/docs/adr/0007-profile-skill-selection.md +11 -0
- package/docs/architecture.md +1 -1
- package/docs/client-setup-design.md +157 -24
- package/docs/client-setup.md +38 -7
- package/docs/doctor-design.md +1 -1
- package/docs/installation.md +8 -2
- package/docs/pi-extensions-design.md +11 -0
- package/docs/releasing.md +25 -10
- package/docs/reviews/2026-10-13-post-0.8.1-audit.md +393 -0
- package/docs/roadmap.md +127 -0
- package/docs/skills.md +173 -16
- package/docs/sprints/2026-10-13-s1-correctness.md +355 -0
- package/docs/sprints/2026-10-13-s2-evidence.md +431 -0
- package/docs/sprints/2026-10-13-s3-live-usage.md +267 -0
- package/docs/sprints/2026-10-13-s4-decision-inputs.md +241 -0
- package/docs/threat-model.md +3 -3
- package/docs/tool-contracts.md +20 -2
- package/package.json +1 -1
- package/skills/pi-session-orchestration/SKILL.md +8 -4
|
@@ -59,6 +59,77 @@ export class SkillTreeError extends Error {
|
|
|
59
59
|
this.name = "SkillTreeError";
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
/** A version 2 skill tree fingerprint as it is stored. */
|
|
63
|
+
export const SKILL_TREE_FINGERPRINT_PATTERN = /^v2:[0-9a-f]{64}$/;
|
|
64
|
+
/** A version 1 fingerprint as 0.8.0 stored it: bare SHA-256 hex. */
|
|
65
|
+
export const LEGACY_SKILL_TREE_FINGERPRINT_PATTERN = /^[0-9a-f]{64}$/;
|
|
66
|
+
const FINGERPRINT_DOMAIN = "pi-session-mcp skill tree v2\n";
|
|
67
|
+
function uint32(value) {
|
|
68
|
+
const bytes = Buffer.alloc(4);
|
|
69
|
+
bytes.writeUInt32BE(value);
|
|
70
|
+
return bytes;
|
|
71
|
+
}
|
|
72
|
+
function uint64(value) {
|
|
73
|
+
const bytes = Buffer.alloc(8);
|
|
74
|
+
bytes.writeBigUInt64BE(BigInt(value));
|
|
75
|
+
return bytes;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Version 2: a domain and version tag, the file count, then per file in byte order of the path
|
|
79
|
+
* the length-prefixed UTF-8 path and the length-prefixed content. Every field has an explicit
|
|
80
|
+
* length, so two different trees never share an encoding.
|
|
81
|
+
*/
|
|
82
|
+
export function skillTreeFingerprint(files) {
|
|
83
|
+
const entries = files.map((file) => ({ path: Buffer.from(file.path, "utf8"), bytes: file.bytes }))
|
|
84
|
+
.sort((left, right) => Buffer.compare(left.path, right.path));
|
|
85
|
+
const hash = createHash("sha256").update(FINGERPRINT_DOMAIN, "utf8").update(uint32(entries.length));
|
|
86
|
+
entries.forEach((entry, index) => {
|
|
87
|
+
if (index > 0 && Buffer.compare(entries[index - 1].path, entry.path) === 0)
|
|
88
|
+
throw new SkillTreeError("skill_tree_invalid");
|
|
89
|
+
hash.update(uint32(entry.path.length)).update(entry.path).update(uint64(entry.bytes.length)).update(entry.bytes);
|
|
90
|
+
});
|
|
91
|
+
return `v2:${hash.digest("hex")}`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Version 1, as 0.8.0 computed it: `path NUL content NUL` per file in reading order. File
|
|
95
|
+
* content may contain NUL bytes, so two different trees can share this value.
|
|
96
|
+
*/
|
|
97
|
+
export function legacySkillTreeFingerprint(files) {
|
|
98
|
+
const hash = createHash("sha256");
|
|
99
|
+
for (const file of files)
|
|
100
|
+
hash.update(file.path).update("\0").update(file.bytes).update("\0");
|
|
101
|
+
return hash.digest("hex");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The shipped skills of every release that recorded version 1 fingerprints (0.8.0 only), as
|
|
105
|
+
* trusted references for recognizing such records. Computed from the `v0.8.0` tag and checked
|
|
106
|
+
* against the published npm package.
|
|
107
|
+
*/
|
|
108
|
+
export const RELEASED_SKILL_TREES = Object.freeze([
|
|
109
|
+
Object.freeze({ name: "pi-session-orchestration", release: "0.8.0", legacyFingerprint: "e2a72b558ea14748a4badd0058ff65b8731cc125033a666706b103842784673a", fingerprint: "v2:39edc4c4783dec7f90345b22844a17e5e48b465722bb0f1b20059d6ee51e1523" }),
|
|
110
|
+
Object.freeze({ name: "pi-session-implementation", release: "0.8.0", legacyFingerprint: "5f719b44da266d3f66dd5bf3c18f3e3e04ca7df357e2fe2eedbf0409e6c1bf64", fingerprint: "v2:f8ce75eff83a5c952f6ca51c04e1e8025cb7c2c558c30d692f291a3f6594eb12" }),
|
|
111
|
+
Object.freeze({ name: "pi-session-review", release: "0.8.0", legacyFingerprint: "8cf58ccb8d473a3eedf12ce6b902b1e91dd9e2fef83ad110c4d8e2e6164cb257", fingerprint: "v2:610c0b4829eeb1ce6c9b2000ad73f8e88ca230707eef7f7b1708a80767952bf0" }),
|
|
112
|
+
]);
|
|
113
|
+
/**
|
|
114
|
+
* Whether a version 1 fingerprint from a record proves that `candidate` is the tree it names:
|
|
115
|
+
* a trusted reference, a released shipped tree of this name or one of `references` (such as
|
|
116
|
+
* the current library tree), has exactly this version 1 fingerprint and is byte-identical to
|
|
117
|
+
* `candidate` by its version 2 fingerprint. The version 1 fingerprint alone never suffices.
|
|
118
|
+
*/
|
|
119
|
+
export function provesLegacyFingerprint(name, legacyFingerprint, candidate, references = [], released = RELEASED_SKILL_TREES) {
|
|
120
|
+
if (!LEGACY_SKILL_TREE_FINGERPRINT_PATTERN.test(legacyFingerprint) || !SKILL_TREE_FINGERPRINT_PATTERN.test(candidate.fingerprint))
|
|
121
|
+
return false;
|
|
122
|
+
const trusted = [...released.filter((tree) => tree.name === name), ...references];
|
|
123
|
+
return trusted.some((tree) => tree.legacyFingerprint === legacyFingerprint && tree.fingerprint === candidate.fingerprint);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The version 2 fingerprint of the released shipped skill that a version 1 fingerprint names,
|
|
127
|
+
* or undefined. Only released trees qualify: the current shipped tree could share a version 1
|
|
128
|
+
* fingerprint with an older original without being that original.
|
|
129
|
+
*/
|
|
130
|
+
export function releasedSkillFingerprint(name, legacyFingerprint, released = RELEASED_SKILL_TREES) {
|
|
131
|
+
return released.find((tree) => tree.name === name && tree.legacyFingerprint === legacyFingerprint)?.fingerprint;
|
|
132
|
+
}
|
|
62
133
|
/**
|
|
63
134
|
* Reads the regular files below a skill directory in a stable order. Entries whose name starts
|
|
64
135
|
* with `.` are skipped or rejected (see options). A symbolic link or special file inside the
|
|
@@ -113,8 +184,5 @@ export async function readSkillTree(directory, options = {}) {
|
|
|
113
184
|
await visit(root, "", 0);
|
|
114
185
|
if (!files.some((file) => file.path === "SKILL.md"))
|
|
115
186
|
throw new SkillTreeError("skill_tree_missing");
|
|
116
|
-
|
|
117
|
-
for (const file of files)
|
|
118
|
-
hash.update(file.path).update("\0").update(file.bytes).update("\0");
|
|
119
|
-
return Object.freeze({ files: Object.freeze(files), fingerprint: hash.digest("hex") });
|
|
187
|
+
return Object.freeze({ files: Object.freeze(files), fingerprint: skillTreeFingerprint(files), legacyFingerprint: legacySkillTreeFingerprint(files) });
|
|
120
188
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,165 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.8.2 — 2026-10-13
|
|
6
|
+
|
|
7
|
+
- Fixed: recovery of an interrupted skill update no longer deletes operator
|
|
8
|
+
content in the transaction's `.trash` directory. It deletes that directory
|
|
9
|
+
only when its contents are the complete, unchanged replaced copy (its
|
|
10
|
+
version 2 fingerprint equals the replaced fingerprint in the pending record).
|
|
11
|
+
A partial, modified, added-to, foreign or otherwise unprovable trash, and any
|
|
12
|
+
trash when the record names no usable replaced fingerprint, is retained and
|
|
13
|
+
reported as `skill_recovery_retained`; the pending record, the skill
|
|
14
|
+
directory and the other intermediates stay unchanged. This deliberately
|
|
15
|
+
replaces the automatic deletion of partial trash trees. See [Pi
|
|
16
|
+
skills](docs/skills.md#interrupted-updates). Issue #59.
|
|
17
|
+
- Fixed: `--verify` now reports `unsupported` when the client is unsupported or
|
|
18
|
+
not discoverable (for example its CLI is not on `PATH`) and `inspect_failed`
|
|
19
|
+
when the inspection fails, instead of `registration_not_equivalent`, which
|
|
20
|
+
stays for an absent, divergent or differently fingerprinted registration. A
|
|
21
|
+
failed Doctor ends the target's verify before inspection and the handshake.
|
|
22
|
+
None of these cases mutates a registration or starts the handshake. See
|
|
23
|
+
[client setup](docs/client-setup.md#verify-diagnostics). Issue #56.
|
|
24
|
+
- Fixed: `--dry-run` of a pending version 2 skill transaction now reports
|
|
25
|
+
`skill_recovery_retained` (divergent, exit 1) whenever `--apply` would retain
|
|
26
|
+
an intermediate, using the same read-only classification as the recovery, and
|
|
27
|
+
otherwise `skill_recovery_planned`. Before, it announced
|
|
28
|
+
`skill_recovery_planned` without inspecting the intermediates. A dry-run
|
|
29
|
+
never writes. See [Pi skills](docs/skills.md#interrupted-updates). Issue #60.
|
|
30
|
+
- Changed: the `pi-session-orchestration` skill classifies every documented
|
|
31
|
+
`pi_session_start` error code as operator needed, transient or unknown, by
|
|
32
|
+
the stable code only. A code that is not listed is unknown, and the
|
|
33
|
+
`startup_failed` diagnostics mean that no MCP server is available. The MCP
|
|
34
|
+
tool contract and the error codes are unchanged. Issue #62.
|
|
35
|
+
- Acceptance harness (maintainer script `scripts/multi-session-acceptance.mjs`,
|
|
36
|
+
not part of the npm package and not the package runtime): a run binds the
|
|
37
|
+
contents of the loaded Pi extensions, Pi's npm installation and the
|
|
38
|
+
candidate's skills, including content behind links, and passes the agent
|
|
39
|
+
directory to its server (#66); it binds every Codex skill source, launches
|
|
40
|
+
Codex with the recorded `CODEX_HOME` and refuses a run when any source holds
|
|
41
|
+
a differing controller skill, found by directory or declared name (#67); an
|
|
42
|
+
unreadable or too large bound resource refuses the run and fails the verdict
|
|
43
|
+
with the new code `resourcesUnbound` (#66, #67); and the handshake test skill
|
|
44
|
+
is offered only to the task that needs it, through a separate profile (#68).
|
|
45
|
+
See [acceptance](docs/acceptance.md#multi-session-skill-acceptance).
|
|
46
|
+
- Acceptance harness: a `trio` scenario runs three concurrent sessions from one
|
|
47
|
+
controller, two writers and a read-only reviewer of a frozen fixture, with a
|
|
48
|
+
neighbor abort (#71). `abort` and `trio` now judge every `pi_session_abort`
|
|
49
|
+
call, so an abort that fails or does not target the `wait` session fails the
|
|
50
|
+
run (new code `offTargetAbort`); `abort` is stricter than before (#71).
|
|
51
|
+
Harness tests that built several runs inside one test no longer time out on
|
|
52
|
+
slow macOS runners (#70).
|
|
53
|
+
- Documentation: the Sprint 3 status file
|
|
54
|
+
([`docs/sprints/2026-10-13-s3-live-usage.md`](docs/sprints/2026-10-13-s3-live-usage.md))
|
|
55
|
+
with the first ten-task Level 3 cycle on Pi SDK `1.x`, the R06, R07 and R08
|
|
56
|
+
live records and a macOS runbook; the Sprint 3 section of
|
|
57
|
+
[acceptance](docs/acceptance.md#sprint-3-live-acceptance-2026-10-13).
|
|
58
|
+
- CI: the test step runs with `NODE_OPTIONS=--unhandled-rejections=strict` on
|
|
59
|
+
the existing matrix, the same command that README, [CONTRIBUTING](CONTRIBUTING.md),
|
|
60
|
+
[Releasing](docs/releasing.md) and acceptance now name. Issue #63.
|
|
61
|
+
- Documentation: delivery-status notes on the historical macOS and Pi
|
|
62
|
+
extension plans (#64); acceptance wording that separates targeted
|
|
63
|
+
multi-session evidence from a Level 3 cycle, and one definition of the
|
|
64
|
+
package version, the MCP tool contract and the setup report `schemaVersion`
|
|
65
|
+
in the [tool contracts](docs/tool-contracts.md#versions) (#65); the Sprint 2
|
|
66
|
+
status file ([`docs/sprints/2026-10-13-s2-evidence.md`](docs/sprints/2026-10-13-s2-evidence.md))
|
|
67
|
+
with the Sprint 1 closure and the roadmap status.
|
|
68
|
+
- Documentation: the Sprint 1 status file
|
|
69
|
+
([`docs/sprints/2026-10-13-s1-correctness.md`](docs/sprints/2026-10-13-s1-correctness.md)),
|
|
70
|
+
the roadmap status, and the v0.8.1 post-release evidence carried from the
|
|
71
|
+
release notes into [acceptance](docs/acceptance.md).
|
|
72
|
+
|
|
73
|
+
## 0.8.1 — 2026-10-12
|
|
74
|
+
|
|
75
|
+
- `updatedAt` of a running turn now follows public SDK activity. It is the time
|
|
76
|
+
of the last observed assistant message start, update or end and tool
|
|
77
|
+
execution start, update or end of that turn, and freezes at the terminal
|
|
78
|
+
transition. Before, it advanced only after a completed tool execution or a
|
|
79
|
+
completed assistant message, so a turn of an extension provider with its own
|
|
80
|
+
agent loop, such as `pi-cursor-sdk`, kept `updatedAt == startedAt` while it
|
|
81
|
+
streamed. Streaming is activity, not evidence of finished or useful work, and
|
|
82
|
+
work without a public event stays invisible, so an unchanged `updatedAt` is
|
|
83
|
+
still no proof of a hang. No event content is stored or projected, and the
|
|
84
|
+
answer still comes only from the final assistant message. See [tool
|
|
85
|
+
contracts](docs/tool-contracts.md). Issue #47.
|
|
86
|
+
- Skill tree fingerprints are now version 2 (`v2:` and 64 hex digits), an
|
|
87
|
+
unambiguous encoding with explicit lengths. Version 1 of 0.8.0 hashed
|
|
88
|
+
`path NUL content NUL` and could give two different trees the same value,
|
|
89
|
+
because content may contain NUL bytes. A version 1 value never proves
|
|
90
|
+
identity alone: it counts only when a trusted reference (the shipped skills of
|
|
91
|
+
0.8.0, or for setup's installed copies the current library tree) has that
|
|
92
|
+
value and is byte-identical to the tree in question. Issue #51.
|
|
93
|
+
- Skill customization is now serialized across processes. `--apply
|
|
94
|
+
--customize-skill` takes an exclusive kernel lock on
|
|
95
|
+
`<override dir>/.pi-session-mcp-bases.lock`, re-reads the bases file under the
|
|
96
|
+
lock and replaces it atomically, so two runs for different skills no longer
|
|
97
|
+
lose a base. The wait is bounded at 30 seconds. New codes:
|
|
98
|
+
`skill_customize_lock_busy` and `skill_customize_lock_unavailable`. The
|
|
99
|
+
dry-run takes no lock and writes nothing. The bases file is now schema version
|
|
100
|
+
2; version 1 files are still read, and every write migrates each legacy value
|
|
101
|
+
that names a released 0.8.0 shipped skill. New codes for a legacy base:
|
|
102
|
+
`skill_base_migration_planned` and `skill_base_migrated` (it names the current
|
|
103
|
+
shipped skill), and `skill_base_unverified` (it names no released skill; the
|
|
104
|
+
Doctor warning `skill_override_outdated` says that the base cannot be
|
|
105
|
+
confirmed). `skill_customize_planned` and `skill_customized` now also complete
|
|
106
|
+
a customization whose bases commit failed. See [Pi
|
|
107
|
+
skills](docs/skills.md#concurrent-runs-and-the-override-lock). Issue #50.
|
|
108
|
+
- Setup's installed skill copies. Ownership records of skill copies now hold
|
|
109
|
+
version 2 fingerprints. A record that 0.8.0 wrote is migrated only when a
|
|
110
|
+
trusted reference proves the copy's exact bytes; otherwise dry-run, apply,
|
|
111
|
+
remove and rollback report the new code `skill_legacy_unverified` and change
|
|
112
|
+
neither the copy nor the record. Updates now name their intermediate
|
|
113
|
+
directories after the pending record's transaction id
|
|
114
|
+
(`.pi-session-mcp-<skill>-<transactionId>.staging`, `.backup`, `.trash`), and
|
|
115
|
+
`--apply`, `--remove` and `--rollback` first settle an interrupted update.
|
|
116
|
+
Anything modified or foreign stays and is reported as the new code
|
|
117
|
+
`skill_recovery_retained`. A failed update leaves its staging directory until
|
|
118
|
+
the next run settles it. Directories of 0.8.0 updates are never searched for
|
|
119
|
+
or deleted. When `--apply` settled an interrupted update and needs no further
|
|
120
|
+
update it reports the new code `skill_recovered`, and a dry-run of a pending
|
|
121
|
+
update reports `skill_recovery_planned`. Issues #51 and #52.
|
|
122
|
+
- Upgrading from 0.8.0: run `pi-session-mcp-setup` as a dry-run and then with
|
|
123
|
+
`--apply`. Unchanged copies of the 0.8.0 controller skill are proven against
|
|
124
|
+
the 0.8.0 table and migrated; others stay and report
|
|
125
|
+
`skill_legacy_unverified`. Doctor resolves 0.8.0 bases against the same table,
|
|
126
|
+
and the next `--apply --customize-skill` rewrites the bases file as version 2.
|
|
127
|
+
0.8.0 rejects the new formats as corrupt (the bases file, and the whole
|
|
128
|
+
ownership file once it holds a version 2 skill record), so it never acts on
|
|
129
|
+
them. See [Upgrade from 0.8.0](docs/skills.md#upgrade-from-080) and
|
|
130
|
+
[Downgrade](docs/skills.md#downgrade).
|
|
131
|
+
- The orchestration skill now says that a successful `pi_session_start` does not
|
|
132
|
+
prove that the provider accepts requests, asks for a bounded observation and
|
|
133
|
+
cleanup budget after `abort_timeout`, separates start errors that need the
|
|
134
|
+
operator from ones that can be transient, and describes `updatedAt` as the
|
|
135
|
+
last observed public SDK activity. `docs/acceptance.md` records the v0.8.0
|
|
136
|
+
post-release acceptance and names the method limits of earlier multi-session
|
|
137
|
+
records. Issue #54.
|
|
138
|
+
- The multi-session acceptance harness (`scripts/multi-session-acceptance.mjs`,
|
|
139
|
+
not in the npm package) now ends a run within validated time bounds and
|
|
140
|
+
terminates only processes that provably belong to it (PID and start time). It
|
|
141
|
+
checks scope against a baseline stored outside the worker workspace (working
|
|
142
|
+
tree, index and HEAD, so committed changes count) and adds its own business
|
|
143
|
+
checks to the fixture tests, with Git pinned to the fixture and hooks,
|
|
144
|
+
`fsmonitor` and replace objects switched off. It binds the build, the
|
|
145
|
+
controller and worker skills, the override directory, the configuration, the
|
|
146
|
+
fixture baselines, itself and the operator's own `pi-session-orchestration`
|
|
147
|
+
copies in `~/.claude/skills` and `~/.agents/skills` at `prepare` and rechecks
|
|
148
|
+
them before launch and after the run; a Codex run is refused while the home
|
|
149
|
+
copy differs from the candidate's skill. It records each launch before the
|
|
150
|
+
controller starts, writes each original result once, records reanalyses
|
|
151
|
+
separately, and lists every launch in the evidence, marking one without a
|
|
152
|
+
result. A failed analysis, or a signal to the harness during it, is recorded
|
|
153
|
+
as `analysisFailed`, never passes, still terminates the run's processes and
|
|
154
|
+
makes the CLI exit with 1. `prepare --with-extensions` copies the exact
|
|
155
|
+
extension allowlist of a selected profile for extension providers such as
|
|
156
|
+
Cursor. Each session reports whether a polled running turn showed
|
|
157
|
+
`updatedAt` beyond `startedAt`. See
|
|
158
|
+
[Acceptance](docs/acceptance.md#multi-session-skill-acceptance). Issue #53.
|
|
159
|
+
- A release now travels in the pull request of the change it ships, with the
|
|
160
|
+
version, the changelog heading and the pre-release acceptance record; records
|
|
161
|
+
that need the published release follow with the next change. See
|
|
162
|
+
[Releasing](docs/releasing.md#prepare-a-release).
|
|
163
|
+
|
|
5
164
|
## 0.8.0 — 2026-10-12
|
|
6
165
|
|
|
7
166
|
This is a minor release because the public contract changes, as ADR 0003
|
package/CONTRIBUTING.md
CHANGED
|
@@ -14,12 +14,14 @@ Linux or macOS and Node.js 22.19.0 or newer:
|
|
|
14
14
|
npm ci
|
|
15
15
|
npm run typecheck
|
|
16
16
|
npm run build:cli
|
|
17
|
-
npm test
|
|
17
|
+
NODE_OPTIONS=--unhandled-rejections=strict npm test
|
|
18
18
|
npm run build
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
The strict option makes an unhandled promise rejection fail the test run, and
|
|
22
|
+
CI runs the tests with the same setting. Tests use a fake Pi adapter or a
|
|
23
|
+
scripted model. They need no provider credentials and no network access beyond
|
|
24
|
+
the npm registry.
|
|
23
25
|
|
|
24
26
|
## Rules
|
|
25
27
|
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ It is not the right tool when:
|
|
|
30
30
|
|
|
31
31
|
## Status
|
|
32
32
|
|
|
33
|
-
The package version is **0.8.
|
|
33
|
+
The package version is **0.8.2**.
|
|
34
34
|
|
|
35
35
|
- **Pre-1.0.** The public tool contract is stable within a minor line: a change to tools, fields, states or error codes needs a new minor release.
|
|
36
36
|
- **Linux and macOS**, Node.js 22.19.0 or newer. macOS is tested in CI on macOS 15 and 26 on Apple silicon; Intel Macs run the same code but are untested. Setup acceptance on a real Mac is recorded for Claude Code only; Codex is still pending. See [Acceptance](docs/acceptance.md#status). On other platforms `setup` refuses with `platform_unsupported`.
|
|
@@ -173,6 +173,8 @@ Contract and design:
|
|
|
173
173
|
Maintain:
|
|
174
174
|
|
|
175
175
|
- [Acceptance](docs/acceptance.md)
|
|
176
|
+
- [Maintenance roadmap](docs/roadmap.md)
|
|
177
|
+
- [Post-0.8.1 audit and backlog snapshot](docs/reviews/2026-10-13-post-0.8.1-audit.md)
|
|
176
178
|
- [Releasing](docs/releasing.md)
|
|
177
179
|
- [Contributing](CONTRIBUTING.md)
|
|
178
180
|
- [Security policy](SECURITY.md)
|
|
@@ -183,11 +185,11 @@ Maintain:
|
|
|
183
185
|
npm ci
|
|
184
186
|
npm run typecheck
|
|
185
187
|
npm run build:cli
|
|
186
|
-
npm test
|
|
188
|
+
NODE_OPTIONS=--unhandled-rejections=strict npm test
|
|
187
189
|
npm run build
|
|
188
190
|
```
|
|
189
191
|
|
|
190
|
-
These are the checks that CI runs. Tests use a fake Pi adapter or a scripted model and need no provider credentials. Every direct dependency is pinned exactly, and the lockfile is committed. The project rules for contributors and coding agents are in [AGENTS.md](AGENTS.md).
|
|
192
|
+
These are the checks that CI runs, with the same strict handling of unhandled promise rejections. Tests use a fake Pi adapter or a scripted model and need no provider credentials. Every direct dependency is pinned exactly, and the lockfile is committed. The project rules for contributors and coding agents are in [AGENTS.md](AGENTS.md).
|
|
191
193
|
|
|
192
194
|
The Pi SDK moves quickly, so a nightly canary checks the unchanged sources against the newest published SDK without touching the pin. [Releasing](docs/releasing.md) describes how to update the pin, record a client contract, patch audit advisories and prepare a release.
|
|
193
195
|
|
package/dist/doctor.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AppConfig } from "./config.js";
|
|
2
2
|
import { type ExtensionSpec } from "./extension-spec.js";
|
|
3
|
+
import { type ReleasedSkillTree } from "./skill-library.js";
|
|
3
4
|
export type DoctorSeverity = "ok" | "warning" | "error";
|
|
4
5
|
export type DoctorExitCode = 0 | 1 | 2 | 64;
|
|
5
6
|
export type DoctorCheckId = "node_version_unsupported" | "package_metadata_unreadable" | "build_missing" | "build_stale" | "version_mismatch" | "config_missing" | "config_unreadable" | "config_invalid" | "workspace_missing" | "workspace_not_directory" | "workspace_unreadable" | "workspace_not_writable" | "extension_missing" | "extension_version_mismatch" | "skill_missing" | "skill_unreadable" | "skill_duplicate" | "skill_override_outdated";
|
|
@@ -44,6 +45,8 @@ export interface DoctorDependencies {
|
|
|
44
45
|
readonly packageMetadata?: () => Promise<DoctorPackageMetadata>;
|
|
45
46
|
/** Reads the installed manifest of a pinned extension; never imports extension code. */
|
|
46
47
|
readonly resolveExtension?: (extension: ExtensionSpec) => Promise<string>;
|
|
48
|
+
/** Reference trees that resolve legacy skill bases; defaults to the released shipped skills. */
|
|
49
|
+
readonly releasedSkillTrees?: readonly ReleasedSkillTree[];
|
|
47
50
|
}
|
|
48
51
|
export declare function runDoctor(dependencies?: DoctorDependencies): Promise<DoctorResult>;
|
|
49
52
|
export declare function renderDoctorJson(result: DoctorResult): string;
|
package/dist/doctor.js
CHANGED
|
@@ -6,7 +6,7 @@ import { PiExtensionError } from "./extension-spec.js";
|
|
|
6
6
|
import { readPackageMetadata } from "./package-metadata.js";
|
|
7
7
|
import { MAX_SKILL_FILE_BYTES } from "./skill-catalog.js";
|
|
8
8
|
import { readSkillBases, SkillBasesError } from "./skill-customize.js";
|
|
9
|
-
import { readSkillTree, shippedSkillsDirectory } from "./skill-library.js";
|
|
9
|
+
import { LEGACY_SKILL_TREE_FINGERPRINT_PATTERN, readSkillTree, releasedSkillFingerprint, shippedSkillsDirectory } from "./skill-library.js";
|
|
10
10
|
const defaultFileSystem = { readFile, stat, access };
|
|
11
11
|
const CHECK_ORDER = ["node_version_unsupported", "package_metadata_unreadable", "build_missing", "build_stale", "version_mismatch", "config_missing", "config_unreadable", "config_invalid", "workspace_missing", "workspace_not_directory", "workspace_unreadable", "workspace_not_writable", "extension_missing", "extension_version_mismatch", "skill_missing", "skill_unreadable", "skill_duplicate", "skill_override_outdated"];
|
|
12
12
|
function failure(id, subject, remediation) { return Object.freeze({ id, severity: "error", subject, remediation }); }
|
|
@@ -67,9 +67,10 @@ async function productionBuildInputMtimes(root) {
|
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
69
|
* Compares the shipped skills with the fingerprints recorded when the operator copied them.
|
|
70
|
-
* Reads only the bases file and the shipped files
|
|
70
|
+
* Reads only the bases file and the shipped files, takes no lock and writes nothing: the bases
|
|
71
|
+
* file is replaced atomically, so a read sees either the old or the new content.
|
|
71
72
|
*/
|
|
72
|
-
async function skillOverrideChecks(overrideDirectory, shippedDirectory) {
|
|
73
|
+
async function skillOverrideChecks(overrideDirectory, shippedDirectory, released) {
|
|
73
74
|
if (overrideDirectory === undefined)
|
|
74
75
|
return [];
|
|
75
76
|
let bases;
|
|
@@ -90,9 +91,15 @@ async function skillOverrideChecks(overrideDirectory, shippedDirectory) {
|
|
|
90
91
|
catch {
|
|
91
92
|
continue;
|
|
92
93
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
const command = `pi-session-mcp-setup --apply --customize-skill ${name}`;
|
|
95
|
+
// A legacy base is meaningful only as the version 2 value of the released original it names.
|
|
96
|
+
const recorded = LEGACY_SKILL_TREE_FINGERPRINT_PATTERN.test(base) ? releasedSkillFingerprint(name, base, released) : base;
|
|
97
|
+
if (recorded === undefined)
|
|
98
|
+
checks.push(warning("skill_override_outdated", `skill:${name}`, `the recorded base predates version 2 fingerprints and cannot be confirmed; review the shipped skill, then run ${command}`));
|
|
99
|
+
else
|
|
100
|
+
checks.push(recorded === fingerprint
|
|
101
|
+
? ok("skill_override_outdated", `skill:${name}`)
|
|
102
|
+
: warning("skill_override_outdated", `skill:${name}`, `review the changed shipped skill, then run ${command}`));
|
|
96
103
|
}
|
|
97
104
|
return checks;
|
|
98
105
|
}
|
|
@@ -260,7 +267,7 @@ export async function runDoctor(dependencies = {}) {
|
|
|
260
267
|
}
|
|
261
268
|
}
|
|
262
269
|
if (config)
|
|
263
|
-
checks.push(...await skillOverrideChecks(config.skillOverrideDirectory, shippedSkillsDirectory(dependencies.packageRoot)));
|
|
270
|
+
checks.push(...await skillOverrideChecks(config.skillOverrideDirectory, shippedSkillsDirectory(dependencies.packageRoot), dependencies.releasedSkillTrees));
|
|
264
271
|
const rank = new Map(CHECK_ORDER.map((id, index) => [id, index]));
|
|
265
272
|
checks.sort((a, b) => (rank.get(a.id) ?? 999) - (rank.get(b.id) ?? 999) || a.subject.localeCompare(b.subject));
|
|
266
273
|
const hasError = checks.some((check) => check.severity === "error");
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type StatfsReader } from "./setup-platform.js";
|
|
2
|
+
/**
|
|
3
|
+
* The kernel lock that serializes every read-modify-write of the skill bases file across
|
|
4
|
+
* processes. It lives on a stable lock file, never on the bases file: that file is replaced by a
|
|
5
|
+
* rename, which would move the lock onto an unlinked inode. The lock file is never deleted, and a
|
|
6
|
+
* holder that dies releases the lock through the kernel.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SKILL_BASES_LOCK_FILE = ".pi-session-mcp-bases.lock";
|
|
9
|
+
export declare const DEFAULT_OVERRIDE_LOCK_TIMEOUT_MS = 30000;
|
|
10
|
+
export declare class OverrideLockError extends Error {
|
|
11
|
+
readonly code: "override_lock_busy" | "override_lock_unavailable";
|
|
12
|
+
constructor(code: "override_lock_busy" | "override_lock_unavailable");
|
|
13
|
+
}
|
|
14
|
+
export interface OverrideLockOptions {
|
|
15
|
+
/** Acquisition bound; a busy lock after this fails with `override_lock_busy`. */
|
|
16
|
+
readonly timeoutMs?: number;
|
|
17
|
+
readonly signal?: AbortSignal;
|
|
18
|
+
/** Test seams for the platform gate; the defaults describe this process. */
|
|
19
|
+
readonly platform?: NodeJS.Platform;
|
|
20
|
+
readonly flockCandidates?: readonly string[];
|
|
21
|
+
readonly isTrustedBinary?: (path: string) => boolean;
|
|
22
|
+
readonly readStatfs?: StatfsReader;
|
|
23
|
+
}
|
|
24
|
+
export interface OverrideLock {
|
|
25
|
+
/** Closes the descriptor that holds the lock; safe to call more than once. */
|
|
26
|
+
release(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Takes the exclusive lock for one override directory, which must exist. Fails closed: an
|
|
30
|
+
* unsupported platform, a missing trusted `flock`, an unsupported filesystem, a link or non-file
|
|
31
|
+
* at the lock path and a descriptor that no longer names the path all end in
|
|
32
|
+
* `override_lock_unavailable`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function acquireOverrideLock(overrideDirectory: string, options?: OverrideLockOptions): Promise<OverrideLock>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, open } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { assertSupportedFilesystem, currentSetupPlatform, isTrustedSystemBinary, openLockedDarwin } from "./setup-platform.js";
|
|
6
|
+
/**
|
|
7
|
+
* The kernel lock that serializes every read-modify-write of the skill bases file across
|
|
8
|
+
* processes. It lives on a stable lock file, never on the bases file: that file is replaced by a
|
|
9
|
+
* rename, which would move the lock onto an unlinked inode. The lock file is never deleted, and a
|
|
10
|
+
* holder that dies releases the lock through the kernel.
|
|
11
|
+
*/
|
|
12
|
+
export const SKILL_BASES_LOCK_FILE = ".pi-session-mcp-bases.lock";
|
|
13
|
+
export const DEFAULT_OVERRIDE_LOCK_TIMEOUT_MS = 30_000;
|
|
14
|
+
const MIN_LOCK_TIMEOUT_MS = 50;
|
|
15
|
+
const MAX_LOCK_TIMEOUT_MS = 120_000;
|
|
16
|
+
/** Exit status of `flock -E` when the lock stayed busy for the whole wait. */
|
|
17
|
+
const FLOCK_BUSY_EXIT = 75;
|
|
18
|
+
const FLOCK_CANDIDATES = ["/usr/bin/flock", "/bin/flock"];
|
|
19
|
+
export class OverrideLockError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
constructor(code) {
|
|
22
|
+
super("Skill override lock is not available");
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.name = "OverrideLockError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Takes the exclusive lock for one override directory, which must exist. Fails closed: an
|
|
29
|
+
* unsupported platform, a missing trusted `flock`, an unsupported filesystem, a link or non-file
|
|
30
|
+
* at the lock path and a descriptor that no longer names the path all end in
|
|
31
|
+
* `override_lock_unavailable`.
|
|
32
|
+
*/
|
|
33
|
+
export async function acquireOverrideLock(overrideDirectory, options = {}) {
|
|
34
|
+
const platform = currentSetupPlatform(options.platform);
|
|
35
|
+
if (!platform)
|
|
36
|
+
throw new OverrideLockError("override_lock_unavailable");
|
|
37
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(MIN_LOCK_TIMEOUT_MS, Math.min(options.timeoutMs, MAX_LOCK_TIMEOUT_MS)) : DEFAULT_OVERRIDE_LOCK_TIMEOUT_MS;
|
|
38
|
+
const flockPath = platform === "linux" ? flockBinary(options) : undefined;
|
|
39
|
+
if (platform === "linux" && flockPath === undefined)
|
|
40
|
+
throw new OverrideLockError("override_lock_unavailable");
|
|
41
|
+
try {
|
|
42
|
+
await assertSupportedFilesystem(overrideDirectory, platform, options.readStatfs);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new OverrideLockError("override_lock_unavailable");
|
|
46
|
+
}
|
|
47
|
+
const lockPath = join(overrideDirectory, SKILL_BASES_LOCK_FILE);
|
|
48
|
+
let handle;
|
|
49
|
+
try {
|
|
50
|
+
// macOS takes the kernel lock at open time; on Linux the trusted flock locks this very
|
|
51
|
+
// open file description, so the lock outlives the flock process and ends with this descriptor.
|
|
52
|
+
handle = await openLockFile(lockPath, platform, timeoutMs, options.signal);
|
|
53
|
+
if (flockPath !== undefined)
|
|
54
|
+
await lockWithFlock(flockPath, handle.fd, timeoutMs, options.signal);
|
|
55
|
+
// The path may have been replaced while waiting: the lock only counts on the file it names.
|
|
56
|
+
const [descriptor, linked] = await Promise.all([handle.stat(), lstat(lockPath)]);
|
|
57
|
+
if (!descriptor.isFile() || !linked.isFile() || descriptor.dev !== linked.dev || descriptor.ino !== linked.ino)
|
|
58
|
+
throw new OverrideLockError("override_lock_unavailable");
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
await handle?.close().catch(() => undefined);
|
|
62
|
+
throw error instanceof OverrideLockError ? error : new OverrideLockError("override_lock_unavailable");
|
|
63
|
+
}
|
|
64
|
+
const held = handle;
|
|
65
|
+
let released = false;
|
|
66
|
+
return {
|
|
67
|
+
async release() {
|
|
68
|
+
if (released)
|
|
69
|
+
return;
|
|
70
|
+
released = true;
|
|
71
|
+
await held.close().catch(() => undefined);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function flockBinary(options) {
|
|
76
|
+
const trusted = options.isTrustedBinary ?? ((path) => isTrustedSystemBinary(path));
|
|
77
|
+
return (options.flockCandidates ?? FLOCK_CANDIDATES).find((candidate) => trusted(candidate));
|
|
78
|
+
}
|
|
79
|
+
async function openLockFile(lockPath, platform, timeoutMs, signal) {
|
|
80
|
+
try {
|
|
81
|
+
if (platform === "darwin")
|
|
82
|
+
return await openLockedDarwin(lockPath, { timeoutMs, ...(signal ? { signal } : {}) });
|
|
83
|
+
return await open(lockPath, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw new OverrideLockError(error.message === "ownership_lock_busy" ? "override_lock_busy" : "override_lock_unavailable");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function lockWithFlock(flockPath, lockFd, timeoutMs, signal) {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
if (signal?.aborted) {
|
|
92
|
+
reject(new OverrideLockError("override_lock_unavailable"));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
let locker;
|
|
96
|
+
try {
|
|
97
|
+
// flock parses fractional seconds with the locale's decimal separator, so only whole
|
|
98
|
+
// seconds go on the command line. The exact bound is the timer below, which stops flock.
|
|
99
|
+
const wholeSeconds = String(Math.max(1, Math.ceil(timeoutMs / 1_000)));
|
|
100
|
+
locker = spawn(flockPath, ["-E", String(FLOCK_BUSY_EXIT), "-w", wholeSeconds, "3"], { stdio: ["ignore", "ignore", "ignore", lockFd], detached: true });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
reject(new OverrideLockError("override_lock_unavailable"));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
let expired = false;
|
|
107
|
+
const stop = () => { if (locker.pid !== undefined) {
|
|
108
|
+
try {
|
|
109
|
+
process.kill(-locker.pid, "SIGKILL");
|
|
110
|
+
}
|
|
111
|
+
catch { /* already gone */ }
|
|
112
|
+
} };
|
|
113
|
+
const cleanup = () => { clearTimeout(bound); signal?.removeEventListener("abort", stop); };
|
|
114
|
+
const bound = setTimeout(() => { expired = true; stop(); }, timeoutMs);
|
|
115
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
116
|
+
locker.once("error", () => { cleanup(); reject(new OverrideLockError("override_lock_unavailable")); });
|
|
117
|
+
locker.once("close", (code) => {
|
|
118
|
+
cleanup();
|
|
119
|
+
if (code === 0)
|
|
120
|
+
resolve();
|
|
121
|
+
else
|
|
122
|
+
reject(new OverrideLockError(code === FLOCK_BUSY_EXIT || expired ? "override_lock_busy" : "override_lock_unavailable"));
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
package/dist/pi-adapter.d.ts
CHANGED
|
@@ -23,10 +23,12 @@ export interface PiSessionHandle {
|
|
|
23
23
|
/** Only whitelisted, post-verified selection metadata may cross the adapter boundary. */
|
|
24
24
|
readonly appliedSelection: PiAppliedSelection;
|
|
25
25
|
/**
|
|
26
|
-
* `onActivity` is an optional content-free liveness callback, invoked
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
26
|
+
* `onActivity` is an optional content-free liveness callback, invoked on observed public
|
|
27
|
+
* SDK activity of the running prompt: assistant message start, update and end, and tool
|
|
28
|
+
* execution start, update and end. Streaming is activity, not evidence of finished or
|
|
29
|
+
* useful work, and internal work without a public event stays invisible. It receives no
|
|
30
|
+
* event payload and must only be used for timestamping; it never carries tool names,
|
|
31
|
+
* arguments, results, text, paths, or credentials.
|
|
30
32
|
*/
|
|
31
33
|
prompt(text: string, preflight: (accepted: boolean) => void, onActivity?: () => void): Promise<PiTurnOutcome>;
|
|
32
34
|
abort(): Promise<void>;
|
package/dist/sdk-pi-adapter.js
CHANGED
|
@@ -192,12 +192,31 @@ class SdkHandle {
|
|
|
192
192
|
}
|
|
193
193
|
catch { /* Liveness reporting is best effort and content-free. */ }
|
|
194
194
|
};
|
|
195
|
+
// Activity is the public SDK stream of this prompt: assistant message start, update and end, and
|
|
196
|
+
// tool execution start, update and end. Only the event discriminant and the message role are
|
|
197
|
+
// inspected: delta text, thinking, tool arguments and partial results are never read, stored or
|
|
198
|
+
// forwarded. Streaming is activity, not evidence of finished work; only the last assistant
|
|
199
|
+
// `message_end` is the answer, and the terminal state comes from the completion paths below.
|
|
195
200
|
const unsubscribe = this.session.subscribe((event) => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
switch (event.type) {
|
|
202
|
+
case "tool_execution_start":
|
|
203
|
+
case "tool_execution_update":
|
|
204
|
+
case "tool_execution_end":
|
|
205
|
+
reportActivity();
|
|
206
|
+
break;
|
|
207
|
+
case "message_start":
|
|
208
|
+
case "message_update":
|
|
209
|
+
if (hasAssistantRole(event.message))
|
|
210
|
+
reportActivity();
|
|
211
|
+
break;
|
|
212
|
+
case "message_end":
|
|
213
|
+
if (isAssistantMessage(event.message)) {
|
|
214
|
+
finalAssistant = event.message;
|
|
215
|
+
reportActivity();
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
default:
|
|
219
|
+
break;
|
|
201
220
|
}
|
|
202
221
|
});
|
|
203
222
|
let preflightSeen = false;
|
|
@@ -264,6 +283,10 @@ function isAssistantMessage(message) {
|
|
|
264
283
|
const candidate = message;
|
|
265
284
|
return candidate.role === "assistant" && Array.isArray(candidate.content);
|
|
266
285
|
}
|
|
286
|
+
/** Role check only: streaming messages are never inspected for content. */
|
|
287
|
+
function hasAssistantRole(message) {
|
|
288
|
+
return typeof message === "object" && message !== null && message.role === "assistant";
|
|
289
|
+
}
|
|
267
290
|
function isTextBlock(block) {
|
|
268
291
|
if (!block || typeof block !== "object")
|
|
269
292
|
return false;
|
|
@@ -16,10 +16,11 @@ export interface SessionView {
|
|
|
16
16
|
lastError?: string;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* `updatedAt` is the
|
|
20
|
-
*
|
|
21
|
-
* transition.
|
|
22
|
-
*
|
|
19
|
+
* `updatedAt` is the time of the last observed public SDK activity of the turn (assistant
|
|
20
|
+
* message start, update and end; tool execution start, update and end), then freezes at the
|
|
21
|
+
* terminal transition. Streaming is activity, not evidence of finished or useful work, and
|
|
22
|
+
* internal work without a public event stays invisible. It is a liveness lower bound, not a
|
|
23
|
+
* progress count, and does not prove that a silent turn is healthy.
|
|
23
24
|
*/
|
|
24
25
|
export interface TurnView {
|
|
25
26
|
turnId: string;
|
|
@@ -81,8 +82,9 @@ export declare class SessionRegistry {
|
|
|
81
82
|
private completeTurn;
|
|
82
83
|
private failTurn;
|
|
83
84
|
/**
|
|
84
|
-
* Liveness only.
|
|
85
|
-
* never another turn or the session.
|
|
85
|
+
* Liveness only. Observed public SDK activity (assistant message start, update and end; tool
|
|
86
|
+
* execution start, update and end) advances this turn, never another turn or the session.
|
|
87
|
+
* Streaming is activity, not evidence of finished or useful work. Activity after accepted preflight but before
|
|
86
88
|
* publication is stamped and applied only if the turn is published; callbacks before
|
|
87
89
|
* acceptance or after terminal cleanup cannot publish or revive a turn.
|
|
88
90
|
*/
|
package/dist/session-registry.js
CHANGED
|
@@ -273,8 +273,9 @@ export class SessionRegistry {
|
|
|
273
273
|
entry.turn = undefined; if (entry.turnPromise)
|
|
274
274
|
entry.turnPromise = undefined; entry.state = "failed"; entry.updatedAt = turn.updatedAt; entry.lastError = "Pi turn failed"; }
|
|
275
275
|
/**
|
|
276
|
-
* Liveness only.
|
|
277
|
-
* never another turn or the session.
|
|
276
|
+
* Liveness only. Observed public SDK activity (assistant message start, update and end; tool
|
|
277
|
+
* execution start, update and end) advances this turn, never another turn or the session.
|
|
278
|
+
* Streaming is activity, not evidence of finished or useful work. Activity after accepted preflight but before
|
|
278
279
|
* publication is stamped and applied only if the turn is published; callbacks before
|
|
279
280
|
* acceptance or after terminal cleanup cannot publish or revive a turn.
|
|
280
281
|
*/
|