@llblab/pi-kit 0.17.1 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-state-flow/README.md +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +3 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/package.json +5 -5
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +27 -82
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +3 -3
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/package.json +5 -5
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +3 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +40 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +32 -30
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +46 -9
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +39 -29
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
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.18.1 - 2026-09-20
|
|
6
|
+
|
|
7
|
+
- `Passive State Flow Guidance`: Advances the exact State Flow pin to `0.16.3`, requiring finalization barriers only in active episodes while preventing gratuitous final-only patches during passive memory use.
|
|
8
|
+
- `Compatibility Contract`: Carries the minimum Pi SDK version `0.84.4` without an artificial upper peer-dependency bound and removes stale compatibility evidence without dropping supported storage migrations or current safety checks.
|
|
9
|
+
- `Package Cohort`: Keeps every other bundled package at its current exact version; the package set, resource inventory, and explicit load order remain unchanged.
|
|
10
|
+
|
|
11
|
+
## 0.18.0 - 2026-09-18
|
|
12
|
+
|
|
13
|
+
- `Telegram Lifecycle Gateway`: Advances the exact Telegram pin to `0.50.0`, replacing the session-specific technical command with one guarded internal gateway for runtime-armed lifecycle actions while preserving `/new` settlement, continuity, and terminal-result guarantees.
|
|
14
|
+
- `Thinking Cadence`: Buffers the first Telegram thinking frame for two seconds and throttles later updates to the same two-second cadence as answer drafts, while still flushing remaining reasoning when the block completes.
|
|
15
|
+
- `Package Cohort`: Keeps every other bundled package at its current exact version; the package set, resource inventory, and explicit load order remain unchanged.
|
|
16
|
+
|
|
5
17
|
## 0.17.1 - 2026-09-18
|
|
6
18
|
|
|
7
19
|
- `State Flow Activation Hotfixes`: Advances the exact State Flow pin to `0.16.2`, allowing resumed activation to reassert its selected session cohort and allowing passive global memory before a new CWD has materialized, while retaining fail-closed handling for genuinely incomplete ownership.
|
package/README.md
CHANGED
|
@@ -14,8 +14,8 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
14
14
|
| [`@llblab/pi-clean-room`](https://github.com/llblab/pi-clean-room) | `0.1.1` | Isolated nested Pi TUI with explicitly selected extensions |
|
|
15
15
|
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.10.0` | Compact Codex/Spark subscription-limit and Business credit-usage status |
|
|
16
16
|
| [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.8.1` | Visible continuation scheduling and bounded worker Skills |
|
|
17
|
-
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.16.
|
|
18
|
-
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.
|
|
17
|
+
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.16.3` | Incremental scoped state/context/memory compiler with mode-aware passive guidance, proportional operational guidance, intentional agency, reactive dangling-reference diagnostics, safe compaction, and exact publication |
|
|
18
|
+
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.50.0` | Telegram companion with native fresh-session replacement, a guarded internal lifecycle gateway, buffered thinking cadence, adaptive Thread continuity, exact queues, files, voice, controls, and Generative Apps guidance |
|
|
19
19
|
| [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
|
|
20
20
|
|
|
21
21
|
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.
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
- Explicit `/state-flow-start` creates the State Flow directory when missing and returns after locally usable runtime acceptance for normal `turn-end`/`off` policy. If Git is installed, initialize an exact-root Git repository when needed, including a populated file-only store, preserving all existing bytes and unrelated files; an ancestor repository is not a valid substitute. Skip full predecessor-format migration planning only when all exact legacy snapshot names are absent, and defer Markdown freshness discovery until before the next enabled inference. If Git is absent, use file persistence. Manual-mode startup/status/restore do not initialize Git; configured automatic start of a genuinely new session uses the same initialization as explicit start. Never create external accounts, remote repositories, credentials, or remote configuration; those remain operator-owned. Never auto-import, delete, or reset files/history in a previous Knowledge-backed store; old branch revisions require their original Git history to remain available in the selected store. `/state-flow-start` must initialize missing global, CWD, and current-session checkpoint/tail pairs plus session config/meta through compare-and-swap publication when required, enable only the current session branch, and bootstrap prior conversation when needed. Explicit start on a proven pre-runtime branch (no checkpoint or an ordinary-disabled marker) establishes an empty session origin rather than importing a later same-session layer; validate existing runtime identity, retain shared streams unchanged, and preserve later branch data in cold Git history. Ordinary new sessions remain manual unless agent-level `autoStart` is true; CWD materialization alone grants no automatic activation. Configured new sessions may initialize missing CWD state and receive distinct empty session layers while inheriting global/CWD values. Resumed and tree-selected branches restore their own config and temporal lineage, regardless of the global flag.
|
|
45
45
|
- `/state-flow-stop` returns to the configured passive bootstrap/tool combination and must persist only the current session/branch's `config.enabled = false` and necessary runtime provenance, preserving all semantic checkpoints/tails and creating no semantic transition. Retain a frozen handoff plus the active user-run trajectory (including later tool results), post-stop conversation, and foreign context-bearing custom messages across same-physical-session reload/resume/tree. Exclude completed pre-run conversation and private State Flow validation feedback; idle Stop does not retain the completed run. Active restart replaces passive mode but uses that bounded boundary for its one migration run; new and forked physical sessions inherit neither projection. It does not rewrite agent-level `autoStart` or change its policy for future new sessions.
|
|
46
46
|
- After an accepted non-bootstrap run settles with no pending input, State Flow may request native manual compaction under a generation-private marker only when public `getContextUsage()` reports at least 24,000 tokens. Keep the complete latest accepted user iteration, store only revision/step identity in details, and never duplicate state in the summary. Unknown or smaller usage skips compaction; a benign native refusal releases the attempt for later work. Skip prefixes containing foreign custom context. Never customize user manual or native threshold/overflow compaction, discard unfinished pre-patch work, create a State Flow origin, or rewrite Pi's append-only JSONL/tree.
|
|
47
|
-
- Keep the injected runtime protocol compact and normative; put rationale and extended explanation in README rather than the model prompt.
|
|
47
|
+
- Keep the injected runtime protocol compact and normative; put rationale and extended explanation in README rather than the model prompt. Foreign comment handling remains owned by other extensions.
|
|
48
48
|
- Do not claim strict boundedness for state, the current run trajectory, the turn specification, or the external full trace.
|
|
49
49
|
- Remain extension-agnostic in core semantics, storage, and inference: core modules never import, name, special-case, or encode policy for another extension or transport. One optional leaf presentation adapter (`lib/telegram.ts`) may import public `pi-telegram` membranes to mirror the live status on exactly one main-menu section button and expose the same start/stop affordances already owned by `state-flow-start` and `state-flow-stop`; it must fail open when the transport is absent or its registry is unready and must never alter core behavior.
|
|
50
50
|
- Activate State Flow model tools while an episode is enabled or passive tools are configured; preserve every unrelated active tool when toggling them. Passive reads never initialize storage, while an explicit passive patch may initialize or migrate storage without enabling episode barriers, continuation, or compaction. Keep mutation confined to `patch_state` and historical observation read-only.
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.16.3: Passive guidance and compatibility cleanup
|
|
8
|
+
|
|
9
|
+
- `Passive finalization guidance`: Makes the always-available `patch_state` tool contract mode-aware, requiring `final:true` only for active episodes and explicitly forbidding final-only calls in passive turns so tool-level guidance no longer contradicts passive runtime context.
|
|
10
|
+
- `Pi compatibility range`: Declares `0.84.4` as the minimum Pi SDK version without an upper peer-dependency bound, replaces stale candidate reports and historical run internals with a concise tested-stack contract, and removes dead legacy test scaffolding plus retired HTML-envelope fixtures without dropping supported storage migrations or current negative-boundary coverage.
|
|
11
|
+
|
|
7
12
|
## 0.16.2: Passive first-CWD memory hotfix
|
|
8
13
|
|
|
9
14
|
- `Passive first-CWD memory`: Treats global-only storage as a valid passive-memory state before the current CWD has ever materialized, projecting an empty CWD overlay instead of warning that shared storage is incomplete; CWD-without-global storage still fails closed.
|
|
@@ -26,7 +26,7 @@ The agent curates what matters; the extension validates, persists, and projects
|
|
|
26
26
|
|
|
27
27
|
## Quick start
|
|
28
28
|
|
|
29
|
-
Requires Pi `0.84.4
|
|
29
|
+
Requires Pi `0.84.4` or newer and Node.js `22.19.0` or newer. There is no declared upper Pi version bound; see the [SDK compatibility matrix](docs/compatibility.md) for exact tested stacks. Git is optional; using it requires a configured commit identity.
|
|
30
30
|
|
|
31
31
|
From npm:
|
|
32
32
|
|
|
@@ -619,10 +619,10 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
619
619
|
pi.registerTool({
|
|
620
620
|
name: PATCH_STATE_TOOL_NAME,
|
|
621
621
|
label: "Patch State",
|
|
622
|
-
description: "The sole State Flow semantic mutation protocol. Supply any combination of global, cwd, and session patches; all supplied scopes commit atomically.
|
|
623
|
-
promptSnippet: "Atomically patch global/cwd/session; final:true
|
|
622
|
+
description: "The sole State Flow semantic mutation protocol. Supply any combination of global, cwd, and session patches; all supplied scopes commit atomically. In an active State Flow episode, set final:true when the current iteration may finish at a later turn_end; use {final:true} when no semantic update is needed. Passive turns have no terminal barrier: never call patch_state only to set final:true. This call must be the only State Flow barrier in its assistant response.",
|
|
623
|
+
promptSnippet: "Atomically patch global/cwd/session; active episodes use final:true before turn_end",
|
|
624
624
|
promptGuidelines: [
|
|
625
|
-
"Use patch_state for durable semantic changes.
|
|
625
|
+
"Use patch_state for durable semantic changes. Only in an active State Flow episode, make the iteration terminal-eligible before a final answer with final:true, optionally alongside atomic global/cwd/session patches. In passive mode, never call patch_state only to set final:true.",
|
|
626
626
|
"Use patch_state as reconciliation, not append-only notes: place new knowledge at the narrowest valid scope and remove superseded or completed state from touched branches.",
|
|
627
627
|
"Call patch_state alone in an assistant response; after its acknowledgement, further reasoning, tools, and later patch_state calls remain allowed.",
|
|
628
628
|
],
|
|
@@ -10,5 +10,5 @@ export declare function separatedFailure(error: unknown): Error;
|
|
|
10
10
|
/** The compact model-facing contract. Semantic writes never travel through terminal prose. */
|
|
11
11
|
export declare function stateFlowProtocol(bootstrap: boolean): string;
|
|
12
12
|
export declare function assistantToolCallCount(content: unknown): number;
|
|
13
|
-
/** The post-handler assistant
|
|
13
|
+
/** The accepted post-handler assistant text is authoritative. */
|
|
14
14
|
export declare function finalizedAssistantResponse(message: AgentMessage): string;
|
|
@@ -89,7 +89,7 @@ export function assistantToolCallCount(content) {
|
|
|
89
89
|
return 0;
|
|
90
90
|
return content.filter((block) => typeof block === "object" && block !== null && block.type === "toolCall").length;
|
|
91
91
|
}
|
|
92
|
-
/** The post-handler assistant
|
|
92
|
+
/** The accepted post-handler assistant text is authoritative. */
|
|
93
93
|
export function finalizedAssistantResponse(message) {
|
|
94
94
|
if (message.role !== "assistant" || !Array.isArray(message.content)) {
|
|
95
95
|
throw new Error("Finalized State Flow turn does not contain an assistant response");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-state-flow",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Incremental scoped state/context/memory compiler for Pi, inspired by SKILL.state",
|
|
6
6
|
"keywords": [
|
|
@@ -66,10 +66,10 @@
|
|
|
66
66
|
"node": ">=22.19.0"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
|
-
"@earendil-works/pi-agent-core": "
|
|
70
|
-
"@earendil-works/pi-ai": "
|
|
71
|
-
"@earendil-works/pi-coding-agent": "
|
|
72
|
-
"@earendil-works/pi-tui": "
|
|
69
|
+
"@earendil-works/pi-agent-core": ">=0.84.4",
|
|
70
|
+
"@earendil-works/pi-ai": ">=0.84.4",
|
|
71
|
+
"@earendil-works/pi-coding-agent": ">=0.84.4",
|
|
72
|
+
"@earendil-works/pi-tui": ">=0.84.4"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@earendil-works/pi-tui": "0.84.4",
|
|
@@ -1,101 +1,46 @@
|
|
|
1
1
|
# Pi SDK compatibility
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
State Flow requires matching Pi SDK packages at `>=0.84.4` without an upper peer-dependency bound. Keep `pi-coding-agent`, `pi-agent-core`, `pi-ai`, and `pi-tui` on the same release line.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The peer range permits newer releases so npm does not impose an artificial ceiling. It does not claim that every future SDK release has been tested. Revalidate the public host seams below when adopting a new Pi release line.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Tested stacks
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
The current repository-local stack uses Linux/x64, Node 26.8.1, Git 2.55.0, and Pi SDK 0.84.4.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
| Pi SDK stack | Validation | Evidence status |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| 0.84.4 | Build, typecheck, import, package dry-run, 445/445 tests | Current full-suite baseline |
|
|
14
|
+
| 0.85.1 | Build, typecheck, import, full suite | Earlier compatibility baseline; not rerun for every later State Flow change |
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
| --- | --- | --- | --- |
|
|
15
|
-
| 0.84.4 / 0.84.4 / 0.84.4 | 0.84.4 / 1.3.7 | Pass / pass | 417/417 |
|
|
16
|
-
| 0.85.1 / 0.85.1 / 0.85.1 | 0.85.1 / 1.3.7 | Pass / pass | 417/417 |
|
|
17
|
-
|
|
18
|
-
The first row is the repository-local stack. The second used a disposable copy of the same working source, with read-only dependency links to the already installed 0.85.1 SDK and its actual AI/agent-core/pi-tui graph. All commands exited zero for the accepted results. The repository lockfile still resolves 0.84.4; broadening its root peer metadata did not install or upgrade dependencies. No live Pi process, installation, production session or state store was modified.
|
|
19
|
-
|
|
20
|
-
Both stacks passed `npm run validate` after adding the four [fatal-writer interruption witnesses](temporal-acceptance.md#fatal-writer-interruption). No runtime correction was needed: runtime SHA-256 remains `ce820cd33c34c4c882c11fc55e93f3914cfc8dffae57683e32cdcaa70f225cd2` from the [context-copy correction](performance.md#context-projection-and-trajectory-selection). Sorted `index.ts`, `lib/*.ts` and `tests/*.ts`, framed as path + NUL + bytes + NUL, now yield `1c5fafc1433631fed33177381e925eeeca52827932ba327690f615d2a4db81ba`. Copied/original source, runtime, test and package bytes were checked unchanged through validation, along with both selected dependency-resolution graphs and their manifests. These hashes do not cover every installed binary or environmental influence. The [benchmark workload fingerprint](performance.md) remains `b861fae36798bf76f56f411cc654c70fc948597bb84cc7a422c83f69ba1ca3e9`. Subsequent [matched runtime-performance controls](performance.md#matched-final-candidate-controls) use the 0.84.4 graph only, not a two-SDK timing comparison. The earlier fork/context witnesses remain included; these results do not replace separate integrated-candidate acceptance.
|
|
21
|
-
|
|
22
|
-
### Post-measurement integrated acceptance
|
|
23
|
-
|
|
24
|
-
After the nine-invocation performance series, inline review traced accumulated Git/CAS, provenance, worker ownership, fork/recovery, Stop/context and discovery changes against their callers and negative/native witnesses, without a confirmed blocker in that inspected closure. Separate `state-flow-integrated-0844` and `state-flow-integrated-0851` Runs repeated `npm run validate`: each passed 417/417 with no failed, cancelled, skipped or todo tests, plus typecheck and import-check, and actual command exit zero. Full captures are 47,957 and 47,950 bytes. All 78 selected runtime/test/package files and both actual dependency graphs remained unchanged; documentation is separately reviewed rather than represented by that source hash.
|
|
25
|
-
|
|
26
|
-
The twenty-property map resolves its quoted witnesses; that structural check is not exhaustive semantic proof. Domain validation reports 31 source files/133 acyclic local edges and no reverse entrypoint imports, with 25 existing header warnings. Context validation reports zero errors/five warnings. The package dry run includes 44 files and no tests. This accepts the local candidate within the documented synthetic/Linux/SDK boundaries, not arbitrary-host compatibility, production-incident attribution, publication permission or remote release-CI success. That checkpoint preceded the final compaction/status slice and its intentional 0.10.0 version alignment.
|
|
27
|
-
|
|
28
|
-
The final slice adds seven compaction policy/race tests and two native lifecycle witnesses, bringing the complete repository-local 0.84.4 suite to 426/426 with typecheck/import-check. On 0.85.1, all affected policy/status and native compaction/tree tests plus typecheck pass; the operator declined a redundant second full-suite repetition after this focused equivalence evidence. Native completed-history compaction preserves full JSONL/tree/state/UUID while shortening active/resumed entries without another model summary; threshold compaction before the first patch remains Pi-owned. Package version is 0.10.0. Remote release CI remains authoritative for the tagged tree.
|
|
29
|
-
|
|
30
|
-
Matching versions do not imply one physical dependency instance. Before/after resolution walks retain separate root and SDK-local 0.84.4 AI/agent-core copies and three TypeBox 1.3.7 locations in the repository installation. The first row describes that actual graph, not a deduplicated installation. The disposable 0.85.1 copy explicitly shares the SDK's AI/agent-core/pi-tui instances. Record canonical resolution edges as well as versions for comparisons; no dependency installation was changed to force a verifier assumption.
|
|
31
|
-
|
|
32
|
-
The focused parent-traversal and barrier cohort passed 3/3 on each stack. The earlier 0.85.1 native cohort passed 10/10: large state/answers, selected-tree restoration, quit/reload push ownership, ordinary/bootstrap mid-tool Stop, compaction/tree/restart, scoped barriers, historical reads, and sibling-tool rejection. The full suite includes those witnesses plus benchmark, persistence, provenance, migration and concurrency contracts; tests are available in a source checkout, not the published runtime package.
|
|
16
|
+
Only exact matching stacks that were actually exercised are test evidence. Mixed SDK versions and untested newer releases are permitted by package metadata but remain unverified.
|
|
33
17
|
|
|
34
18
|
## Public host seams
|
|
35
19
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- `session-manager.js` / `session-manager.d.ts`: Read-only extension context exposes `getLeafEntry()`, `getEntry(id)` and each entry's `parentId`. Both lookups use the native ID map; `getBranch()` instead walks to the root and reverses a newly allocated path. State Flow now uses that public parent traversal for preflight without deleting or replacing native history.
|
|
39
|
-
- `agent-session.js`, `_handleAgentEvent()` / `_installAgentToolHooks()`, and agent-core's `agent-loop.js`: `message_end` extension handlers run before session persistence; the awaited event then appends the accepted assistant message before tool preflight. Inspect that synchronized current assistant, not a presumed last persisted assistant inside `message_end`. The native sibling-tool regression is the executable barrier witness.
|
|
40
|
-
- `sdk.js` and `extensions/runner.js`, `emitContext()`: Pi connects context transformation to the extension runner, which first `structuredClone`s native messages. State Flow projection runs after that host copy. Reducing extension traversal cannot make total inference work independent of history size.
|
|
41
|
-
- `agent-session.js`, `prompt()` / `reload()` / `dispose()`: The system prompt is composed at `before_agent_start`; Stop can change tools/projection but not that already composed prompt for the current run. Reload awaits `session_shutdown` before replacement. Bare disposal invalidates/disconnects without emitting shutdown; use the [embedding contract](architecture.md#embedding).
|
|
42
|
-
- `agent-session-runtime.js`, `teardownCurrent()`: Owner-driven new/resume/fork aborts the outgoing session, awaits shutdown, then disposes and creates the replacement. Active-push witnesses now cover new, same-file resume and fork teardown before invalidation, alongside quit and actual reload. Correct teardown does not imply successful fork restoration.
|
|
43
|
-
- `sdk.js`, `createAgentSession()`: Session selection precedes default resource/extension loading. No extension-only pre-session resolver seam was established; native default continuation remains an [external integration boundary](architecture.md#session-continuation).
|
|
44
|
-
|
|
45
|
-
These observations establish the specific seams used here, not compatibility with every Pi UI mode, extension combination, provider or operating system. Native tests use deterministic faux inference, temporary sessions/stores and local remotes. An installed SDK version does not fingerprint a still-running host. Actual-process incident attribution remains unproven; final comparable measurements and integrated acceptance remain in [BACKLOG.md](../BACKLOG.md).
|
|
46
|
-
|
|
47
|
-
## Native replacement witnesses
|
|
20
|
+
State Flow depends on these public Pi SDK behaviors:
|
|
48
21
|
|
|
49
|
-
|
|
22
|
+
- Extension lifecycle events and branch metadata for start, stop, reload, resume, fork, and tree navigation.
|
|
23
|
+
- Read-only session parent traversal through `getLeafEntry()` and `getEntry(id)`.
|
|
24
|
+
- `message_end` and `turn_end` ordering before accepted assistant messages are reconciled.
|
|
25
|
+
- Context transformation through the extension runner.
|
|
26
|
+
- `getContextUsage()` and native compaction hooks.
|
|
27
|
+
- Sequential tool execution and tool-call preflight.
|
|
28
|
+
- Session replacement awaiting outgoing shutdown before invalidation.
|
|
50
29
|
|
|
51
|
-
|
|
52
|
-
- The exact shutdown reason/target, child exit and a claimable lease at `setBeforeSessionInvalidate()`, before a successor can start.
|
|
53
|
-
- Byte-identical outgoing JSONL and session checkpoint/tail/config/meta, the same selected leaf, and exact cold state at the accepted revision.
|
|
54
|
-
- An empty private layer plus inherited shared state for configured new sessions, and exact selected state/identity for same-file resume.
|
|
55
|
-
- No old-generation relaunch or queue writes after replacement; only a valid successor attempts the exact pending target and acknowledges its controlled child result.
|
|
30
|
+
Compatibility with those seams does not prove every UI mode, provider, operating system, extension combination, or future SDK release.
|
|
56
31
|
|
|
57
|
-
|
|
32
|
+
## Validation procedure
|
|
58
33
|
|
|
59
|
-
|
|
34
|
+
Validate another Pi SDK line in an isolated copy so the live extension, sessions, and runtime store remain unchanged:
|
|
60
35
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
1. Copy the intended current source, including retained uncommitted files and tests, to a fresh temporary directory. Do not substitute baseline `HEAD` for a dirty candidate. Give the copy its own synthetic Git `HEAD` for benchmark source-identity checks; never copy production session/store data.
|
|
68
|
-
2. Create a real `node_modules` directory in the copy. Link the existing development dependencies, then link `@earendil-works/pi-coding-agent` to the selected installed SDK and AI/agent-core/pi-tui to the dependencies actually resolved by that SDK. Do not use `--preserve-symlinks` or install into the live package. Verify the SDK and extension resolve the same selected dependency instances.
|
|
69
|
-
3. Resolve import-only package manifests with `findPackageJSON(name, pathToFileURL(ownerManifest))`, not `require.resolve(name)`. Record canonical manifest paths, package names/versions and hashes before and after testing. For example, run the following inside the copy:
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
node --input-type=module <<'JS'
|
|
73
|
-
import { findPackageJSON } from 'node:module';
|
|
74
|
-
import { readFileSync, realpathSync } from 'node:fs';
|
|
75
|
-
import { pathToFileURL } from 'node:url';
|
|
76
|
-
const owner = pathToFileURL(`${process.cwd()}/package.json`);
|
|
77
|
-
for (const name of ['pi-coding-agent', 'pi-ai', 'pi-agent-core', 'pi-tui']) {
|
|
78
|
-
const path = realpathSync(findPackageJSON(`@earendil-works/${name}`, owner));
|
|
79
|
-
console.log(path, JSON.parse(readFileSync(path, 'utf8')).version);
|
|
80
|
-
}
|
|
81
|
-
JS
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Use a fresh `PI_CODING_AGENT_DIR`, `PI_OFFLINE=1`, `GIT_CONFIG_NOSYSTEM=1` and a temporary `GIT_CONFIG_GLOBAL` containing only a synthetic commit identity and disabled commit signing. Fixtures already supply in-memory credentials, no model-catalog refresh and explicit resource/session roots. Leave fixture-local Git hooks enabled: tests intentionally use temporary `pre-receive`/`post-receive` hooks.
|
|
85
|
-
|
|
86
|
-
Inside that prepared copy, the compatibility checks are ordinary project commands:
|
|
36
|
+
1. Copy the complete candidate source, including retained uncommitted changes, into a temporary directory.
|
|
37
|
+
2. Install or link one matching version of `pi-coding-agent`, `pi-agent-core`, `pi-ai`, and `pi-tui`.
|
|
38
|
+
3. Confirm all four packages resolve to the intended release line.
|
|
39
|
+
4. Use isolated Pi agent/session directories and synthetic Git identity; do not copy production session or State Flow data.
|
|
40
|
+
5. Run:
|
|
87
41
|
|
|
88
42
|
```bash
|
|
89
|
-
npm run
|
|
90
|
-
npm run check
|
|
91
|
-
node --experimental-strip-types --test \
|
|
92
|
-
--test-name-pattern='tool preflight walks only|patch_state is the only tool|real Pi executes only patch_state|replacement closes its outgoing publisher' \
|
|
93
|
-
tests/extension.test.ts tests/integration.test.ts
|
|
94
|
-
npm test
|
|
43
|
+
npm run validate
|
|
95
44
|
```
|
|
96
45
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
### Rejected setup and correction
|
|
100
|
-
|
|
101
|
-
The first 0.85.1 full run passed 395/397 because the temporary global Git config incorrectly set `core.hooksPath` to an empty directory. This disabled two fixtures' hooks: prepared receipts no longer observed the synthetic post-push write, and file-to-Git adoption no longer observed the intentionally pending push. The same two failures reproduced on 0.84.4 with that config. Removing only the temporary override made both witnesses pass on both SDKs, followed by 397/397 on 0.85.1. No runtime or test correction was needed; the failed run is not an SDK incompatibility or a successful full-suite result.
|
|
46
|
+
A successful focused test does not replace the full suite. Record the exact dependency graph and command exit for any compatibility claim.
|
|
@@ -131,7 +131,7 @@ Native controls have two rather than three inferences per request and no State F
|
|
|
131
131
|
- **Short runs are not uniformly faster:** At twenty 8 KiB requests, B2's 1132.5ms p50 exceeds A1's 1115.7ms despite fewer calls. Both B short-case p95 values exceed A1's 1140.8ms. A1/A2 also change materially without code changes: long 8 KiB run p50 is 1373.7 → 1165.1ms, while short 128 KiB is 1602.8 → 1234.7ms. Keep these observations rather than collapsing the controls into one average.
|
|
132
132
|
- **No history-independent latency claim:** B1's first-context medians rise 316.6 → 387.4ms and 297.6 → 347.5ms with unchanged 31 calls; B2's 128 KiB case rises 311.9 → 388.2ms. Identical counts do not bound internal Git work, host cloning, allocation or scheduling. These samples do not isolate the cause.
|
|
133
133
|
- **Projection and native trace remain distinct:** First resumed enabled contexts are exactly 9694 / 9712 bytes at 8 KiB and 132574 / 132592 at 128 KiB across all four invocations. Native first-context medians grow from about 105 KB to 1.054 MB. Preserving a compact delivered context does not bound semantic state, the full native trace or Pi's pre-projection history clone.
|
|
134
|
-
- **Remaining evidence is separate:** This block covers the 4 KiB-read lifecycle comparison only. The larger-read pair and current large-state boundary follow below; the publisher pair also follows.
|
|
134
|
+
- **Remaining evidence is separate:** This block covers the 4 KiB-read lifecycle comparison only. The larger-read pair and current large-state boundary follow below; the publisher pair also follows. Repository validation is recorded independently from these measurements. It establishes neither the production incident's cause nor real-provider latency or universal responsiveness.
|
|
135
135
|
|
|
136
136
|
### Larger delivered history: matched A/B pair
|
|
137
137
|
|
|
@@ -191,7 +191,7 @@ Reported mixed-attempt p50/p95 milliseconds are A/B: same-session 3.1/465.9 vers
|
|
|
191
191
|
|
|
192
192
|
All nine planned invocations are accepted, sequential and source-bound; the seven lifecycle invocations contain 108 preserved copied probes, excluding six setup probes. Complete captures, actual exits and guards are retained with `/tmp/state-flow-final-controls-CzaHqM/final-series-receipts.json`. Lifecycle distributions were independently recomputed from retained samples; publisher aggregates have the narrower evidence boundary above.
|
|
193
193
|
|
|
194
|
-
The candidate reduces proven Git/allocation work and shows lower enabled long-case medians in these controls while preserving selected state and accepted continuation. It does not eliminate native history handling, payload cost, timing variability or safe contention refusal. Adverse short-case/tail observations remain. No further runtime correction is justified by these measurements alone; original production slowdown attribution remains unproven. Separate
|
|
194
|
+
The candidate reduces proven Git/allocation work and shows lower enabled long-case medians in these controls while preserving selected state and accepted continuation. It does not eliminate native history handling, payload cost, timing variability or safe contention refusal. Adverse short-case/tail observations remain. No further runtime correction is justified by these measurements alone; original production slowdown attribution remains unproven. Separate repository validation, not this measurement closure, governs local candidate readiness; release authorization and remote CI remain separate gates.
|
|
195
195
|
|
|
196
196
|
The following older datasets retain their own source identities and adverse observations; they are not substituted for this matched final-runtime comparison.
|
|
197
197
|
|
|
@@ -24,7 +24,7 @@ The child starts at step zero and a new temporal origin. Its copied tail is pres
|
|
|
24
24
|
|
|
25
25
|
Initial copying requires a native fork start event, a regular canonical direct-parent session file, matching CWD/identity, a readable temporal source and an unused child namespace. Missing/unsafe evidence or CAS conflicts leave the copy unavailable rather than importing unrelated or newer private state. Explicit Start can retry an unaccepted copy in the same loaded fork after the cause is corrected.
|
|
26
26
|
|
|
27
|
-
Selecting a copied parent checkpoint through the child's `/tree` does not make it child-owned: State Flow stays disabled without resetting existing child data. Select a child-owned checkpoint or resume the original session. Cold recovery before the first child checkpoint, startup paths lacking the fork event, in-memory parent locators and cross-CWD imports remain outside this slice. File-only copying requires an exact still-available source cohort. See the [contract](fork-contract.md) and [SDK
|
|
27
|
+
Selecting a copied parent checkpoint through the child's `/tree` does not make it child-owned: State Flow stays disabled without resetting existing child data. Select a child-owned checkpoint or resume the original session. Cold recovery before the first child checkpoint, startup paths lacking the fork event, in-memory parent locators and cross-CWD imports remain outside this slice. File-only copying requires an exact still-available source cohort. See the [contract](fork-contract.md) and [SDK compatibility boundary](compatibility.md#public-host-seams); do not rewrite UUIDs or delete pointers to force recovery.
|
|
28
28
|
|
|
29
29
|
## Configuration
|
|
30
30
|
|
|
@@ -662,10 +662,10 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
662
662
|
pi.registerTool({
|
|
663
663
|
name: PATCH_STATE_TOOL_NAME,
|
|
664
664
|
label: "Patch State",
|
|
665
|
-
description: "The sole State Flow semantic mutation protocol. Supply any combination of global, cwd, and session patches; all supplied scopes commit atomically.
|
|
666
|
-
promptSnippet: "Atomically patch global/cwd/session; final:true
|
|
665
|
+
description: "The sole State Flow semantic mutation protocol. Supply any combination of global, cwd, and session patches; all supplied scopes commit atomically. In an active State Flow episode, set final:true when the current iteration may finish at a later turn_end; use {final:true} when no semantic update is needed. Passive turns have no terminal barrier: never call patch_state only to set final:true. This call must be the only State Flow barrier in its assistant response.",
|
|
666
|
+
promptSnippet: "Atomically patch global/cwd/session; active episodes use final:true before turn_end",
|
|
667
667
|
promptGuidelines: [
|
|
668
|
-
"Use patch_state for durable semantic changes.
|
|
668
|
+
"Use patch_state for durable semantic changes. Only in an active State Flow episode, make the iteration terminal-eligible before a final answer with final:true, optionally alongside atomic global/cwd/session patches. In passive mode, never call patch_state only to set final:true.",
|
|
669
669
|
"Use patch_state as reconciliation, not append-only notes: place new knowledge at the narrowest valid scope and remove superseded or completed state from touched branches.",
|
|
670
670
|
"Call patch_state alone in an assistant response; after its acknowledgement, further reasoning, tools, and later patch_state calls remain allowed.",
|
|
671
671
|
],
|
|
@@ -91,7 +91,7 @@ export function assistantToolCallCount(content: unknown): number {
|
|
|
91
91
|
return content.filter((block) => typeof block === "object" && block !== null && (block as { type?: unknown }).type === "toolCall").length;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
/** The post-handler assistant
|
|
94
|
+
/** The accepted post-handler assistant text is authoritative. */
|
|
95
95
|
export function finalizedAssistantResponse(message: AgentMessage): string {
|
|
96
96
|
if (message.role !== "assistant" || !Array.isArray(message.content)) {
|
|
97
97
|
throw new Error("Finalized State Flow turn does not contain an assistant response");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-state-flow",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Incremental scoped state/context/memory compiler for Pi, inspired by SKILL.state",
|
|
6
6
|
"keywords": [
|
|
@@ -66,10 +66,10 @@
|
|
|
66
66
|
"node": ">=22.19.0"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
|
-
"@earendil-works/pi-agent-core": "
|
|
70
|
-
"@earendil-works/pi-ai": "
|
|
71
|
-
"@earendil-works/pi-coding-agent": "
|
|
72
|
-
"@earendil-works/pi-tui": "
|
|
69
|
+
"@earendil-works/pi-agent-core": ">=0.84.4",
|
|
70
|
+
"@earendil-works/pi-ai": ">=0.84.4",
|
|
71
|
+
"@earendil-works/pi-coding-agent": ">=0.84.4",
|
|
72
|
+
"@earendil-works/pi-tui": ">=0.84.4"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@earendil-works/pi-tui": "0.84.4",
|
|
@@ -121,7 +121,7 @@ The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md
|
|
|
121
121
|
- Command templates remain compact and shell-free. Use string leaves or ordered `template` arrays; shell operators are not an execution contract. Examples use portable executable placeholders, never machine-local paths.
|
|
122
122
|
- `telegram_attach` is the canonical file path and `telegram_message` the direct Markdown text/buttons path. Both require current direct or registered-follower authority and must not replace the normal active-turn reply.
|
|
123
123
|
- Inbound handlers transform text/media before queueing; outbound handlers precede programmatic/provider fallbacks. Public contracts and ordering live in `docs/inbound.md`, `docs/outbound.md`, and `docs/public-api.md`.
|
|
124
|
-
- Pi integration uses public hooks and APIs. Telegram `/new` is scheduled against the exact durable update, dispatched only after that update is removed from the journal, and then routed through
|
|
124
|
+
- Pi integration uses public hooks and APIs. Telegram `/new` is scheduled against the exact durable update, dispatched only after that update is removed from the journal, and then routed through the single `/telegram-internal` Pi gateway via `pi.sendUserMessage(..., { expandPromptTemplates: true })`; only a runtime-armed typed action may execute, manual invocation reports that the command cannot be run manually, and the handler receives the real `ExtensionCommandContext` before calling `ctx.newSession()`. Before replacement, CAS-publish one exact expiring handoff in the profile target snapshot. `workspace-thread` successors re-key the matching Workspace binding; `classic-chat` successors preserve Profile/CWD/session/chat continuity without creating a binding or invoking topic APIs. Both atomically claim the intent before one terminal result, and the old `withSession` path never publishes the same success. Never replace the session while inbound authority is unsettled, store stale command contexts, accept an expired or mismatched handoff, inject terminal input, spawn a shadow Pi process, or mutate session files.
|
|
125
125
|
|
|
126
126
|
## 7. Engineering Conventions
|
|
127
127
|
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.50.0: Internal lifecycle gateway and thinking cadence
|
|
8
|
+
|
|
9
|
+
- `Internal lifecycle gateway`: Replaces the session-specific technical command with one `/telegram-internal` gateway for runtime-armed typed actions. Manual invocation performs no action and explains that the command cannot be run manually; settled `/new` replacement retains the same lifecycle and terminal-result guarantees.
|
|
10
|
+
- `Thinking cadence`: Thinking now follows the same cadence as answer drafts: it accumulates the opening frame for two seconds, then updates at most once every two seconds while still flushing buffered reasoning when the block completes.
|
|
11
|
+
|
|
7
12
|
## 0.49.0: Unified fresh-session continuity
|
|
8
13
|
|
|
9
14
|
- `/new`: Classic and Threaded Mode now share one confirmed fresh-session flow. The settled callback deletes its dialog and publishes an expiring exact-target intent; a same- or cross-process successor preserves the classic chat or re-keys the Thread/slot/name, atomically claims once, then sends one terminal result. Identity mismatches fail closed.
|
|
@@ -126,7 +126,7 @@ Enable the optional capabilities the bridge needs in the [@BotFather](https://t.
|
|
|
126
126
|
| Model and thinking | Switch model or thinking level from Telegram through safe continuation flows. | Mobile control can adjust execution strategy without tearing down the current session. |
|
|
127
127
|
| Compaction | Confirm `/compact`, show native active status during compaction, and preserve Telegram-owned turn semantics. | Context maintenance is visible and safe from the phone. |
|
|
128
128
|
| Draft previews | Show Telegram's native `…typing` indicator whenever the connected instance is doing agent work, or enable Rich Draft previews for streamed answer text. | Local prompts, Telegram turns, and autonomous continuations remain visibly active while draft visibility stays independent from final rendering. |
|
|
129
|
-
| Activity | Keep the default `verbose` technical surface, show only `thinking`, show only `tools`, or select `quiet` for answer-only delivery. Every instance reloads this shared file-backed choice before a new agent run; thinking
|
|
129
|
+
| Activity | Keep the default `verbose` technical surface, show only `thinking`, show only `tools`, or select `quiet` for answer-only delivery. Every instance reloads this shared file-backed choice before a new agent run; thinking accumulates for two seconds and then updates at most every two seconds in a headerless expandable quote, while each tool uses one iconless closed root row containing nested evidence details. | Persistent collapsed technical activity minimizes chat height and stays bounded, redacted, target-fenced, free of URL previews, and visually separate from semantic assistant answers. |
|
|
130
130
|
| Assistant rendering | Choose Native Rich Markdown or legacy Markdown-to-HTML for final assistant replies. | Renderer compatibility is explicit instead of being conflated with draft previews. |
|
|
131
131
|
| Bridge UI rendering | Render thinking through headerless expandable HTML with inline emphasis/code, render each tool as an iconless native Rich root details tree with immediately visible arguments and collapsed secondary evidence, and keep menus, queue controls, status, settings, diagnostics, and sections on Telegram HTML/plain UI. | Harness-owned surfaces remain operationally predictable and visually distinct from model-authored answers. |
|
|
132
132
|
| Inbound files | Download inbound files to the Pi agent temp directory with size limits. | Screenshots, PDFs, datasets, and artifacts enter Pi as inspectable local files. |
|
|
@@ -11,7 +11,7 @@ export declare const TELEGRAM_ACTIVITY_MESSAGE_MAX_CHARS = 3900;
|
|
|
11
11
|
export declare const TELEGRAM_ACTIVITY_MESSAGE_MAX_TOOLS = 6;
|
|
12
12
|
export declare const TELEGRAM_REASONING_MESSAGE_MAX_FRAMES = 24;
|
|
13
13
|
export declare const TELEGRAM_REASONING_BUFFER_MAX_CHARS = 1200;
|
|
14
|
-
export declare const TELEGRAM_REASONING_MIN_INTERVAL_MS =
|
|
14
|
+
export declare const TELEGRAM_REASONING_MIN_INTERVAL_MS = 2000;
|
|
15
15
|
export declare const TELEGRAM_TOOL_UPDATE_MAX_ENTRIES = 4;
|
|
16
16
|
interface ToolActivity {
|
|
17
17
|
id: string;
|
|
@@ -41,6 +41,8 @@ export declare function createTelegramActivityVerbosityRuntime<TAuthority>(deps:
|
|
|
41
41
|
getActivityMode: () => "quiet" | "thinking" | "tools" | "verbose";
|
|
42
42
|
refreshActivityMode?: () => Promise<void>;
|
|
43
43
|
getNowMs?: () => number;
|
|
44
|
+
setReasoningTimeout?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
|
45
|
+
clearReasoningTimeout?: (timer: ReturnType<typeof setTimeout>) => void;
|
|
44
46
|
resolveTarget: (event: TelegramActivityEvent) => TelegramTarget | undefined;
|
|
45
47
|
captureAuthority: () => TAuthority;
|
|
46
48
|
isAuthorityActive: (authority: TAuthority) => boolean;
|
|
@@ -9,7 +9,9 @@ export const TELEGRAM_ACTIVITY_MESSAGE_MAX_CHARS = 3_900;
|
|
|
9
9
|
export const TELEGRAM_ACTIVITY_MESSAGE_MAX_TOOLS = 6;
|
|
10
10
|
export const TELEGRAM_REASONING_MESSAGE_MAX_FRAMES = 24;
|
|
11
11
|
export const TELEGRAM_REASONING_BUFFER_MAX_CHARS = 1_200;
|
|
12
|
-
|
|
12
|
+
// Match native answer drafts: accumulate the opening frame for one full
|
|
13
|
+
// interval, then publish at most one updated frame per interval.
|
|
14
|
+
export const TELEGRAM_REASONING_MIN_INTERVAL_MS = 2_000;
|
|
13
15
|
export const TELEGRAM_TOOL_UPDATE_MAX_ENTRIES = 4;
|
|
14
16
|
function targetEquals(left, right) {
|
|
15
17
|
return left.chatId === right.chatId && left.threadId === right.threadId;
|
|
@@ -234,10 +236,18 @@ export function createTelegramActivityVerbosityRuntime(deps) {
|
|
|
234
236
|
let reasoningMessage;
|
|
235
237
|
let reasoningBlocked = false;
|
|
236
238
|
let lastReasoningPublishMs = 0;
|
|
239
|
+
let reasoningFlushTimer;
|
|
237
240
|
let toolMessage;
|
|
238
241
|
const tools = new Map();
|
|
239
242
|
const toolOrder = [];
|
|
243
|
+
const clearReasoningFlushTimer = () => {
|
|
244
|
+
if (!reasoningFlushTimer)
|
|
245
|
+
return;
|
|
246
|
+
(deps.clearReasoningTimeout ?? clearTimeout)(reasoningFlushTimer);
|
|
247
|
+
reasoningFlushTimer = undefined;
|
|
248
|
+
};
|
|
240
249
|
const clearActivity = () => {
|
|
250
|
+
clearReasoningFlushTimer();
|
|
241
251
|
activityId = undefined;
|
|
242
252
|
authority = undefined;
|
|
243
253
|
target = undefined;
|
|
@@ -330,6 +340,30 @@ export function createTelegramActivityVerbosityRuntime(deps) {
|
|
|
330
340
|
deps.recordFailure?.(canEdit ? "reasoning-edit" : "reasoning-send", event, error);
|
|
331
341
|
}
|
|
332
342
|
};
|
|
343
|
+
const scheduleReasoningPublish = (event, acceptedGeneration) => {
|
|
344
|
+
if (reasoningFlushTimer || reasoningBlocked)
|
|
345
|
+
return;
|
|
346
|
+
const elapsed = reasoningMessageFrames === 0
|
|
347
|
+
? 0
|
|
348
|
+
: getNowMs() - lastReasoningPublishMs;
|
|
349
|
+
const delayMs = Math.max(0, TELEGRAM_REASONING_MIN_INTERVAL_MS - elapsed);
|
|
350
|
+
const schedule = deps.setReasoningTimeout ?? setTimeout;
|
|
351
|
+
reasoningFlushTimer = schedule(() => {
|
|
352
|
+
reasoningFlushTimer = undefined;
|
|
353
|
+
const admittedAuthority = authority;
|
|
354
|
+
const task = async () => {
|
|
355
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority) ||
|
|
356
|
+
reasoningChars <= lastReasoningMessageChars)
|
|
357
|
+
return;
|
|
358
|
+
await publishReasoning(event, acceptedGeneration);
|
|
359
|
+
};
|
|
360
|
+
const enqueue = deps.enqueue ?? ((next) => tail.then(next));
|
|
361
|
+
tail = enqueue(task).catch((error) => {
|
|
362
|
+
deps.recordFailure?.("reasoning-send", event, error);
|
|
363
|
+
});
|
|
364
|
+
}, delayMs);
|
|
365
|
+
reasoningFlushTimer?.unref?.();
|
|
366
|
+
};
|
|
333
367
|
const publishTool = async (event, tool, acceptedGeneration) => {
|
|
334
368
|
const admittedAuthority = authority;
|
|
335
369
|
if (!isCurrent(acceptedGeneration, admittedAuthority) || !target) {
|
|
@@ -461,12 +495,8 @@ export function createTelegramActivityVerbosityRuntime(deps) {
|
|
|
461
495
|
return;
|
|
462
496
|
reasoningChars += event.delta.length;
|
|
463
497
|
reasoningBuffer = `${reasoningBuffer}${event.delta}`.slice(-TELEGRAM_REASONING_BUFFER_MAX_CHARS);
|
|
464
|
-
if (reasoningMessageFrames < TELEGRAM_REASONING_MESSAGE_MAX_FRAMES
|
|
465
|
-
(
|
|
466
|
-
(getNowMs() - lastReasoningPublishMs >=
|
|
467
|
-
TELEGRAM_REASONING_MIN_INTERVAL_MS &&
|
|
468
|
-
reasoningChars - lastReasoningMessageChars >= 160))) {
|
|
469
|
-
await publishReasoning(event, acceptedGeneration);
|
|
498
|
+
if (reasoningMessageFrames < TELEGRAM_REASONING_MESSAGE_MAX_FRAMES) {
|
|
499
|
+
scheduleReasoningPublish(event, acceptedGeneration);
|
|
470
500
|
}
|
|
471
501
|
return;
|
|
472
502
|
}
|
|
@@ -477,6 +507,7 @@ export function createTelegramActivityVerbosityRuntime(deps) {
|
|
|
477
507
|
reasoningChars = event.text.length;
|
|
478
508
|
reasoningBuffer = event.text.slice(-TELEGRAM_REASONING_BUFFER_MAX_CHARS);
|
|
479
509
|
}
|
|
510
|
+
clearReasoningFlushTimer();
|
|
480
511
|
if (reasoningChars > 0 &&
|
|
481
512
|
reasoningChars > lastReasoningMessageChars &&
|
|
482
513
|
!reasoningBlocked) {
|
|
@@ -550,8 +581,8 @@ export function createTelegramActivityVerbosityRuntime(deps) {
|
|
|
550
581
|
return;
|
|
551
582
|
}
|
|
552
583
|
if (event.type === "agent-end" || event.type === "agent-settled") {
|
|
553
|
-
|
|
554
|
-
|
|
584
|
+
clearReasoningFlushTimer();
|
|
585
|
+
if (reasoningChars > lastReasoningMessageChars &&
|
|
555
586
|
!reasoningBlocked) {
|
|
556
587
|
await publishReasoning(event, acceptedGeneration);
|
|
557
588
|
}
|
|
@@ -574,8 +574,9 @@ export declare function createTelegramCommandHandler<TMessage extends TelegramCo
|
|
|
574
574
|
export declare function createTelegramCommandOrPromptRuntime<TMessage, TContext>(deps: TelegramCommandOrPromptRuntimeDeps<TMessage, TContext>): {
|
|
575
575
|
dispatchMessages: (messages: TMessage[], ctx: TContext) => Promise<void>;
|
|
576
576
|
};
|
|
577
|
-
export declare const
|
|
578
|
-
export declare const
|
|
577
|
+
export declare const TELEGRAM_INTERNAL_COMMAND_NAME = "telegram-internal";
|
|
578
|
+
export declare const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION = "(internal) dispatch one settled Telegram lifecycle action";
|
|
579
|
+
export declare const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE = "This internal Telegram command cannot be run manually.";
|
|
579
580
|
export declare function delayTelegramSessionAction(delayMs: number): Promise<void>;
|
|
580
581
|
export interface TelegramSessionActionRuntimeDeps {
|
|
581
582
|
registerCommand: Pi.ExtensionAPI["registerCommand"];
|
|
@@ -1232,8 +1232,9 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
|
|
|
1232
1232
|
},
|
|
1233
1233
|
}, commandArgs);
|
|
1234
1234
|
}
|
|
1235
|
-
export const
|
|
1236
|
-
export const
|
|
1235
|
+
export const TELEGRAM_INTERNAL_COMMAND_NAME = "telegram-internal";
|
|
1236
|
+
export const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION = "(internal) dispatch one settled Telegram lifecycle action";
|
|
1237
|
+
export const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE = "This internal Telegram command cannot be run manually.";
|
|
1237
1238
|
export function delayTelegramSessionAction(delayMs) {
|
|
1238
1239
|
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1239
1240
|
}
|
|
@@ -1364,8 +1365,7 @@ export function createTelegramSessionActionAssembly(deps) {
|
|
|
1364
1365
|
export function createTelegramSessionActionRuntime(deps) {
|
|
1365
1366
|
let pendingUpdateId;
|
|
1366
1367
|
let pendingTarget;
|
|
1367
|
-
let
|
|
1368
|
-
let commandUpdateId;
|
|
1368
|
+
let pendingAction;
|
|
1369
1369
|
let registered = false;
|
|
1370
1370
|
const reportFailure = (error) => {
|
|
1371
1371
|
try {
|
|
@@ -1380,33 +1380,34 @@ export function createTelegramSessionActionRuntime(deps) {
|
|
|
1380
1380
|
if (registered)
|
|
1381
1381
|
return;
|
|
1382
1382
|
registered = true;
|
|
1383
|
-
deps.registerCommand(
|
|
1384
|
-
description:
|
|
1383
|
+
deps.registerCommand(TELEGRAM_INTERNAL_COMMAND_NAME, {
|
|
1384
|
+
description: TELEGRAM_INTERNAL_COMMAND_DESCRIPTION,
|
|
1385
1385
|
handler: async (_args, ctx) => {
|
|
1386
|
-
|
|
1386
|
+
const action = pendingAction;
|
|
1387
|
+
if (!action) {
|
|
1388
|
+
ctx.ui.notify(TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE, "warning");
|
|
1387
1389
|
return;
|
|
1388
|
-
commandPending = false;
|
|
1389
|
-
const target = pendingTarget;
|
|
1390
|
-
const updateId = commandUpdateId;
|
|
1391
|
-
pendingTarget = undefined;
|
|
1392
|
-
commandUpdateId = undefined;
|
|
1393
|
-
if (!target || updateId === undefined)
|
|
1394
|
-
return;
|
|
1395
|
-
try {
|
|
1396
|
-
await deps.prepareReplacement?.(ctx, updateId, target);
|
|
1397
|
-
const result = await ctx.newSession();
|
|
1398
|
-
if (result.cancelled)
|
|
1399
|
-
await deps.notifyResult(target, "cancelled");
|
|
1400
1390
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1391
|
+
pendingAction = undefined;
|
|
1392
|
+
switch (action.kind) {
|
|
1393
|
+
case "replace-session":
|
|
1394
|
+
try {
|
|
1395
|
+
await deps.prepareReplacement?.(ctx, action.updateId, action.target);
|
|
1396
|
+
const result = await ctx.newSession();
|
|
1397
|
+
if (result.cancelled)
|
|
1398
|
+
await deps.notifyResult(action.target, "cancelled");
|
|
1399
|
+
}
|
|
1400
|
+
catch (error) {
|
|
1401
|
+
reportFailure(error);
|
|
1402
|
+
await deps.notifyResult(action.target, "failure");
|
|
1403
|
+
}
|
|
1404
|
+
return;
|
|
1404
1405
|
}
|
|
1405
1406
|
},
|
|
1406
1407
|
});
|
|
1407
1408
|
},
|
|
1408
1409
|
scheduleAfterUpdate(updateId, target) {
|
|
1409
|
-
if (pendingUpdateId !== undefined ||
|
|
1410
|
+
if (pendingUpdateId !== undefined || pendingAction !== undefined)
|
|
1410
1411
|
return false;
|
|
1411
1412
|
pendingUpdateId = updateId;
|
|
1412
1413
|
pendingTarget = { ...target };
|
|
@@ -1416,21 +1417,22 @@ export function createTelegramSessionActionRuntime(deps) {
|
|
|
1416
1417
|
if (pendingUpdateId !== updateId)
|
|
1417
1418
|
return;
|
|
1418
1419
|
pendingUpdateId = undefined;
|
|
1419
|
-
|
|
1420
|
-
|
|
1420
|
+
const target = pendingTarget;
|
|
1421
|
+
pendingTarget = undefined;
|
|
1422
|
+
if (!target)
|
|
1423
|
+
return;
|
|
1424
|
+
pendingAction = { kind: "replace-session", updateId, target };
|
|
1421
1425
|
void Promise.resolve()
|
|
1422
|
-
.then(() => deps.sendUserMessage(`/${
|
|
1426
|
+
.then(() => deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME}`, {
|
|
1423
1427
|
expandPromptTemplates: true,
|
|
1424
1428
|
}))
|
|
1425
1429
|
.catch((error) => {
|
|
1426
|
-
|
|
1427
|
-
commandUpdateId = undefined;
|
|
1428
|
-
pendingTarget = undefined;
|
|
1430
|
+
pendingAction = undefined;
|
|
1429
1431
|
reportFailure(error);
|
|
1430
1432
|
});
|
|
1431
1433
|
},
|
|
1432
1434
|
hasPending() {
|
|
1433
|
-
return pendingUpdateId !== undefined ||
|
|
1435
|
+
return pendingUpdateId !== undefined || pendingAction !== undefined;
|
|
1434
1436
|
},
|
|
1435
1437
|
};
|
|
1436
1438
|
}
|
|
@@ -610,7 +610,7 @@ Queue reaction behavior, lane-tail transitions, Keep/Skip independence, multi-re
|
|
|
610
610
|
|
|
611
611
|
Complete intermediate assistant text blocks from Telegram-originated activity are sent once to the immutable originating target before active-turn final delivery; final and terminal-partial segments stay with settlement so replies are not duplicated. While this instance has exact direct or follower transport authority, completed public blocks from local/autonomous work are always sent once and in source order to the instance's authorized target. Connected companion projection is not configurable; disconnect or authority loss is its boundary. Both paths use the configured Rich or HTML renderer and exclude reasoning, tool traffic, token deltas, local prompt text, unknown sources, and stale generations. Each admitted block remains fenced to its exact target, profile/token stamp, leader epoch or follower registration generation, and session generation; non-idempotent acknowledgement ambiguity never authorizes replay.
|
|
612
612
|
|
|
613
|
-
`assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node renders snake-case names as title words while preserving an uppercase two- or three-letter repeated prefix per word, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with lowercase monospaced, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call. Bridge-owned assistant output and activity projection share one activity-publication sequence admitted synchronously from the activity bus, so later tool disclosures cannot overtake earlier assistant blocks. Activity targets and transport authority are captured before queued work starts. Active Telegram-turn final replies/artifacts and automatic compaction notices use that same publication owner before the existing direct/follower transport split, without mutually waiting on separate output tails. Final settlement captures the exact active turn and assistant result and reserves its publication position before config loading. Empty outcomes without a publication candidate reserve no position. Replacement, preparation failure, or an unused reservation releases the position without resetting or replying for a replacement turn. Final errors also publish through this owner. Reservations accept one task; cancellation cannot undo a published task, and session reset releases unresolved reservations while fencing old queued tasks. Terminal assistant messages with a publication candidate reserve the final position synchronously; settlement consumes that reservation only for the exact originating turn. Compaction notices enter the same queue immediately, behind the reserved final rather than through a separate notice buffer. Settlement, a new agent run, or session replacement cancels an unconsumed reservation. Notice target and authority are captured when the event is observed. Pi lifecycle completion does not wait for network publication. This order is process-local and does not serialize unrelated instances or independent registered activity handlers. Settlement, replacement, disconnect, failure, or stale authority clears only local ownership; already-sent activity messages remain in chat.
|
|
613
|
+
`assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Like answer drafts, it accumulates for two seconds before the first frame, publishes at most one update every two seconds, and flushes remaining buffered reasoning on terminal completion. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node renders snake-case names as title words while preserving an uppercase two- or three-letter repeated prefix per word, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with lowercase monospaced, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call. Bridge-owned assistant output and activity projection share one activity-publication sequence admitted synchronously from the activity bus, so later tool disclosures cannot overtake earlier assistant blocks. Activity targets and transport authority are captured before queued work starts. Active Telegram-turn final replies/artifacts and automatic compaction notices use that same publication owner before the existing direct/follower transport split, without mutually waiting on separate output tails. Final settlement captures the exact active turn and assistant result and reserves its publication position before config loading. Empty outcomes without a publication candidate reserve no position. Replacement, preparation failure, or an unused reservation releases the position without resetting or replying for a replacement turn. Final errors also publish through this owner. Reservations accept one task; cancellation cannot undo a published task, and session reset releases unresolved reservations while fencing old queued tasks. Terminal assistant messages with a publication candidate reserve the final position synchronously; settlement consumes that reservation only for the exact originating turn. Compaction notices enter the same queue immediately, behind the reserved final rather than through a separate notice buffer. Settlement, a new agent run, or session replacement cancels an unconsumed reservation. Notice target and authority are captured when the event is observed. Pi lifecycle completion does not wait for network publication. This order is process-local and does not serialize unrelated instances or independent registered activity handlers. Settlement, replacement, disconnect, failure, or stale authority clears only local ownership; already-sent activity messages remain in chat.
|
|
614
614
|
|
|
615
615
|
Telegram prompt guidance is context- and authority-aware. The package and source-checkout extension contribute `telegram-bridge`, optional `generated-control-surface`, and `generative-apps` Skills through Pi resource discovery. Generated Control Surface treats `interface = f(state, capabilities, intent)` as a renderer-neutral primitive, compiling transient evidence-backed controls over domain-owned workflows, systems, navigation, supervision, and decisions without creating parallel application state. It composes an ordered ragged sequence of independently sized semantic rows rather than filling a rectangular grid: compact rows contain genuine peers, singleton rows isolate structurally independent actions, and rectangular layouts remain reserved for genuinely spatial state. Text-bearing controls use at most two columns and flow into additional rows, while denser rows are reserved for short position-bearing glyphs or codes and never exceed the eight-column phone-width UX maximum. Vertical extent is independent: a true spatial surface may retain substantially more rows, while non-spatial button walls route to grouping, disclosure, or pagination. Symmetry is treated as an evidence claim about equal relationships or real spatial topology; an abstract layout catalog supplies adaptable singleton, peer, staged, navigational, repeated-pair, and rectangular shapes without forcing tasks into preset grids. Repeated controls carry the smallest sufficient action delta when visible conversation is unambiguous; larger or error-prone state moves to a deterministic task-owned Markdown artifact, correctness-sensitive transitions move to a small domain-owned transition implementation, and repeated clicks are adjudicated against current state rather than stale button appearance. Its filesystem adapter reserves the first full-width row for parent traversal outside root, places available Previous/Next controls together in one compact row immediately afterward, orders visible directories, hidden directories, visible files, and hidden files alphabetically within each category before fixed ten-entry pagination, renders path/range metadata as stacked status-style key-value rows instead of middle-dot prose, emits the complete Telegram control set through one JSON-matrix action, suppresses duplicate plain/monospaced listings and default Refresh unless user preference overrides presentation, and retains an ordinary numbered fallback when buttons are unavailable. Only an exact direct owner or live registered follower exposes the two pi-telegram delivery tools, their active-tool metadata, and the compact routing suffix. Disconnect or authority loss removes those tool surfaces for subsequent requests without touching foreign tools; reconnect/recovery restores only the pi-telegram subset that was active before suspension, including across same-process reload. Repeated stable interactions may graduate from that model-mediated surface into a reviewed Generative App whose deterministic bound methods bypass Pi queue admission; the `generative-apps` Skill owns this compilation and operating workflow while the underlying capability retains domain authority. Telegram-originated turns route to the stable Skill contracts and retain dynamic blocks such as `[voice] delivery: automatic voice`; the Skills and public documentation own syntax, target routing, Threaded Mode behavior, Generative App operation, and diagnostics.
|
|
616
616
|
|
|
@@ -51,7 +51,7 @@ Stable commands inside Pi:
|
|
|
51
51
|
Stable commands inside the paired Telegram DM:
|
|
52
52
|
|
|
53
53
|
- `/start` — pair when needed and open the main application menu.
|
|
54
|
-
- `/new` — after idle and empty-queue checks, request a new Pi session in the current classic chat or Thread. The bridge acknowledges the callback, deletes its confirmation, completes and removes the exact durable update, then dispatches
|
|
54
|
+
- `/new` — after idle and empty-queue checks, request a new Pi session in the current classic chat or Thread. The bridge acknowledges the callback, deletes its confirmation, completes and removes the exact durable update, then dispatches one runtime-armed typed action through the internal `/telegram-internal` Pi gateway using `pi.sendUserMessage(..., { expandPromptTemplates: true })`. Manual invocation reports that the gateway cannot be run manually; the armed handler receives a real `ExtensionCommandContext` and calls `ctx.newSession()`; one discriminated durable intent preserves either exact classic Profile/CWD/session/chat continuity or the Thread binding with slot/name re-key. The successor CAS-claims that intent before sending one terminal result. Busy, identity-mismatch, and unavailable-host paths fail closed.
|
|
55
55
|
- `/compact` — open confirmation and compact when idle.
|
|
56
56
|
- `/next` — abort active work first when needed, let the interrupted prompt receive its abort notice, then reply `Dispatching next queued turn.` to the exact queued prompt selected for the next model turn. The command itself is never the lifecycle-notice reply target, and aborted pending assistant text is suppressed.
|
|
57
57
|
- `/continue` — enqueue a priority `continue` prompt.
|
|
@@ -24,7 +24,9 @@ export const TELEGRAM_ACTIVITY_MESSAGE_MAX_CHARS = 3_900;
|
|
|
24
24
|
export const TELEGRAM_ACTIVITY_MESSAGE_MAX_TOOLS = 6;
|
|
25
25
|
export const TELEGRAM_REASONING_MESSAGE_MAX_FRAMES = 24;
|
|
26
26
|
export const TELEGRAM_REASONING_BUFFER_MAX_CHARS = 1_200;
|
|
27
|
-
|
|
27
|
+
// Match native answer drafts: accumulate the opening frame for one full
|
|
28
|
+
// interval, then publish at most one updated frame per interval.
|
|
29
|
+
export const TELEGRAM_REASONING_MIN_INTERVAL_MS = 2_000;
|
|
28
30
|
export const TELEGRAM_TOOL_UPDATE_MAX_ENTRIES = 4;
|
|
29
31
|
|
|
30
32
|
interface ToolActivity {
|
|
@@ -325,6 +327,11 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
325
327
|
getActivityMode: () => "quiet" | "thinking" | "tools" | "verbose";
|
|
326
328
|
refreshActivityMode?: () => Promise<void>;
|
|
327
329
|
getNowMs?: () => number;
|
|
330
|
+
setReasoningTimeout?: (
|
|
331
|
+
callback: () => void,
|
|
332
|
+
delayMs: number,
|
|
333
|
+
) => ReturnType<typeof setTimeout>;
|
|
334
|
+
clearReasoningTimeout?: (timer: ReturnType<typeof setTimeout>) => void;
|
|
328
335
|
resolveTarget: (event: TelegramActivityEvent) => TelegramTarget | undefined;
|
|
329
336
|
captureAuthority: () => TAuthority;
|
|
330
337
|
isAuthorityActive: (authority: TAuthority) => boolean;
|
|
@@ -360,11 +367,18 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
360
367
|
let reasoningMessage: ReasoningMessage | undefined;
|
|
361
368
|
let reasoningBlocked = false;
|
|
362
369
|
let lastReasoningPublishMs = 0;
|
|
370
|
+
let reasoningFlushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
363
371
|
let toolMessage: ToolMessage | undefined;
|
|
364
372
|
const tools = new Map<string, ToolActivity>();
|
|
365
373
|
const toolOrder: string[] = [];
|
|
366
374
|
|
|
375
|
+
const clearReasoningFlushTimer = () => {
|
|
376
|
+
if (!reasoningFlushTimer) return;
|
|
377
|
+
(deps.clearReasoningTimeout ?? clearTimeout)(reasoningFlushTimer);
|
|
378
|
+
reasoningFlushTimer = undefined;
|
|
379
|
+
};
|
|
367
380
|
const clearActivity = () => {
|
|
381
|
+
clearReasoningFlushTimer();
|
|
368
382
|
activityId = undefined;
|
|
369
383
|
authority = undefined;
|
|
370
384
|
target = undefined;
|
|
@@ -466,6 +480,33 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
466
480
|
);
|
|
467
481
|
}
|
|
468
482
|
};
|
|
483
|
+
const scheduleReasoningPublish = (
|
|
484
|
+
event: TelegramActivityEvent,
|
|
485
|
+
acceptedGeneration: number,
|
|
486
|
+
) => {
|
|
487
|
+
if (reasoningFlushTimer || reasoningBlocked) return;
|
|
488
|
+
const elapsed = reasoningMessageFrames === 0
|
|
489
|
+
? 0
|
|
490
|
+
: getNowMs() - lastReasoningPublishMs;
|
|
491
|
+
const delayMs = Math.max(0, TELEGRAM_REASONING_MIN_INTERVAL_MS - elapsed);
|
|
492
|
+
const schedule = deps.setReasoningTimeout ?? setTimeout;
|
|
493
|
+
reasoningFlushTimer = schedule(() => {
|
|
494
|
+
reasoningFlushTimer = undefined;
|
|
495
|
+
const admittedAuthority = authority;
|
|
496
|
+
const task = async () => {
|
|
497
|
+
if (
|
|
498
|
+
!isCurrent(acceptedGeneration, admittedAuthority) ||
|
|
499
|
+
reasoningChars <= lastReasoningMessageChars
|
|
500
|
+
) return;
|
|
501
|
+
await publishReasoning(event, acceptedGeneration);
|
|
502
|
+
};
|
|
503
|
+
const enqueue = deps.enqueue ?? ((next: () => Promise<void>) => tail.then(next));
|
|
504
|
+
tail = enqueue(task).catch((error) => {
|
|
505
|
+
deps.recordFailure?.("reasoning-send", event, error);
|
|
506
|
+
});
|
|
507
|
+
}, delayMs) as ReturnType<typeof setTimeout>;
|
|
508
|
+
reasoningFlushTimer?.unref?.();
|
|
509
|
+
};
|
|
469
510
|
const publishTool = async (
|
|
470
511
|
event: TelegramActivityEvent,
|
|
471
512
|
tool: ToolActivity,
|
|
@@ -602,13 +643,8 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
602
643
|
reasoningBuffer = `${reasoningBuffer}${event.delta}`.slice(
|
|
603
644
|
-TELEGRAM_REASONING_BUFFER_MAX_CHARS,
|
|
604
645
|
);
|
|
605
|
-
if (reasoningMessageFrames < TELEGRAM_REASONING_MESSAGE_MAX_FRAMES
|
|
606
|
-
(
|
|
607
|
-
(getNowMs() - lastReasoningPublishMs >=
|
|
608
|
-
TELEGRAM_REASONING_MIN_INTERVAL_MS &&
|
|
609
|
-
reasoningChars - lastReasoningMessageChars >= 160))
|
|
610
|
-
) {
|
|
611
|
-
await publishReasoning(event, acceptedGeneration);
|
|
646
|
+
if (reasoningMessageFrames < TELEGRAM_REASONING_MESSAGE_MAX_FRAMES) {
|
|
647
|
+
scheduleReasoningPublish(event, acceptedGeneration);
|
|
612
648
|
}
|
|
613
649
|
return;
|
|
614
650
|
}
|
|
@@ -620,6 +656,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
620
656
|
-TELEGRAM_REASONING_BUFFER_MAX_CHARS,
|
|
621
657
|
);
|
|
622
658
|
}
|
|
659
|
+
clearReasoningFlushTimer();
|
|
623
660
|
if (
|
|
624
661
|
reasoningChars > 0 &&
|
|
625
662
|
reasoningChars > lastReasoningMessageChars &&
|
|
@@ -687,8 +724,8 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
687
724
|
return;
|
|
688
725
|
}
|
|
689
726
|
if (event.type === "agent-end" || event.type === "agent-settled") {
|
|
727
|
+
clearReasoningFlushTimer();
|
|
690
728
|
if (
|
|
691
|
-
reasoningMessage &&
|
|
692
729
|
reasoningChars > lastReasoningMessageChars &&
|
|
693
730
|
!reasoningBlocked
|
|
694
731
|
) {
|
|
@@ -2335,9 +2335,11 @@ async function handleTelegramCommandRuntime<
|
|
|
2335
2335
|
);
|
|
2336
2336
|
}
|
|
2337
2337
|
|
|
2338
|
-
export const
|
|
2339
|
-
export const
|
|
2340
|
-
"(internal)
|
|
2338
|
+
export const TELEGRAM_INTERNAL_COMMAND_NAME = "telegram-internal";
|
|
2339
|
+
export const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION =
|
|
2340
|
+
"(internal) dispatch one settled Telegram lifecycle action";
|
|
2341
|
+
export const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE =
|
|
2342
|
+
"This internal Telegram command cannot be run manually.";
|
|
2341
2343
|
|
|
2342
2344
|
export function delayTelegramSessionAction(delayMs: number): Promise<void> {
|
|
2343
2345
|
return new Promise<void>((resolve) => setTimeout(resolve, delayMs));
|
|
@@ -2538,6 +2540,12 @@ export function createTelegramSessionActionAssembly(
|
|
|
2538
2540
|
return { action, settlement };
|
|
2539
2541
|
}
|
|
2540
2542
|
|
|
2543
|
+
type TelegramPendingInternalAction = {
|
|
2544
|
+
kind: "replace-session";
|
|
2545
|
+
updateId: number;
|
|
2546
|
+
target: { chatId: number; threadId?: number; messageId: number };
|
|
2547
|
+
};
|
|
2548
|
+
|
|
2541
2549
|
export interface TelegramSessionActionRuntime {
|
|
2542
2550
|
register: () => void;
|
|
2543
2551
|
scheduleAfterUpdate: (
|
|
@@ -2557,8 +2565,7 @@ export function createTelegramSessionActionRuntime(
|
|
|
2557
2565
|
threadId?: number;
|
|
2558
2566
|
messageId: number;
|
|
2559
2567
|
} | undefined;
|
|
2560
|
-
let
|
|
2561
|
-
let commandUpdateId: number | undefined;
|
|
2568
|
+
let pendingAction: TelegramPendingInternalAction | undefined;
|
|
2562
2569
|
let registered = false;
|
|
2563
2570
|
|
|
2564
2571
|
const reportFailure = (error: unknown): void => {
|
|
@@ -2573,29 +2580,32 @@ export function createTelegramSessionActionRuntime(
|
|
|
2573
2580
|
register() {
|
|
2574
2581
|
if (registered) return;
|
|
2575
2582
|
registered = true;
|
|
2576
|
-
deps.registerCommand(
|
|
2577
|
-
description:
|
|
2583
|
+
deps.registerCommand(TELEGRAM_INTERNAL_COMMAND_NAME, {
|
|
2584
|
+
description: TELEGRAM_INTERNAL_COMMAND_DESCRIPTION,
|
|
2578
2585
|
handler: async (_args, ctx) => {
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2586
|
+
const action = pendingAction;
|
|
2587
|
+
if (!action) {
|
|
2588
|
+
ctx.ui.notify(TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE, "warning");
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
pendingAction = undefined;
|
|
2592
|
+
switch (action.kind) {
|
|
2593
|
+
case "replace-session":
|
|
2594
|
+
try {
|
|
2595
|
+
await deps.prepareReplacement?.(ctx, action.updateId, action.target);
|
|
2596
|
+
const result = await ctx.newSession();
|
|
2597
|
+
if (result.cancelled) await deps.notifyResult(action.target, "cancelled");
|
|
2598
|
+
} catch (error) {
|
|
2599
|
+
reportFailure(error);
|
|
2600
|
+
await deps.notifyResult(action.target, "failure");
|
|
2601
|
+
}
|
|
2602
|
+
return;
|
|
2593
2603
|
}
|
|
2594
2604
|
},
|
|
2595
2605
|
});
|
|
2596
2606
|
},
|
|
2597
2607
|
scheduleAfterUpdate(updateId, target) {
|
|
2598
|
-
if (pendingUpdateId !== undefined ||
|
|
2608
|
+
if (pendingUpdateId !== undefined || pendingAction !== undefined) return false;
|
|
2599
2609
|
pendingUpdateId = updateId;
|
|
2600
2610
|
pendingTarget = { ...target };
|
|
2601
2611
|
return true;
|
|
@@ -2603,23 +2613,23 @@ export function createTelegramSessionActionRuntime(
|
|
|
2603
2613
|
onUpdateCompleted(updateId) {
|
|
2604
2614
|
if (pendingUpdateId !== updateId) return;
|
|
2605
2615
|
pendingUpdateId = undefined;
|
|
2606
|
-
|
|
2607
|
-
|
|
2616
|
+
const target = pendingTarget;
|
|
2617
|
+
pendingTarget = undefined;
|
|
2618
|
+
if (!target) return;
|
|
2619
|
+
pendingAction = { kind: "replace-session", updateId, target };
|
|
2608
2620
|
void Promise.resolve()
|
|
2609
2621
|
.then(() =>
|
|
2610
|
-
deps.sendUserMessage(`/${
|
|
2622
|
+
deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME}`, {
|
|
2611
2623
|
expandPromptTemplates: true,
|
|
2612
2624
|
}),
|
|
2613
2625
|
)
|
|
2614
2626
|
.catch((error) => {
|
|
2615
|
-
|
|
2616
|
-
commandUpdateId = undefined;
|
|
2617
|
-
pendingTarget = undefined;
|
|
2627
|
+
pendingAction = undefined;
|
|
2618
2628
|
reportFailure(error);
|
|
2619
2629
|
});
|
|
2620
2630
|
},
|
|
2621
2631
|
hasPending() {
|
|
2622
|
-
return pendingUpdateId !== undefined ||
|
|
2632
|
+
return pendingUpdateId !== undefined || pendingAction !== undefined;
|
|
2623
2633
|
},
|
|
2624
2634
|
};
|
|
2625
2635
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"@llblab/pi-clean-room": "0.1.1",
|
|
45
45
|
"@llblab/pi-codex-usage": "0.10.0",
|
|
46
46
|
"@llblab/pi-grow-loop": "0.8.1",
|
|
47
|
-
"@llblab/pi-state-flow": "0.16.
|
|
48
|
-
"@llblab/pi-telegram": "0.
|
|
47
|
+
"@llblab/pi-state-flow": "0.16.3",
|
|
48
|
+
"@llblab/pi-telegram": "0.50.0",
|
|
49
49
|
"@llblab/skills": "1.15.0"
|
|
50
50
|
},
|
|
51
51
|
"bundledDependencies": [
|