@llblab/pi-telegram 0.53.0 → 0.53.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/lib/journal.js +42 -29
- package/dist/lib/outbound.js +4 -0
- package/dist/lib/queue.js +3 -1
- package/dist/lib/status.js +16 -5
- package/dist/package.json +1 -1
- package/docs/architecture.md +1 -1
- package/lib/journal.ts +50 -32
- package/lib/outbound.ts +3 -0
- package/lib/queue.ts +5 -1
- package/lib/status.ts +15 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.53.2: Idle runtime without lock churn
|
|
8
|
+
|
|
9
|
+
- `Idle disk load`: Polling-cursor and status reads of the update journal no longer open config and journal transactions, so a connected idle session stops creating `telegram.json.transaction.staged.*` and `inbox.json.transaction.staged.*` guard folders; only reads that need repair take the serialized path. Unchanged runtime status snapshots are compared before locking and skip their `state.json` transaction.
|
|
10
|
+
|
|
11
|
+
## 0.53.1: Text replies without false voice errors
|
|
12
|
+
|
|
13
|
+
- `Voice artifacts`: A reply plan without voice content no longer reaches outbound voice artifact delivery, so plain text replies stop recording "every voice synthesis provider failed" delivery errors; explicit `telegram_voice` markup and genuine synthesis failures keep their existing fallback behavior (#318).
|
|
14
|
+
|
|
7
15
|
## 0.53.0: Live Thread rebinding, routing tabs and Windows support
|
|
8
16
|
|
|
9
17
|
- `Live-rebind state`: Atomic same-session Workspace binding/operation publication, fenced recipient application and bounded one-shot cleanup scheduling. Failed/unknown cleanup keeps the new binding without blocking the next attempt; chooser retirement keeps saved inputs. Shared-state retries recheck captured authority before every rename, so a revoked grant cannot overwrite retained state.
|
package/dist/lib/journal.js
CHANGED
|
@@ -3221,31 +3221,29 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3221
3221
|
return { file: reset, exists: true, serializedBytes };
|
|
3222
3222
|
}
|
|
3223
3223
|
};
|
|
3224
|
+
const acquireSource = () => sourceAccess
|
|
3225
|
+
? acquireTelegramUpdateJournalFamily({
|
|
3226
|
+
...sourceAccess,
|
|
3227
|
+
path,
|
|
3228
|
+
profile,
|
|
3229
|
+
botIdentity: expectedIdentity,
|
|
3230
|
+
}, version)
|
|
3231
|
+
: undefined;
|
|
3232
|
+
const readAcquiredSource = (source) => {
|
|
3233
|
+
const file = source.evidence.kind === "present" ? source.evidence.file : emptyFile();
|
|
3234
|
+
return {
|
|
3235
|
+
file,
|
|
3236
|
+
exists: source.evidence.kind === "present",
|
|
3237
|
+
serializedBytes: assertCapacity(file),
|
|
3238
|
+
source,
|
|
3239
|
+
};
|
|
3240
|
+
};
|
|
3224
3241
|
const runJournalTransaction = (operation) => {
|
|
3225
3242
|
try {
|
|
3226
3243
|
// Acquire before the transaction helper can create parents or staging names.
|
|
3227
3244
|
// The config continuation excludes participating writers through consumption.
|
|
3228
|
-
const source =
|
|
3229
|
-
|
|
3230
|
-
...sourceAccess,
|
|
3231
|
-
path,
|
|
3232
|
-
profile,
|
|
3233
|
-
botIdentity: expectedIdentity,
|
|
3234
|
-
}, version)
|
|
3235
|
-
: undefined;
|
|
3236
|
-
const readSource = () => {
|
|
3237
|
-
if (!source)
|
|
3238
|
-
return readCurrent();
|
|
3239
|
-
const file = source.evidence.kind === "present"
|
|
3240
|
-
? source.evidence.file
|
|
3241
|
-
: emptyFile();
|
|
3242
|
-
return {
|
|
3243
|
-
file,
|
|
3244
|
-
exists: source.evidence.kind === "present",
|
|
3245
|
-
serializedBytes: assertCapacity(file),
|
|
3246
|
-
source,
|
|
3247
|
-
};
|
|
3248
|
-
};
|
|
3245
|
+
const source = acquireSource();
|
|
3246
|
+
const readSource = () => source ? readAcquiredSource(source) : readCurrent();
|
|
3249
3247
|
return withTelegramFileTransaction(`${path}.transaction`, () => operation(readSource));
|
|
3250
3248
|
}
|
|
3251
3249
|
catch (error) {
|
|
@@ -4091,16 +4089,31 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4091
4089
|
assertCurrent();
|
|
4092
4090
|
return { issued, entries: result };
|
|
4093
4091
|
});
|
|
4092
|
+
const projectRead = (current) => ({
|
|
4093
|
+
...cloneFile(current.file),
|
|
4094
|
+
exists: current.exists,
|
|
4095
|
+
serializedBytes: current.serializedBytes,
|
|
4096
|
+
});
|
|
4094
4097
|
const journal = {
|
|
4095
4098
|
read() {
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4099
|
+
// Atomic publication makes a repair-free read safe without serialization, so idle
|
|
4100
|
+
// status and polling cursor reads create no guards. Any read that needs reconciliation,
|
|
4101
|
+
// or races a compaction, falls back to the serialized path that owns repair.
|
|
4102
|
+
const attempt = () => {
|
|
4103
|
+
try {
|
|
4104
|
+
const source = acquireSource();
|
|
4105
|
+
return source ? readAcquiredSource(source) : readCurrentStrict(false);
|
|
4106
|
+
}
|
|
4107
|
+
catch {
|
|
4108
|
+
return undefined;
|
|
4109
|
+
}
|
|
4110
|
+
};
|
|
4111
|
+
const current = options.withWriterAdmission
|
|
4112
|
+
? options.withWriterAdmission(attempt)
|
|
4113
|
+
: attempt();
|
|
4114
|
+
return current
|
|
4115
|
+
? projectRead(current)
|
|
4116
|
+
: runMutation((readCurrent) => projectRead(readCurrent()));
|
|
4104
4117
|
},
|
|
4105
4118
|
appendBatch(updates, requestedAcceptedThroughUpdateId) {
|
|
4106
4119
|
const canonicalUpdates = updates.map((update) => normalizeIncomingJournaledUpdate(update, path));
|
package/dist/lib/outbound.js
CHANGED
|
@@ -449,6 +449,10 @@ export function createTelegramOutboundReplyArtifactSender(deps) {
|
|
|
449
449
|
: plan.voiceText
|
|
450
450
|
? [{ text: plan.voiceText, lang: plan.lang, rate: plan.rate }]
|
|
451
451
|
: [];
|
|
452
|
+
// A plan without voice content has nothing to deliver; absence of
|
|
453
|
+
// content is not a synthesis failure.
|
|
454
|
+
if (voiceReplies.length === 0)
|
|
455
|
+
return;
|
|
452
456
|
let anyDelivered = false;
|
|
453
457
|
for (const reply of voiceReplies) {
|
|
454
458
|
if (!isDeliveryActive())
|
package/dist/lib/queue.js
CHANGED
|
@@ -1384,7 +1384,9 @@ export async function handleTelegramAgentEndRuntime(deps) {
|
|
|
1384
1384
|
}
|
|
1385
1385
|
if (!isDeliveryActive())
|
|
1386
1386
|
return;
|
|
1387
|
-
if (outboundReply &&
|
|
1387
|
+
if (outboundReply &&
|
|
1388
|
+
hasOutboundArtifacts &&
|
|
1389
|
+
deps.sendOutboundReplyArtifacts) {
|
|
1388
1390
|
try {
|
|
1389
1391
|
await deps.sendOutboundReplyArtifacts(turn, outboundReply, {
|
|
1390
1392
|
replyToPrompt: !finalText,
|
package/dist/lib/status.js
CHANGED
|
@@ -434,15 +434,26 @@ export function createTelegramRuntimeProjectionStore(options) {
|
|
|
434
434
|
throw new Error("Telegram runtime projection is malformed.");
|
|
435
435
|
delete semantic.diagnostics.recentRuntimeEvents;
|
|
436
436
|
const isCurrent = () => getPath() === path && getProfile() === profile && authority() === true;
|
|
437
|
+
const isUnchanged = (current) => {
|
|
438
|
+
if (!isRuntimeProjection(current))
|
|
439
|
+
return false;
|
|
440
|
+
const { writtenAtMs: _writtenAtMs, ...existing } = current;
|
|
441
|
+
return isDeepStrictEqual(existing, semantic);
|
|
442
|
+
};
|
|
437
443
|
const publication = publicationQueue.then(() => {
|
|
438
444
|
if (!isCurrent())
|
|
439
445
|
return false;
|
|
446
|
+
// Observational state needs no transaction when the published projection already matches.
|
|
447
|
+
try {
|
|
448
|
+
if (isUnchanged(read({ path, profile })))
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// An unreadable envelope stays with the transactional publisher's fail-closed path.
|
|
453
|
+
}
|
|
440
454
|
return (publish({ path, profile }, (current) => {
|
|
441
|
-
if (
|
|
442
|
-
|
|
443
|
-
if (isDeepStrictEqual(existing, semantic))
|
|
444
|
-
return { value: current, changed: false };
|
|
445
|
-
}
|
|
455
|
+
if (isUnchanged(current))
|
|
456
|
+
return { value: current, changed: false };
|
|
446
457
|
const writtenAtMs = getNowMs();
|
|
447
458
|
if (!Number.isSafeInteger(writtenAtMs) || writtenAtMs < 0)
|
|
448
459
|
throw new Error("Telegram runtime projection timestamp is invalid.");
|
package/dist/package.json
CHANGED
package/docs/architecture.md
CHANGED
|
@@ -273,7 +273,7 @@ Source audit confirms that canonical inventory is not yet usable as a production
|
|
|
273
273
|
The opt-in journal-store composition now separates continuations rather than adding a second prepared-worker projection. Independent review covers all 12 stateful methods, source-callback rejection before journal locking/read/recovery, actual contention, publication-failure release/retry and unchanged receipt CAS:
|
|
274
274
|
|
|
275
275
|
- `Append`: Canonicalize once → Workspace admission → choose polling observation or paired-human admission → private journal transaction. Without a sender-admission mode, use the lock-only continuation when configured. Do not wrap an admission callback in another config transaction.
|
|
276
|
-
- `Other store operations`: Lock-only config serialization → opt-in `sourceAccess` family acquisition → private journal transaction → direct consumption of that operation-local evidence. The same ordering applies after append's selected admission continuation. Acquiring before journal staging is valid only when source serialization excludes all participating family writers through consumption. Without `sourceAccess`, ordinary recovery-capable `readCurrent()` remains unchanged. Receipt/owner/handoff CAS and process-birth proof stay authoritative; no caller-supplied lock-held bypass or ambient reentrancy depth is exposed.
|
|
276
|
+
- `Other store operations`: Lock-only config serialization → opt-in `sourceAccess` family acquisition → private journal transaction → direct consumption of that operation-local evidence. The same ordering applies after append's selected admission continuation. Acquiring before journal staging is valid only when source serialization excludes all participating family writers through consumption. Without `sourceAccess`, ordinary recovery-capable `readCurrent()` remains unchanged. Ordinary `read()` is optimistic: it first runs the same strict, repair-free evidence read without config or journal guards, because atomic publication makes it safe; only a refused or reconciliation-requiring read retries once through this serialized path, which owns repair. Receipt/owner/handoff CAS and process-birth proof stay authoritative; no caller-supplied lock-held bypass or ambient reentrancy depth is exposed.
|
|
277
277
|
- `Resource binding`: Participating continuations must use the same actual config resource. Arbitrary injected callbacks cannot prove that themselves; real factory/operation tests must establish one acquisition per operation. The dispatcher is implemented and locally reviewed. `sourceAccess` requires the lock-only continuation and captures an independently approved directory plus physical inspection limits; its store integration is independently reviewed, including generated-state refusal remediation. Production wiring remains absent.
|
|
278
278
|
- `Evidence`: Real isolated probes show an outer observation plus existing admitted append times out on nested config acquisition. Current-token admission can reject an otherwise valid old-source receipt completion after token rotation; a lock-only transaction still permits exact receipt CAS while rejecting a wrong acquisition ID. Current observation also permits profile-switch/revocation cases, so it is not a lifetime fence. A journal `read()` can repair an empty foreign snapshot while another process holds config, proving append-only coverage is insufficient. These are storage/order witnesses, not permission for obsolete handlers to execute.
|
|
279
279
|
- `Remaining gates`: Serialization does not provide exact reference reconciliation, old-token migration policy, settled-handler or legacy-snapshot barriers, executable receipt freshness, whole-root quiescence, all-writer closure or final-grant readiness. The whole-root census cannot run inside a journal transaction. Final grant requires an internal already-in-config-transaction check, not reacquisition of this wrapper. Production stores, inspectors and UI remain disconnected from this candidate.
|
package/lib/journal.ts
CHANGED
|
@@ -5591,36 +5591,40 @@ function createJournalStoreCore(
|
|
|
5591
5591
|
}
|
|
5592
5592
|
};
|
|
5593
5593
|
|
|
5594
|
+
const acquireSource = (): JournalSourceAcquisition | undefined =>
|
|
5595
|
+
sourceAccess
|
|
5596
|
+
? acquireTelegramUpdateJournalFamily(
|
|
5597
|
+
{
|
|
5598
|
+
...sourceAccess,
|
|
5599
|
+
path,
|
|
5600
|
+
profile,
|
|
5601
|
+
botIdentity: expectedIdentity,
|
|
5602
|
+
},
|
|
5603
|
+
version,
|
|
5604
|
+
)
|
|
5605
|
+
: undefined;
|
|
5606
|
+
const readAcquiredSource = (
|
|
5607
|
+
source: JournalSourceAcquisition,
|
|
5608
|
+
): ReadTelegramUpdateJournalResult => {
|
|
5609
|
+
const file =
|
|
5610
|
+
source.evidence.kind === "present" ? source.evidence.file : emptyFile();
|
|
5611
|
+
return {
|
|
5612
|
+
file,
|
|
5613
|
+
exists: source.evidence.kind === "present",
|
|
5614
|
+
serializedBytes: assertCapacity(file),
|
|
5615
|
+
source,
|
|
5616
|
+
};
|
|
5617
|
+
};
|
|
5618
|
+
|
|
5594
5619
|
const runJournalTransaction = <T>(
|
|
5595
5620
|
operation: (read: typeof readCurrent) => T,
|
|
5596
5621
|
): T => {
|
|
5597
5622
|
try {
|
|
5598
5623
|
// Acquire before the transaction helper can create parents or staging names.
|
|
5599
5624
|
// The config continuation excludes participating writers through consumption.
|
|
5600
|
-
const source =
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
...sourceAccess,
|
|
5604
|
-
path,
|
|
5605
|
-
profile,
|
|
5606
|
-
botIdentity: expectedIdentity,
|
|
5607
|
-
},
|
|
5608
|
-
version,
|
|
5609
|
-
)
|
|
5610
|
-
: undefined;
|
|
5611
|
-
const readSource = (): ReadTelegramUpdateJournalResult => {
|
|
5612
|
-
if (!source) return readCurrent();
|
|
5613
|
-
const file =
|
|
5614
|
-
source.evidence.kind === "present"
|
|
5615
|
-
? source.evidence.file
|
|
5616
|
-
: emptyFile();
|
|
5617
|
-
return {
|
|
5618
|
-
file,
|
|
5619
|
-
exists: source.evidence.kind === "present",
|
|
5620
|
-
serializedBytes: assertCapacity(file),
|
|
5621
|
-
source,
|
|
5622
|
-
};
|
|
5623
|
-
};
|
|
5625
|
+
const source = acquireSource();
|
|
5626
|
+
const readSource = (): ReadTelegramUpdateJournalResult =>
|
|
5627
|
+
source ? readAcquiredSource(source) : readCurrent();
|
|
5624
5628
|
return withTelegramFileTransaction(`${path}.transaction`, () =>
|
|
5625
5629
|
operation(readSource),
|
|
5626
5630
|
);
|
|
@@ -7024,16 +7028,30 @@ function createJournalStoreCore(
|
|
|
7024
7028
|
assertCurrent();
|
|
7025
7029
|
return { issued, entries: result };
|
|
7026
7030
|
});
|
|
7031
|
+
const projectRead = (current: ReadTelegramUpdateJournalResult) => ({
|
|
7032
|
+
...cloneFile(current.file),
|
|
7033
|
+
exists: current.exists,
|
|
7034
|
+
serializedBytes: current.serializedBytes,
|
|
7035
|
+
});
|
|
7027
7036
|
const journal: TelegramUpdateJournalStore = {
|
|
7028
7037
|
read() {
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7038
|
+
// Atomic publication makes a repair-free read safe without serialization, so idle
|
|
7039
|
+
// status and polling cursor reads create no guards. Any read that needs reconciliation,
|
|
7040
|
+
// or races a compaction, falls back to the serialized path that owns repair.
|
|
7041
|
+
const attempt = (): ReadTelegramUpdateJournalResult | undefined => {
|
|
7042
|
+
try {
|
|
7043
|
+
const source = acquireSource();
|
|
7044
|
+
return source ? readAcquiredSource(source) : readCurrentStrict(false);
|
|
7045
|
+
} catch {
|
|
7046
|
+
return undefined;
|
|
7047
|
+
}
|
|
7048
|
+
};
|
|
7049
|
+
const current = options.withWriterAdmission
|
|
7050
|
+
? options.withWriterAdmission(attempt)
|
|
7051
|
+
: attempt();
|
|
7052
|
+
return current
|
|
7053
|
+
? projectRead(current)
|
|
7054
|
+
: runMutation((readCurrent) => projectRead(readCurrent()));
|
|
7037
7055
|
},
|
|
7038
7056
|
appendBatch(updates, requestedAcceptedThroughUpdateId) {
|
|
7039
7057
|
const canonicalUpdates = updates.map((update) =>
|
package/lib/outbound.ts
CHANGED
|
@@ -900,6 +900,9 @@ export function createTelegramOutboundReplyArtifactSender(
|
|
|
900
900
|
: plan.voiceText
|
|
901
901
|
? [{ text: plan.voiceText, lang: plan.lang, rate: plan.rate }]
|
|
902
902
|
: [];
|
|
903
|
+
// A plan without voice content has nothing to deliver; absence of
|
|
904
|
+
// content is not a synthesis failure.
|
|
905
|
+
if (voiceReplies.length === 0) return;
|
|
903
906
|
|
|
904
907
|
let anyDelivered = false;
|
|
905
908
|
|
package/lib/queue.ts
CHANGED
|
@@ -2356,7 +2356,11 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
2356
2356
|
}
|
|
2357
2357
|
}
|
|
2358
2358
|
if (!isDeliveryActive()) return;
|
|
2359
|
-
if (
|
|
2359
|
+
if (
|
|
2360
|
+
outboundReply &&
|
|
2361
|
+
hasOutboundArtifacts &&
|
|
2362
|
+
deps.sendOutboundReplyArtifacts
|
|
2363
|
+
) {
|
|
2360
2364
|
try {
|
|
2361
2365
|
await deps.sendOutboundReplyArtifacts(turn, outboundReply, {
|
|
2362
2366
|
replyToPrompt: !finalText,
|
package/lib/status.ts
CHANGED
|
@@ -1010,17 +1010,27 @@ export function createTelegramRuntimeProjectionStore(
|
|
|
1010
1010
|
delete semantic.diagnostics.recentRuntimeEvents;
|
|
1011
1011
|
const isCurrent = (): boolean =>
|
|
1012
1012
|
getPath() === path && getProfile() === profile && authority() === true;
|
|
1013
|
+
const isUnchanged = (
|
|
1014
|
+
current: unknown,
|
|
1015
|
+
): current is TelegramStoredRuntimeProjection => {
|
|
1016
|
+
if (!isRuntimeProjection(current)) return false;
|
|
1017
|
+
const { writtenAtMs: _writtenAtMs, ...existing } = current;
|
|
1018
|
+
return isDeepStrictEqual(existing, semantic);
|
|
1019
|
+
};
|
|
1013
1020
|
const publication = publicationQueue.then(() => {
|
|
1014
1021
|
if (!isCurrent()) return false;
|
|
1022
|
+
// Observational state needs no transaction when the published projection already matches.
|
|
1023
|
+
try {
|
|
1024
|
+
if (isUnchanged(read({ path, profile }))) return false;
|
|
1025
|
+
} catch {
|
|
1026
|
+
// An unreadable envelope stays with the transactional publisher's fail-closed path.
|
|
1027
|
+
}
|
|
1015
1028
|
return (
|
|
1016
1029
|
publish(
|
|
1017
1030
|
{ path, profile },
|
|
1018
1031
|
(current) => {
|
|
1019
|
-
if (
|
|
1020
|
-
|
|
1021
|
-
if (isDeepStrictEqual(existing, semantic))
|
|
1022
|
-
return { value: current, changed: false };
|
|
1023
|
-
}
|
|
1032
|
+
if (isUnchanged(current))
|
|
1033
|
+
return { value: current, changed: false };
|
|
1024
1034
|
const writtenAtMs = getNowMs();
|
|
1025
1035
|
if (!Number.isSafeInteger(writtenAtMs) || writtenAtMs < 0)
|
|
1026
1036
|
throw new Error(
|