@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 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.
@@ -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 = sourceAccess
3229
- ? acquireTelegramUpdateJournalFamily({
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
- return runMutation((readCurrent) => {
4097
- const current = readCurrent();
4098
- return {
4099
- ...cloneFile(current.file),
4100
- exists: current.exists,
4101
- serializedBytes: current.serializedBytes,
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));
@@ -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 && deps.sendOutboundReplyArtifacts) {
1387
+ if (outboundReply &&
1388
+ hasOutboundArtifacts &&
1389
+ deps.sendOutboundReplyArtifacts) {
1388
1390
  try {
1389
1391
  await deps.sendOutboundReplyArtifacts(turn, outboundReply, {
1390
1392
  replyToPrompt: !finalText,
@@ -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 (isRuntimeProjection(current)) {
442
- const { writtenAtMs: _writtenAtMs, ...existing } = current;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.53.0",
3
+ "version": "0.53.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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 = sourceAccess
5601
- ? acquireTelegramUpdateJournalFamily(
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
- return runMutation((readCurrent) => {
7030
- const current = readCurrent();
7031
- return {
7032
- ...cloneFile(current.file),
7033
- exists: current.exists,
7034
- serializedBytes: current.serializedBytes,
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 (outboundReply && deps.sendOutboundReplyArtifacts) {
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 (isRuntimeProjection(current)) {
1020
- const { writtenAtMs: _writtenAtMs, ...existing } = current;
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(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.53.0",
3
+ "version": "0.53.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"