@llblab/pi-kit 0.35.3 → 0.35.5
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/AGENTS.md +3 -0
- package/BACKLOG.md +6 -0
- package/CHANGELOG.md +12 -0
- package/README.md +50 -3
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +7 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +49 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +5 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +44 -17
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +74 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +89 -33
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +6 -4
package/AGENTS.md
CHANGED
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
- An explicitly requested bundled-extension release also authorizes synchronizing and releasing the kit in the same task; do not stop at local preparation. Follow the guarded repository release flow. Installation and live-client reload remain separately authorized.
|
|
24
24
|
- Bundle every included Pi package so npm installation is self-contained under this package's module root. Keep repository-local `legacy-peer-deps=true` so host-provided Pi peers are not resolved into the kit's lockfile; Pi supplies them at runtime.
|
|
25
25
|
- Expose only resources declared by each included package. Prefer published distribution entrypoints over source entrypoints when both exist.
|
|
26
|
+
- Extension entrypoints are pointers, not copies: every declared extension re-exports the exact bundled entrypoint from `extensions/<name>/index.<ext>`, and that folder is published so Pi shows a compact label. A bundled package must therefore resolve auxiliary files, workers, spawned scripts, sibling packages, and extension-local resources from its own module URL (`import.meta.url`), never from the declared entry path or `process.argv[1]`; only the declared entry is redirected, so anything located from that path breaks.
|
|
26
27
|
- Do not copy extension source, Skills, or documentation into this repository.
|
|
27
28
|
- Preserve package independence: a kit release may advance any subset of included extensions without forcing lockstep extension releases.
|
|
28
29
|
- Adding packages or newly exposed capabilities requires a kit minor bump. For updates to the existing composition, derive the kit version bump from the highest dependency bump in the synchronized release cohort: dependency patch-only updates require a kit patch bump, while any dependency minor update requires a kit minor bump. Do not let lower-level dependency bumps force a larger kit bump.
|
|
30
|
+
- Automated dependency updates must remain reviewable PRs with a visible kit version bump; never auto-merge. Keep unreviewed preparation read-only and secret-free, isolate PR writes on a fresh runner, and accept only declared data artifacts without executing them. Publish only the reviewed main commit after clean validation, and never overwrite release tags.
|
|
29
31
|
- Use English for public and repository-facing text.
|
|
30
32
|
- Do not commit, publish, tag, create a remote repository, or mutate Pi settings without explicit authorization.
|
|
31
33
|
|
|
@@ -34,4 +36,5 @@
|
|
|
34
36
|
- Run `npm test` after manifest or inventory changes.
|
|
35
37
|
- Run `npm run pack:check` before declaring package readiness.
|
|
36
38
|
- Inspect the dry-run tarball inventory to confirm every package in `bundledDependencies` and its declared resources are present.
|
|
39
|
+
- Confirm that a newly added or advanced package resolves its auxiliary files from `import.meta.url`; a pointer-loaded extension must still start its own workers and scripts.
|
|
37
40
|
- Run `npm run validate` at completion.
|
package/BACKLOG.md
CHANGED
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Package pins, resource order and bundled runtime ownership remain authoritative in `package.json`. Release outcomes belong in [CHANGELOG.md](./CHANGELOG.md).
|
|
4
4
|
|
|
5
|
+
## Automation activation (administrator-owned)
|
|
6
|
+
|
|
7
|
+
- Before relying on automated updates/publication, configure the repository settings described in [Update automation](./README.md#repository-setup-and-secrets): required human Code Owner approval, stale-approval dismissal, the `validate` PR check, no bypass/auto-merge and Actions PR creation permission. Add an automation-account `PI_KIT_PAT` only if downstream checks must trigger automatically. Local validation does not establish these remote settings.
|
|
8
|
+
- **First OIDC publication:** The operator created the `publish-pi-kit.yml` npm trusted publisher on October 21, 2026. Confirm direct `npm publish` is permitted and complete the first successful workflow publication before **October 23, 2026 at 18:13 UTC** to validate that binding; otherwise recreate it. Keep the existing `release.yml` connection until success. No `NPM_TOKEN` is required.
|
|
9
|
+
|
|
5
10
|
## Carried checks
|
|
6
11
|
|
|
12
|
+
- **Installed 0.35.4 Telegram smoke (operator-owned):** After separately authorized kit installation/reload of every Pi process together, run `/telegram-new` in one follower instance and confirm the same Thread is reused with a new letter slot and title, with no second tab and no orphan left behind. Then restart that instance, open a new session in the same folder, and confirm it adopts the retained Thread instead of provisioning another. Do not erase accepted work or replay uncertain input. Packed validation does not certify installed clients.
|
|
7
13
|
- **Installed 0.35.3 label smoke (operator-owned):** After separately authorized kit installation/reload, confirm the startup list and `pi config` show one compact entry per bundled extension (`@llblab/pi-kit:pi-actors` and so on), that every extension still loads exactly once without duplicate-registration warnings, and that Skills are unaffected. Packed validation does not certify installed clients.
|
|
8
14
|
- **Installed 0.35.2 Telegram smoke (operator-owned):** After separately authorized kit installation/reload, connect a new Thread with every letter slot occupied and confirm it appears in seconds without the leader stalling or its followers going stale. Repeat the operator's earlier checks for leader failover after `/telegram-disconnect`, follower takeover, and collapsed repeated log records. Packed validation does not certify installed clients.
|
|
9
15
|
- **Installed 0.35.1 Telegram smoke (operator-owned):** After separately authorized kit installation/reload, verify Reroute removes an exhausted temporary tab and Restore keeps the new tab while deleting its unprotected predecessor. Confirm protected work survives. Guest PNG/JPEG, document and MP3 replacement plus staging cleanup remain explicitly deferred upstream live checks; verify unsupported OGG/Opus feedback separately. Upstream idle-leader acceptance and kit packaging checks do not certify this installation.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.35.5: Reviewed Updates and OIDC Releases
|
|
6
|
+
|
|
7
|
+
- `Update preparation`: Adds daily/manual npm discovery and one human-reviewed update PR containing exact pins, both bundle aliases, regenerated lockfile, README inventory, kit version and changelog. Preparation is read-only without secrets or dependency hooks/bin links; a fresh write runner accepts only four checked data files. Major/prerelease updates and resource changes remain manual; failed validation prevents PR creation, and no auto-merge is enabled.
|
|
8
|
+
- `Publication`: Replaces tag-triggered publication with a main-only npm Trusted Publisher using short-lived OIDC, without a persistent npm token. It reads the reviewed version, skips an unchanged latest tag, validates before a non-forced tag push, and publishes npm before the GitHub Release. Existing-version recovery requires matching tag/npm commit identity; public packed inventory remains verified. Stage-only permission is rejected, not silently substituted.
|
|
9
|
+
- `Review and evidence`: Adds automation regression tests, executable lock/bundle/tarball coherence checks, CODEOWNERS and maintainer setup/recovery guidance. The npm trust binding and enforced main-branch review protection remain administrator-owned activation steps.
|
|
10
|
+
- `Compatibility`: Keeps all eight dependency pins, bundled resource order and runtime entrypoints unchanged. This patch changes update/release tooling and documentation; it does not install or reload Pi clients.
|
|
11
|
+
|
|
12
|
+
## 0.35.4: Telegram Session Thread Continuity
|
|
13
|
+
|
|
14
|
+
- `Telegram pin`: Advances only the exact Telegram pin from `0.57.2` to `0.57.3`. A session replaced inside one Pi process now reuses its preserved Thread on every registration path instead of provisioning a second tab, and a new session registering in a folder whose previous session is provably gone adopts that binding's exact target and slot while the predecessor binding is re-keyed. A live predecessor or an ambiguous candidate set keeps ordinary provisioning.
|
|
15
|
+
- `Checks`: The exact-pin test, README inventory and pointer targets now read `0.57.3`; the seven published pointer entrypoints and declared Skill paths are unchanged. Packed validation covers build, types, tests, audit and bundle inventory only, so installed-client behavior remains an operator check in [Backlog](./BACKLOG.md).
|
|
16
|
+
|
|
5
17
|
## 0.35.3: Compact Bundled Extension Labels
|
|
6
18
|
|
|
7
19
|
- `Extension wiring`: Each bundled extension is now declared through a thin pointer at `extensions/<name>/index.js` that re-exports the exact bundled entrypoint, so the Pi startup list and `pi config` show `@llblab/pi-kit:pi-telegram` instead of the full nested path. Pins, load order and package code are unchanged, and an explicit `pi.extensions` still suppresses conventional directory discovery, so nothing loads twice.
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
18
18
|
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.13.1` | OpenAI/legacy Codex subscription quota and Business credit status, with persistent priority Fast toggle mirrored in Telegram |
|
|
19
19
|
| [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.9.0` | Visible continuation scheduling and bounded worker Skills through compiled, manifest-owned resources |
|
|
20
20
|
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.26.4` | Scoped context/memory compiler with step-independent current memory, stable Passive/Active modes and visible blocked-state status |
|
|
21
|
-
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.57.
|
|
21
|
+
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.57.3` | Telegram companion with per-prompt Steer, queued controls, Reroute/Restore, recoverable connections, previews, follower Threads, files, and voice |
|
|
22
22
|
| [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
|
|
23
23
|
|
|
24
24
|
Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
|
|
@@ -56,8 +56,55 @@ npm install
|
|
|
56
56
|
npm run validate
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
The repository-local `.npmrc` keeps Pi-provided peer packages out of the kit's lockfile; Pi supplies those peers at runtime. To advance an included package, update its exact version in `dependencies`, run `npm install`, synchronize bundled dependencies, each declared extension pointer and its target, declared Skill paths, tests, the table above, and the changelog, then validate the packed artifact. Keep every pointer target identical to the entrypoint its pin declares. Expose only resources declared by the published owning package; do not use version ranges or unpublished local paths.
|
|
59
|
+
The repository-local `.npmrc` keeps Pi-provided peer packages out of the kit's lockfile; Pi supplies those peers at runtime. To advance an included package, update its exact version in `dependencies`, run `npm install`, synchronize bundled dependencies, each declared extension pointer and its target, declared Skill paths, tests, the table above, and the changelog, then validate the packed artifact. Keep every pointer target identical to the entrypoint its pin declares. A bundled package must resolve auxiliary files, workers and sibling packages from its own module URL rather than from the declared entry path, because a pointer redirects only that entry. Expose only resources declared by the published owning package; do not use version ranges or unpublished local paths.
|
|
60
|
+
|
|
61
|
+
## Update automation
|
|
62
|
+
|
|
63
|
+
Two independent workflows prepare updates and publish **human-reviewed** kit versions. Automation reduces synchronization work; it does not decide whether an extension is safe and never enables auto-merge.
|
|
64
|
+
|
|
65
|
+
### Prepare an update PR
|
|
66
|
+
|
|
67
|
+
[Update pi-kit dependencies](./.github/workflows/update-pi-kit-deps.yml) runs daily at **03:00 UTC** and supports `workflow_dispatch` on `main`. Manual dispatch defaults to `dry_run: true`: it prepares and validates the complete working-tree diff and prints it in the run log/summary, but does not push a branch or create/update a PR. Select `false` to open or refresh the single `chore/pi-kit-deps` PR, labelled `dependencies` and `automated`.
|
|
68
|
+
|
|
69
|
+
The dependency-free [updater](./scripts/check-updates.mjs) reads npm's `latest` metadata for the existing direct dependencies. A custom updater is used instead of Dependabot because one coherent PR must synchronize kit-owned artifacts, not just npm pins:
|
|
70
|
+
|
|
71
|
+
- Exact dependency versions, both `bundledDependencies`/`bundleDependencies` aliases, and `package-lock.json` regenerated by `npm install --ignore-scripts --bin-links=false`.
|
|
72
|
+
- The README package/version inventory and versioned upstream documentation links.
|
|
73
|
+
- The kit version and [changelog](./CHANGELOG.md): patch-only direct updates produce a kit patch; any direct minor update produces a kit minor. Transitive bumps do not determine the kit bump; pending `Unreleased` outcomes are carried into the new release within the changelog budget.
|
|
74
|
+
- Existing package membership, extension pointers and resource order are preserved. Tests reject changed upstream resource declarations; prepare those composition changes manually rather than guessing new entrypoints.
|
|
75
|
+
|
|
76
|
+
Major updates and non-stable `latest` versions are reported for manual preparation, never silently accepted. Pre-1.0 minor releases can still break compatibility and require particular care. After synchronization, the updater runs **`npm ci --ignore-scripts --bin-links=false` and `npm run validate`**. Failure stops the run before any PR write; inspect the failed run's log. Both installs disable dependency lifecycle hooks and executable links, so a dependency-provided `node`/`npm` bin cannot shadow validation commands. This is not a security verdict about the bundled code. For a read-only local discovery plan, run `node scripts/check-updates.mjs --plan`; `npm run deps:check` instead prepares and validates changes locally and should be used on a disposable working branch.
|
|
77
|
+
|
|
78
|
+
Preparation runs in a **read-only job without repository secrets**. A separate fresh runner receives only `package.json`, `package-lock.json`, `README.md` and `CHANGELOG.md` from that run's artifact. Before any PR write, the trusted updater rejects extra paths, symlinks, package-set/resource/script changes, incorrect version bumps and unexpected prose edits; it regenerates the PR report itself. The write job never installs dependencies or executes artifact-provided scripts. Read-only preparation cannot poison the later write runner's command path.
|
|
79
|
+
|
|
80
|
+
### Maintainer review
|
|
81
|
+
|
|
82
|
+
For every bot PR, use the [Security](#security) guidance and its generated checklist:
|
|
83
|
+
|
|
84
|
+
1. Read the linked upstream releases, changelogs and source comparisons. Investigate breaking changes and compatibility with the supported Pi/Node versions.
|
|
85
|
+
2. Review new transitive dependencies, registry integrity changes, executable hooks and auxiliary workers/scripts. Confirm pointer-loaded extensions resolve auxiliary files from their own `import.meta.url`, not the redirected entry path.
|
|
86
|
+
3. Inspect the entire diff: exact pins with no ranges, both bundle aliases matching the package set, lockfile, README inventory, kit version and changelog. No new package, capability or resource should enter through an automatic pin-only update.
|
|
87
|
+
4. Inspect successful `validate` output, including the dry-run tarball's bundled manifests, extension targets and Skills. Run necessary isolated runtime/client smoke checks; package checks and a clean audit do not establish extension safety.
|
|
88
|
+
5. Obtain human approval, then **merge manually**. Do not enable auto-merge. If the updater refreshes the PR, review the new diff again before approval.
|
|
89
|
+
|
|
90
|
+
The existing [Release / validate](./.github/workflows/release.yml) PR check is retained, without a tag-triggered publisher. PRs created with `GITHUB_TOKEN` normally do **not** trigger another workflow. The updater already validates before creating them; to obtain the separate PR check, a maintainer can close/reopen the PR or push a reviewed change with their own credentials. `workflow_dispatch` on the PR branch can also provide separate validation evidence, but it does not replace a required PR check. Use an automation-account `PI_KIT_PAT` when automatic downstream PR checks are needed; do not use the sole reviewer's PAT, since authors cannot approve their own PRs.
|
|
91
|
+
|
|
92
|
+
### Publish after merge
|
|
93
|
+
|
|
94
|
+
[Publish pi-kit](./.github/workflows/publish-pi-kit.yml) runs on `main` pushes changing `package.json`, or by manual dispatch on `main`. It **reads**, never bumps, the reviewed kit version. If `v<version>` equals the latest `v*` tag in version-sort order, the run exits successfully without publishing. Older versions and existing non-latest tags are rejected, not overwritten.
|
|
95
|
+
|
|
96
|
+
For a new version the order is: verify the hosted main-workflow OIDC environment and npm >= 11.5.1 → clean `npm ci --ignore-scripts --bin-links=false` → `npm run validate` → create and push `vX.Y.Z` → `npm publish --access public` through Trusted Publishing → verify public npm identity and packed inventory → `gh release create --generate-notes`. Validation must succeed before a tag is created. The tag push is non-forced, concurrent publications are serialized, and an npm version-conflict error is accepted only when the tag and npm `gitHead` both match the current HEAD. Authentication, registry failures and contradictory identities remain errors. The workflow never commits to `main`.
|
|
97
|
+
|
|
98
|
+
A tag may remain if npm or GitHub fails **after** successful validation and tag push. An ordinary rerun intentionally no-ops once that version is the latest tag; it does not republish. Recovery requires a separate maintainer decision: verify the existing remote tag and npm state, preserve the exact tagged commit, and rerun `npm ci --ignore-scripts --bin-links=false` and `npm run validate` before any missing publication. Use an explicitly reviewed OIDC recovery or an interactive maintainer publication with 2FA; never silently fall back to a persistent token or staging. Run `node scripts/publish-pi-kit.mjs verify` before creating a missing GitHub Release with `gh release create vX.Y.Z --verify-tag --generate-notes`. Never move/delete a published tag or reuse a version for different content.
|
|
99
|
+
|
|
100
|
+
### Repository setup and secrets
|
|
101
|
+
|
|
102
|
+
- **npm Trusted Publishing (required; no `NPM_TOKEN`):** In the npm settings for `@llblab/pi-kit`, add a GitHub Actions trusted publisher for user/organization `llblab`, repository `pi-kit`, workflow filename `publish-pi-kit.yml` (without `.github/workflows/`), and no environment name. Explicitly allow **`npm publish`**, not only `npm stage publish`. Keep an existing `release.yml` trust connection until the new publisher succeeds. A new configuration must complete its first successful publication within two days or be recreated. The preflight verifies runtime prerequisites, not npm authorization; only the successful publish validates the binding. See [npm's Trusted Publishing documentation](https://docs.npmjs.com/trusted-publishers/).
|
|
103
|
+
- **`PI_KIT_PAT` (optional):** A repository-scoped automation-account token with contents and pull-request write access. Without it, PR writes and tag pushes use `GITHUB_TOKEN`; those events do not trigger downstream workflows. Use the PAT only if those downstream events are required. No token is stored in source or committed git configuration.
|
|
104
|
+
- **Actions permissions:** Permit Actions to create pull requests in the repository settings. Only the updater's fresh PR-write job requests `contents: write` and `pull-requests: write`; dependency preparation and PR validation are read-only. The publisher requests `contents: write` for the validated tag/GitHub Release and `id-token: write` for short-lived npm OIDC authentication on a GitHub-hosted runner. No long-lived npm credential is needed.
|
|
105
|
+
- **Do not substitute stage-only tokens:** npm is removing granular-token direct publication in January 2027. A stage-only token returns `E_STAGE_REQUIRED` for `npm publish` and requires separate maintainer approval with 2FA after `npm stage publish`. That is a different release process; this workflow keeps automatic post-merge publication through explicitly permitted OIDC and never silently stages instead.
|
|
106
|
+
- **Required human review:** [CODEOWNERS](./.github/CODEOWNERS) assigns `@llblab` to workflows, manifests and automation scripts. An administrator must separately protect `main` with a pull-request requirement, required Code Owner approval, stale-approval dismissal after new commits, and the `validate` status check; disallow bypasses and auto-merge. CODEOWNERS alone does **not** enforce approval. Configure these settings before relying on the pipeline as a mandatory review gate.
|
|
60
107
|
|
|
61
108
|
## Security
|
|
62
109
|
|
|
63
|
-
Pi extensions execute with the user's system permissions, and Skills can guide executable actions. Review each included package and release before advancing its pin. Private Knowledge, credentials, and personal Pi configuration are not bundled.
|
|
110
|
+
Pi extensions execute with the user's system permissions, and Skills can guide executable actions. Review each included package and release before advancing its pin; the [maintainer checklist](#maintainer-review) makes that review explicit for automated PRs. Automated validation and npm audit are packaging signals, not approval to trust an update. Private Knowledge, credentials, and personal Pi configuration are not bundled.
|
|
@@ -87,6 +87,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
87
87
|
- Admission is journal-first: validate and persist the complete `getUpdates` response before one monotonic offset commit, then signal an independent worker without awaiting semantic execution. Missing cursor with a non-empty journal, malformed/foreign authority, or capacity exhaustion fails closed. “Durable” means process-crash recovery after atomic rename, not unflushed host/kernel/filesystem/device/power-loss survival.
|
|
88
88
|
- Storage cutovers must reconcile actual consumer locations before correcting path adapters. Never normalize a relative historical reference into new authority or treat equal reference strings / empty canonical storage as source completeness. Exact-path preflight is lexical only; physical identity, historical coverage, writer closure and migration remain separate proofs.
|
|
89
89
|
- Workspace mutations acquire cross-process admission before their shared process-local gate and hold it through asynchronous API work and durable settlement. Topic lifecycle, complete unbound/reroute target handling, manual disconnect, and session-restart cleanup use profile-wide scope; either retained retirement-fence phase rejects them before state access. Cleanup admission spans intent publication, target mutation, persistence, and successful transport release. Explicit manual disconnect may stop only its captured local transport after cleanup admission/effects fail; it must not bypass Thread mutation fences, release a replacement connection, claim unconfirmed deletion, or retry an uncertain stop. Session-restart cleanup retains its admission-required behavior. Detached reconciliation that mutates Thread state must reacquire fresh profile admission through the same gate; it cannot inherit a caller lease that ended before its timer runs. A live operation ID has one process-local caller: concurrent reuse is rejected before lease acquisition, while retry after the caller exits may resume exact durable authority.
|
|
90
|
+
- One Thread keeps one owner across session replacement. A session replaced inside the same process reuses its preserved handoff Thread instead of provisioning another. A new session in a folder whose previous session is provably gone (no live registration for its target, absent runtime PID) adopts that binding's exact target and slot, and the predecessor binding is re-keyed to the successor rather than left as an orphan active row. A live predecessor, an ambiguous candidate set, an unknown runtime, or a missing liveness probe keeps ordinary provisioning.
|
|
90
91
|
- Workspace retirement is capacity-pressure-only. Elapsed time and heartbeat silence never trigger deletion; only complete `A`–`Z` exhaustion may propose the oldest continuously proven inactive, fully unprotected binding. Exact deletion, durable retirement, and fence completion precede slot reuse. Authorized demand-driven rotation retries one failed fresh allocation after retirement; restore-only follower startup never evicts. Release ordinary registration/provisioning leases before acquiring the destructive fence, retain the shared mutation gate across retirement, and validate the exact permit immediately before one non-retried deletion. Persist an exact method/target-matched rejection before withdrawing its intent; release that fence only after durable withdrawal, retaining the binding. A later attempt requires fresh operation authority. Protection reads must never repair, quarantine, or reset journals. Non-destructive owner detachment must atomically retain one exact Workspace binding and its letter while removing only its uniquely matched owner record and stamping first inactivity; it never manufactures Thread-deletion evidence or clears accepted work. During authenticated follower registration, a mismatched retained target record is repaired to the exact Workspace claim before binding commit; if same-target commit adopts the retained binding's slot, its returned binding then owns the registration result and owner-record slot. Unrelated bindings that own a stale or provisional letter remain untouched. Same-target re-keying retains historical journal keys and exact source tuples without upgrading unknown completeness. Retained prune observations are bounded, non-routing and registration/profile/epoch/runtime-fenced. One unfinished preservation operation retains its admission identity across fresh-PID-proof retries; it can never become deletion authority. Leader quit requires completed delivery/polling/worker teardown under the captured session generation, profile and epoch; reload/new/resume/fork never establish inactivity. Unknown deletion outcomes retain their fence; only confirmed durable completion permits reuse.
|
|
91
92
|
- Foreign forwarding settles as `accepted`, `retryable`, or `terminal-rejected`. Only an authenticated acknowledgement carrying the expected `deliveryId` and `sourceUpdateId` releases leader journal authority. Negative, missing, stale, mismatched, or capacity-failed settlement remains durable; callback error answers are side effects only. Exception: the operator-approved fixed chooser deadline may discard its exact unclaimed donor pending source without a body archive or delivery-success claim. Accepted recipient queue/work and bound Threads are untouched; old donor controls and late reports cannot replay it. The lifetime and temporary cleanup contract lives in `docs/architecture.md`.
|
|
92
93
|
- A forwarding delivery id is stable across registration replacement and derives from envelope kind, source `update_id`, and stable recipient binding. Runtime instance and registration generation remain separate attempt fences. Persisted message ownership carries the stable binding so replay can rebind only to its current authenticated registration.
|
|
@@ -2,4 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
_This file owns unresolved project work only. Completed behavior belongs in `CHANGELOG.md`; durable contracts belong in `AGENTS.md` and `/docs`._
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Transit Thread lifecycle
|
|
6
|
+
|
|
7
|
+
An All input's temporary routing Thread must outlive its pending choice and leave no residue.
|
|
8
|
+
|
|
9
|
+
- [ ] A Thread with a pending Reroute/Restore choice is never deleted by unbound-topic reconciliation; only its own Reroute, cancel or expiry removes it.
|
|
10
|
+
- [ ] Cancel resolves from the durable journal, so it still works after the tab or chooser message is gone.
|
|
11
|
+
- [ ] Expiry reports a visible "prompt not delivered" result instead of silently discarding the input as `routing-expiry`.
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.57.3: Session Thread Continuity
|
|
8
|
+
|
|
9
|
+
- `Session handoff`: A session replaced inside one Pi process now reuses its preserved Thread on every registration path, not only connect recovery. The successor re-keys onto the same tab and slot with the new session id, so `/new` no longer provisions a second tab and leaves the previous one without an owner.
|
|
10
|
+
- `Successor adoption`: A new session registering in a folder whose previous session is provably gone adopts that binding's exact target and slot, and the retained binding is re-keyed to the successor instead of leaving an orphan active row. A live predecessor, an ambiguous candidate set, an unknown runtime or a missing liveness probe keeps ordinary provisioning.
|
|
11
|
+
|
|
7
12
|
## 0.57.2: Connect Latency and Session Identity
|
|
8
13
|
|
|
9
14
|
- `Connect latency`: A new Thread no longer waits about 20 seconds when all 26 letter slots are occupied. Slot rotation read the complete journal namespace and every shared journal once per binding inside one synchronous pass, freezing the leader's event loop and staling healthy follower heartbeats. One occupancy snapshot now reads that binding-independent evidence once, and every fresh snapshot still re-reads it so post-fence checks see newly arrived work.
|
|
@@ -1851,9 +1851,9 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1851
1851
|
deps.setActiveAuthSecret?.(undefined);
|
|
1852
1852
|
await deps.stopReceiving?.();
|
|
1853
1853
|
};
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1854
|
+
// A fresh handoff belongs to this same process, so a replaced session reuses its Thread: the tab keeps
|
|
1855
|
+
// one identity and only its owner rotates. Explicit options still override the preserved target.
|
|
1856
|
+
const pendingHandoff = getTelegramFollowerSessionHandoff();
|
|
1857
1857
|
const pendingHandoffOptions = isTelegramFollowerSessionHandoffFresh(pendingHandoff)
|
|
1858
1858
|
? {
|
|
1859
1859
|
target: pendingHandoff.target,
|
|
@@ -922,6 +922,55 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
|
|
|
922
922
|
if (candidates.length === 1)
|
|
923
923
|
restoredPredecessor = candidates[0];
|
|
924
924
|
}
|
|
925
|
+
// A new session in a folder whose previous session is gone takes over that Thread: the tab keeps a single
|
|
926
|
+
// owner and the retired binding is re-keyed instead of being left behind as an orphan.
|
|
927
|
+
if (!restoredPredecessor &&
|
|
928
|
+
registration.cwd &&
|
|
929
|
+
registration.sessionId &&
|
|
930
|
+
!registration.previousInstanceId &&
|
|
931
|
+
deps.getLiveFollowers &&
|
|
932
|
+
deps.isFollowerProcessAlive) {
|
|
933
|
+
const identity = Threads.createTelegramWorkspaceBindingIdentity(registration.cwd, 0, registration.sessionId);
|
|
934
|
+
const liveFollowers = deps.getLiveFollowers();
|
|
935
|
+
const candidates = deps.topicTargetStore
|
|
936
|
+
.listWorkspaceBindings()
|
|
937
|
+
.filter((binding) => !!(identity &&
|
|
938
|
+
binding.cwd === identity.cwd &&
|
|
939
|
+
binding.sessionId !== identity.sessionId))
|
|
940
|
+
.flatMap((binding) => {
|
|
941
|
+
const matches = deps.topicTargetStore
|
|
942
|
+
.list()
|
|
943
|
+
.filter((record) => record.status === "active" &&
|
|
944
|
+
record.owner?.kind === "manual-follower" &&
|
|
945
|
+
record.target.chatId === binding.target.chatId &&
|
|
946
|
+
record.target.threadId === binding.target.threadId);
|
|
947
|
+
return matches.length === 1
|
|
948
|
+
? [{ binding, record: matches[0] }]
|
|
949
|
+
: [];
|
|
950
|
+
})
|
|
951
|
+
.filter(({ record }) => {
|
|
952
|
+
if (liveFollowers.some((peer) => peer.instanceId === record.instanceId ||
|
|
953
|
+
(peer.target?.chatId === record.target.chatId &&
|
|
954
|
+
peer.target.threadId === record.target.threadId)))
|
|
955
|
+
return false;
|
|
956
|
+
const match = /^([1-9][0-9]*):[0-9]+$/.exec(record.instanceId ?? ""), pid = match ? Number(match[1]) : undefined;
|
|
957
|
+
if (pid === undefined || !Number.isSafeInteger(pid))
|
|
958
|
+
return false;
|
|
959
|
+
try {
|
|
960
|
+
return deps.isFollowerProcessAlive(pid) === false;
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
// Ambiguity keeps ordinary provisioning: never guess which retained tab a new session inherits.
|
|
967
|
+
if (candidates.length === 1 && identity?.sessionId) {
|
|
968
|
+
const candidate = candidates[0];
|
|
969
|
+
if (candidate.binding.sessionId &&
|
|
970
|
+
deps.topicTargetStore.adoptWorkspaceThreadBindingForSession(candidate.binding.cwd, candidate.binding.sessionId, identity.sessionId, candidate.binding.target))
|
|
971
|
+
restoredPredecessor = candidate.record;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
925
974
|
let capacityUnavailable = false;
|
|
926
975
|
const workspaceIdentity = registration.cwd
|
|
927
976
|
? deps.topicTargetStore.claimWorkspaceIdentity(registration.cwd, registration.instanceId, registration.previousInstanceId ?? restoredPredecessor?.instanceId, {
|
|
@@ -569,6 +569,11 @@ export interface TelegramTopicTargetStore {
|
|
|
569
569
|
sessionHandoff?: boolean;
|
|
570
570
|
onCapacityUnavailable?: () => void;
|
|
571
571
|
}) => TelegramWorkspaceBindingIdentity | undefined;
|
|
572
|
+
/**
|
|
573
|
+
* Re-keys one retained Thread binding from a retired session to its successor at the same slot and target,
|
|
574
|
+
* so the tab keeps a single owner. Fails without mutation when the source binding or slot identity is absent.
|
|
575
|
+
*/
|
|
576
|
+
adoptWorkspaceThreadBindingForSession: (cwd: string, sourceSessionId: string, sessionId: string, target: TelegramTarget) => boolean;
|
|
572
577
|
releaseWorkspaceClaim: (instanceId: string) => boolean;
|
|
573
578
|
upsertWorkspaceBinding: (binding: TelegramWorkspaceThreadBinding, claimInstanceId?: string) => TelegramWorkspaceThreadBinding | undefined;
|
|
574
579
|
upsert: (record: TelegramTopicTargetRecord) => TelegramTopicTargetRecord;
|
|
@@ -2877,6 +2877,34 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2877
2877
|
}
|
|
2878
2878
|
};
|
|
2879
2879
|
};
|
|
2880
|
+
/**
|
|
2881
|
+
* Re-keys one retained Thread binding from a retired session to its successor at the same slot and target,
|
|
2882
|
+
* so the tab keeps a single owner. Fails without mutation when the source binding or slot identity is absent.
|
|
2883
|
+
*/
|
|
2884
|
+
const rekeyWorkspaceBindingForSession = (cwd, sourceSessionId, sessionId, target) => {
|
|
2885
|
+
const sourceEntry = Array.from(workspaceBindings.entries()).find(([, binding]) => binding.cwd === cwd &&
|
|
2886
|
+
binding.sessionId === sourceSessionId &&
|
|
2887
|
+
targetMatches(binding.target, target));
|
|
2888
|
+
if (!sourceEntry)
|
|
2889
|
+
return false;
|
|
2890
|
+
const [sourceKey, sourceBinding] = sourceEntry;
|
|
2891
|
+
const replacementIdentity = Array.from({
|
|
2892
|
+
length: TELEGRAM_WORKSPACE_SLOTS.length,
|
|
2893
|
+
})
|
|
2894
|
+
.map((_, ordinal) => createTelegramWorkspaceBindingIdentityWithKey(sourceBinding.cwd, sourceBinding.workspaceKey, ordinal, sessionId))
|
|
2895
|
+
.find((identity) => identity?.instanceSlot === sourceBinding.instanceSlot);
|
|
2896
|
+
if (!replacementIdentity ||
|
|
2897
|
+
replacementIdentity.instanceSlot !== sourceBinding.instanceSlot)
|
|
2898
|
+
return false;
|
|
2899
|
+
workspaceBindings.delete(sourceKey);
|
|
2900
|
+
workspaceBindings.set(getWorkspaceBindingMapKey(replacementIdentity), {
|
|
2901
|
+
...sourceBinding,
|
|
2902
|
+
...replacementIdentity,
|
|
2903
|
+
updatedAtMs: getNowMs(),
|
|
2904
|
+
});
|
|
2905
|
+
markDirty();
|
|
2906
|
+
return true;
|
|
2907
|
+
};
|
|
2880
2908
|
return {
|
|
2881
2909
|
async load() {
|
|
2882
2910
|
if (workspaceIO && dirty && !loadedScopeMatches(getPath()))
|
|
@@ -4797,6 +4825,20 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4797
4825
|
const binding = workspaceBindings.get(mapKey);
|
|
4798
4826
|
return binding ? cloneWorkspaceBinding(binding) : undefined;
|
|
4799
4827
|
},
|
|
4828
|
+
adoptWorkspaceThreadBindingForSession(cwd, sourceSessionId, sessionId, target) {
|
|
4829
|
+
if (workspaceRetirementCommitInFlight)
|
|
4830
|
+
return false;
|
|
4831
|
+
const normalizedCwd = normalizeTelegramWorkspacePath(cwd);
|
|
4832
|
+
const normalizedSource = normalizeTelegramSessionId(sourceSessionId);
|
|
4833
|
+
const normalizedSession = normalizeTelegramSessionId(sessionId);
|
|
4834
|
+
if (!normalizedCwd ||
|
|
4835
|
+
!normalizedSource ||
|
|
4836
|
+
!normalizedSession ||
|
|
4837
|
+
normalizedSource === normalizedSession ||
|
|
4838
|
+
hasWorkspaceRetirementConflict({ cwd: normalizedCwd }))
|
|
4839
|
+
return false;
|
|
4840
|
+
return rekeyWorkspaceBindingForSession(normalizedCwd, normalizedSource, normalizedSession, target);
|
|
4841
|
+
},
|
|
4800
4842
|
claimWorkspaceIdentity(cwd, instanceId, previousInstanceId, options) {
|
|
4801
4843
|
if (workspaceRetirementCommitInFlight)
|
|
4802
4844
|
return undefined;
|
|
@@ -4824,24 +4866,9 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4824
4866
|
binding.sessionId === replacement.sourceSessionId &&
|
|
4825
4867
|
targetMatches(binding.target, replacement.target));
|
|
4826
4868
|
if (sourceEntry) {
|
|
4827
|
-
const
|
|
4828
|
-
|
|
4829
|
-
length: TELEGRAM_WORKSPACE_SLOTS.length,
|
|
4830
|
-
})
|
|
4831
|
-
.map((_, ordinal) => createTelegramWorkspaceBindingIdentityWithKey(sourceBinding.cwd, sourceBinding.workspaceKey, ordinal, normalizedSessionId))
|
|
4832
|
-
.find((identity) => identity?.instanceSlot === sourceBinding.instanceSlot);
|
|
4833
|
-
if (replacementIdentity &&
|
|
4834
|
-
replacementIdentity.instanceSlot === sourceBinding.instanceSlot) {
|
|
4835
|
-
const existingTargetRecord = Array.from(records.values()).find((record) => targetMatches(record.target, replacement.target));
|
|
4869
|
+
const existingTargetRecord = Array.from(records.values()).find((record) => targetMatches(record.target, replacement.target));
|
|
4870
|
+
if (rekeyWorkspaceBindingForSession(normalizedCwd, replacement.sourceSessionId, normalizedSessionId, replacement.target))
|
|
4836
4871
|
replacementPreviousInstanceId = existingTargetRecord?.instanceId;
|
|
4837
|
-
workspaceBindings.delete(sourceKey);
|
|
4838
|
-
workspaceBindings.set(getWorkspaceBindingMapKey(replacementIdentity), {
|
|
4839
|
-
...sourceBinding,
|
|
4840
|
-
...replacementIdentity,
|
|
4841
|
-
updatedAtMs: getNowMs(),
|
|
4842
|
-
});
|
|
4843
|
-
markDirty();
|
|
4844
|
-
}
|
|
4845
4872
|
}
|
|
4846
4873
|
}
|
|
4847
4874
|
const effectivePreviousInstanceId = previousInstanceId ?? replacementPreviousInstanceId;
|
|
@@ -3027,9 +3027,9 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
3027
3027
|
deps.setActiveAuthSecret?.(undefined);
|
|
3028
3028
|
await deps.stopReceiving?.();
|
|
3029
3029
|
};
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3030
|
+
// A fresh handoff belongs to this same process, so a replaced session reuses its Thread: the tab keeps
|
|
3031
|
+
// one identity and only its owner rotates. Explicit options still override the preserved target.
|
|
3032
|
+
const pendingHandoff = getTelegramFollowerSessionHandoff();
|
|
3033
3033
|
const pendingHandoffOptions = isTelegramFollowerSessionHandoffFresh(
|
|
3034
3034
|
pendingHandoff,
|
|
3035
3035
|
)
|
|
@@ -1583,6 +1583,80 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1583
1583
|
});
|
|
1584
1584
|
if (candidates.length === 1) restoredPredecessor = candidates[0];
|
|
1585
1585
|
}
|
|
1586
|
+
// A new session in a folder whose previous session is gone takes over that Thread: the tab keeps a single
|
|
1587
|
+
// owner and the retired binding is re-keyed instead of being left behind as an orphan.
|
|
1588
|
+
if (
|
|
1589
|
+
!restoredPredecessor &&
|
|
1590
|
+
registration.cwd &&
|
|
1591
|
+
registration.sessionId &&
|
|
1592
|
+
!registration.previousInstanceId &&
|
|
1593
|
+
deps.getLiveFollowers &&
|
|
1594
|
+
deps.isFollowerProcessAlive
|
|
1595
|
+
) {
|
|
1596
|
+
const identity = Threads.createTelegramWorkspaceBindingIdentity(
|
|
1597
|
+
registration.cwd,
|
|
1598
|
+
0,
|
|
1599
|
+
registration.sessionId,
|
|
1600
|
+
);
|
|
1601
|
+
const liveFollowers = deps.getLiveFollowers();
|
|
1602
|
+
const candidates = deps.topicTargetStore
|
|
1603
|
+
.listWorkspaceBindings()
|
|
1604
|
+
.filter(
|
|
1605
|
+
(binding) =>
|
|
1606
|
+
!!(
|
|
1607
|
+
identity &&
|
|
1608
|
+
binding.cwd === identity.cwd &&
|
|
1609
|
+
binding.sessionId !== identity.sessionId
|
|
1610
|
+
),
|
|
1611
|
+
)
|
|
1612
|
+
.flatMap((binding) => {
|
|
1613
|
+
const matches = deps.topicTargetStore
|
|
1614
|
+
.list()
|
|
1615
|
+
.filter(
|
|
1616
|
+
(record) =>
|
|
1617
|
+
record.status === "active" &&
|
|
1618
|
+
record.owner?.kind === "manual-follower" &&
|
|
1619
|
+
record.target.chatId === binding.target.chatId &&
|
|
1620
|
+
record.target.threadId === binding.target.threadId,
|
|
1621
|
+
);
|
|
1622
|
+
return matches.length === 1
|
|
1623
|
+
? [{ binding, record: matches[0]! }]
|
|
1624
|
+
: [];
|
|
1625
|
+
})
|
|
1626
|
+
.filter(({ record }) => {
|
|
1627
|
+
if (
|
|
1628
|
+
liveFollowers.some(
|
|
1629
|
+
(peer) =>
|
|
1630
|
+
peer.instanceId === record.instanceId ||
|
|
1631
|
+
(peer.target?.chatId === record.target.chatId &&
|
|
1632
|
+
peer.target.threadId === record.target.threadId),
|
|
1633
|
+
)
|
|
1634
|
+
)
|
|
1635
|
+
return false;
|
|
1636
|
+
const match = /^([1-9][0-9]*):[0-9]+$/.exec(record.instanceId ?? ""),
|
|
1637
|
+
pid = match ? Number(match[1]) : undefined;
|
|
1638
|
+
if (pid === undefined || !Number.isSafeInteger(pid)) return false;
|
|
1639
|
+
try {
|
|
1640
|
+
return deps.isFollowerProcessAlive!(pid) === false;
|
|
1641
|
+
} catch {
|
|
1642
|
+
return false;
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
// Ambiguity keeps ordinary provisioning: never guess which retained tab a new session inherits.
|
|
1646
|
+
if (candidates.length === 1 && identity?.sessionId) {
|
|
1647
|
+
const candidate = candidates[0]!;
|
|
1648
|
+
if (
|
|
1649
|
+
candidate.binding.sessionId &&
|
|
1650
|
+
deps.topicTargetStore.adoptWorkspaceThreadBindingForSession(
|
|
1651
|
+
candidate.binding.cwd,
|
|
1652
|
+
candidate.binding.sessionId,
|
|
1653
|
+
identity.sessionId,
|
|
1654
|
+
candidate.binding.target,
|
|
1655
|
+
)
|
|
1656
|
+
)
|
|
1657
|
+
restoredPredecessor = candidate.record;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1586
1660
|
let capacityUnavailable = false;
|
|
1587
1661
|
const workspaceIdentity = registration.cwd
|
|
1588
1662
|
? deps.topicTargetStore.claimWorkspaceIdentity(
|
|
@@ -914,6 +914,16 @@ export interface TelegramTopicTargetStore {
|
|
|
914
914
|
onCapacityUnavailable?: () => void;
|
|
915
915
|
},
|
|
916
916
|
) => TelegramWorkspaceBindingIdentity | undefined;
|
|
917
|
+
/**
|
|
918
|
+
* Re-keys one retained Thread binding from a retired session to its successor at the same slot and target,
|
|
919
|
+
* so the tab keeps a single owner. Fails without mutation when the source binding or slot identity is absent.
|
|
920
|
+
*/
|
|
921
|
+
adoptWorkspaceThreadBindingForSession: (
|
|
922
|
+
cwd: string,
|
|
923
|
+
sourceSessionId: string,
|
|
924
|
+
sessionId: string,
|
|
925
|
+
target: TelegramTarget,
|
|
926
|
+
) => boolean;
|
|
917
927
|
releaseWorkspaceClaim: (instanceId: string) => boolean;
|
|
918
928
|
upsertWorkspaceBinding: (
|
|
919
929
|
binding: TelegramWorkspaceThreadBinding,
|
|
@@ -4971,6 +4981,50 @@ export function createTelegramTopicTargetStore(
|
|
|
4971
4981
|
};
|
|
4972
4982
|
};
|
|
4973
4983
|
|
|
4984
|
+
/**
|
|
4985
|
+
* Re-keys one retained Thread binding from a retired session to its successor at the same slot and target,
|
|
4986
|
+
* so the tab keeps a single owner. Fails without mutation when the source binding or slot identity is absent.
|
|
4987
|
+
*/
|
|
4988
|
+
const rekeyWorkspaceBindingForSession = (
|
|
4989
|
+
cwd: string,
|
|
4990
|
+
sourceSessionId: string,
|
|
4991
|
+
sessionId: string,
|
|
4992
|
+
target: TelegramTarget,
|
|
4993
|
+
): boolean => {
|
|
4994
|
+
const sourceEntry = Array.from(workspaceBindings.entries()).find(
|
|
4995
|
+
([, binding]) =>
|
|
4996
|
+
binding.cwd === cwd &&
|
|
4997
|
+
binding.sessionId === sourceSessionId &&
|
|
4998
|
+
targetMatches(binding.target, target),
|
|
4999
|
+
);
|
|
5000
|
+
if (!sourceEntry) return false;
|
|
5001
|
+
const [sourceKey, sourceBinding] = sourceEntry;
|
|
5002
|
+
const replacementIdentity = Array.from({
|
|
5003
|
+
length: TELEGRAM_WORKSPACE_SLOTS.length,
|
|
5004
|
+
})
|
|
5005
|
+
.map((_, ordinal) =>
|
|
5006
|
+
createTelegramWorkspaceBindingIdentityWithKey(
|
|
5007
|
+
sourceBinding.cwd,
|
|
5008
|
+
sourceBinding.workspaceKey,
|
|
5009
|
+
ordinal,
|
|
5010
|
+
sessionId,
|
|
5011
|
+
),
|
|
5012
|
+
)
|
|
5013
|
+
.find((identity) => identity?.instanceSlot === sourceBinding.instanceSlot);
|
|
5014
|
+
if (
|
|
5015
|
+
!replacementIdentity ||
|
|
5016
|
+
replacementIdentity.instanceSlot !== sourceBinding.instanceSlot
|
|
5017
|
+
)
|
|
5018
|
+
return false;
|
|
5019
|
+
workspaceBindings.delete(sourceKey);
|
|
5020
|
+
workspaceBindings.set(getWorkspaceBindingMapKey(replacementIdentity), {
|
|
5021
|
+
...sourceBinding,
|
|
5022
|
+
...replacementIdentity,
|
|
5023
|
+
updatedAtMs: getNowMs(),
|
|
5024
|
+
});
|
|
5025
|
+
markDirty();
|
|
5026
|
+
return true;
|
|
5027
|
+
};
|
|
4974
5028
|
return {
|
|
4975
5029
|
async load() {
|
|
4976
5030
|
if (workspaceIO && dirty && !loadedScopeMatches(getPath()))
|
|
@@ -7505,6 +7559,31 @@ export function createTelegramTopicTargetStore(
|
|
|
7505
7559
|
const binding = workspaceBindings.get(mapKey);
|
|
7506
7560
|
return binding ? cloneWorkspaceBinding(binding) : undefined;
|
|
7507
7561
|
},
|
|
7562
|
+
adoptWorkspaceThreadBindingForSession(
|
|
7563
|
+
cwd,
|
|
7564
|
+
sourceSessionId,
|
|
7565
|
+
sessionId,
|
|
7566
|
+
target,
|
|
7567
|
+
) {
|
|
7568
|
+
if (workspaceRetirementCommitInFlight) return false;
|
|
7569
|
+
const normalizedCwd = normalizeTelegramWorkspacePath(cwd);
|
|
7570
|
+
const normalizedSource = normalizeTelegramSessionId(sourceSessionId);
|
|
7571
|
+
const normalizedSession = normalizeTelegramSessionId(sessionId);
|
|
7572
|
+
if (
|
|
7573
|
+
!normalizedCwd ||
|
|
7574
|
+
!normalizedSource ||
|
|
7575
|
+
!normalizedSession ||
|
|
7576
|
+
normalizedSource === normalizedSession ||
|
|
7577
|
+
hasWorkspaceRetirementConflict({ cwd: normalizedCwd })
|
|
7578
|
+
)
|
|
7579
|
+
return false;
|
|
7580
|
+
return rekeyWorkspaceBindingForSession(
|
|
7581
|
+
normalizedCwd,
|
|
7582
|
+
normalizedSource,
|
|
7583
|
+
normalizedSession,
|
|
7584
|
+
target,
|
|
7585
|
+
);
|
|
7586
|
+
},
|
|
7508
7587
|
claimWorkspaceIdentity(cwd, instanceId, previousInstanceId, options) {
|
|
7509
7588
|
if (workspaceRetirementCommitInFlight) return undefined;
|
|
7510
7589
|
const normalizedCwd = normalizeTelegramWorkspacePath(cwd);
|
|
@@ -7539,41 +7618,18 @@ export function createTelegramTopicTargetStore(
|
|
|
7539
7618
|
targetMatches(binding.target, replacement.target),
|
|
7540
7619
|
);
|
|
7541
7620
|
if (sourceEntry) {
|
|
7542
|
-
const
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
})
|
|
7546
|
-
.map((_, ordinal) =>
|
|
7547
|
-
createTelegramWorkspaceBindingIdentityWithKey(
|
|
7548
|
-
sourceBinding.cwd,
|
|
7549
|
-
sourceBinding.workspaceKey,
|
|
7550
|
-
ordinal,
|
|
7551
|
-
normalizedSessionId,
|
|
7552
|
-
),
|
|
7553
|
-
)
|
|
7554
|
-
.find(
|
|
7555
|
-
(identity) =>
|
|
7556
|
-
identity?.instanceSlot === sourceBinding.instanceSlot,
|
|
7557
|
-
);
|
|
7621
|
+
const existingTargetRecord = Array.from(records.values()).find(
|
|
7622
|
+
(record) => targetMatches(record.target, replacement.target),
|
|
7623
|
+
);
|
|
7558
7624
|
if (
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
)
|
|
7625
|
+
rekeyWorkspaceBindingForSession(
|
|
7626
|
+
normalizedCwd,
|
|
7627
|
+
replacement.sourceSessionId,
|
|
7628
|
+
normalizedSessionId,
|
|
7629
|
+
replacement.target,
|
|
7630
|
+
)
|
|
7631
|
+
)
|
|
7565
7632
|
replacementPreviousInstanceId = existingTargetRecord?.instanceId;
|
|
7566
|
-
workspaceBindings.delete(sourceKey);
|
|
7567
|
-
workspaceBindings.set(
|
|
7568
|
-
getWorkspaceBindingMapKey(replacementIdentity),
|
|
7569
|
-
{
|
|
7570
|
-
...sourceBinding,
|
|
7571
|
-
...replacementIdentity,
|
|
7572
|
-
updatedAtMs: getNowMs(),
|
|
7573
|
-
},
|
|
7574
|
-
);
|
|
7575
|
-
markDirty();
|
|
7576
|
-
}
|
|
7577
7633
|
}
|
|
7578
7634
|
}
|
|
7579
7635
|
const effectivePreviousInstanceId =
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -29,8 +29,10 @@
|
|
|
29
29
|
"scripts": {
|
|
30
30
|
"test": "node --test test/*.test.mjs",
|
|
31
31
|
"audit": "npm audit --omit=dev",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
32
|
+
"deps:check": "node scripts/check-updates.mjs",
|
|
33
|
+
"check:coherence": "node scripts/validate.mjs",
|
|
34
|
+
"pack:check": "node scripts/validate.mjs --pack",
|
|
35
|
+
"validate": "npm run check:coherence && npm test && npm run audit && npm run pack:check"
|
|
34
36
|
},
|
|
35
37
|
"files": [
|
|
36
38
|
"extensions/",
|
|
@@ -48,7 +50,7 @@
|
|
|
48
50
|
"@llblab/pi-codex-usage": "0.13.1",
|
|
49
51
|
"@llblab/pi-grow-loop": "0.9.0",
|
|
50
52
|
"@llblab/pi-state-flow": "0.26.4",
|
|
51
|
-
"@llblab/pi-telegram": "0.57.
|
|
53
|
+
"@llblab/pi-telegram": "0.57.3",
|
|
52
54
|
"@llblab/skills": "1.15.0"
|
|
53
55
|
},
|
|
54
56
|
"bundledDependencies": [
|