instar 1.3.1346 → 1.3.1348

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.
Files changed (56) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +69 -20
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  5. package/dist/core/PostUpdateMigrator.js +14 -0
  6. package/dist/core/PostUpdateMigrator.js.map +1 -1
  7. package/dist/core/SessionManager.d.ts +71 -9
  8. package/dist/core/SessionManager.d.ts.map +1 -1
  9. package/dist/core/SessionManager.js +108 -20
  10. package/dist/core/SessionManager.js.map +1 -1
  11. package/dist/core/SingleInstanceLock.d.ts +62 -0
  12. package/dist/core/SingleInstanceLock.d.ts.map +1 -1
  13. package/dist/core/SingleInstanceLock.js +89 -4
  14. package/dist/core/SingleInstanceLock.js.map +1 -1
  15. package/dist/data/standards-guard-index.json +1 -1
  16. package/dist/data/standards-guard-index.meta.json +2 -2
  17. package/dist/data/standards-registry.meta.json +1 -1
  18. package/dist/lifeline/ServerSupervisor.d.ts +2 -0
  19. package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
  20. package/dist/lifeline/ServerSupervisor.js +42 -3
  21. package/dist/lifeline/ServerSupervisor.js.map +1 -1
  22. package/dist/scaffold/templates.d.ts.map +1 -1
  23. package/dist/scaffold/templates.js +4 -0
  24. package/dist/scaffold/templates.js.map +1 -1
  25. package/dist/server/routes.js +1 -1
  26. package/dist/server/routes.js.map +1 -1
  27. package/dist/threadline/ListenerSessionManager.d.ts +35 -2
  28. package/dist/threadline/ListenerSessionManager.d.ts.map +1 -1
  29. package/dist/threadline/ListenerSessionManager.js +48 -4
  30. package/dist/threadline/ListenerSessionManager.js.map +1 -1
  31. package/dist/threadline/ThreadlineRouter.d.ts +12 -0
  32. package/dist/threadline/ThreadlineRouter.d.ts.map +1 -1
  33. package/dist/threadline/ThreadlineRouter.js +18 -0
  34. package/dist/threadline/ThreadlineRouter.js.map +1 -1
  35. package/dist/threadline/TopicLinkageHandler.d.ts +40 -4
  36. package/dist/threadline/TopicLinkageHandler.d.ts.map +1 -1
  37. package/dist/threadline/TopicLinkageHandler.js +45 -2
  38. package/dist/threadline/TopicLinkageHandler.js.map +1 -1
  39. package/dist/threadline/autoAck.d.ts +87 -4
  40. package/dist/threadline/autoAck.d.ts.map +1 -1
  41. package/dist/threadline/autoAck.js +63 -0
  42. package/dist/threadline/autoAck.js.map +1 -1
  43. package/dist/threadline/topicRespawnHook.d.ts +17 -2
  44. package/dist/threadline/topicRespawnHook.d.ts.map +1 -1
  45. package/dist/threadline/topicRespawnHook.js +7 -2
  46. package/dist/threadline/topicRespawnHook.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/data/builtin-manifest.json +64 -64
  49. package/src/data/standards-guard-index.json +1 -1
  50. package/src/data/standards-guard-index.meta.json +2 -2
  51. package/src/data/standards-registry.meta.json +1 -1
  52. package/src/scaffold/templates.ts +4 -0
  53. package/upgrades/1.3.1347.md +38 -0
  54. package/upgrades/1.3.1348.md +24 -0
  55. package/upgrades/side-effects/relay-ack-waits-for-delivery.md +147 -0
  56. package/upgrades/side-effects/single-instance-lock-pid-reuse.md +170 -0
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAwGA,OAAO,EAAE,cAAc,EAAwD,MAAM,2BAA2B,CAAC;AAUjH,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AA4BjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAyI7D,OAAO,KAAK,EAAW,oBAAoB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACjG,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAmFtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AA0ID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAoDD,+EAA+E;AAC/E,wBAAgB,qCAAqC,CACnD,SAAS,EAAE,IAAI,CAAC,OAAO,2BAA2B,EAAE,cAAc,EAAE,QAAQ,CAAC,GAAG,IAAI,EACpF,OAAO,EAAE,MAAM,GACd,IAAI,CAEN;AA8gBD,wBAAsB,oBAAoB,CACxC,cAAc,EAAE,cAAc,EAC9B,QAAQ,EAAE,eAAe,EACzB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,aAAa,CAAC,EAAE,MAAM,EACtB,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,kBAAkB,CAAC,EAAE,MAAM;AAC3B;;qCAEqC;AACrC,WAAW,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;AACzD;;wEAEwE;AACxE,SAAS,CAAC,EAAE;IACV,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;sEAGkE;IAClE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GACA,OAAO,CAAC,MAAM,CAAC,CAqYjB;AA02BD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,YAAY,EACpB,YAAY,EAAE,oBAAoB,GAAG,IAAI,GACxC;IACD,YAAY,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAC3C,QAAQ,EAAE,MAAM,CAAC;CAClB,CAqBA;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,EAQrD,oBAAoB,CAAC,EAAE,MAAM;IAC3B,YAAY,EAAE,OAAO,kBAAkB,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACrE,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAC3C,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,IAAI,GACP,IAAI,CA42BN;AAgmBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA8/xBtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"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"}
@@ -242,7 +242,13 @@ import { ConversationStore } from '../threadline/ConversationStore.js';
242
242
  import { ThreadLog } from '../threadline/ThreadLog.js';
243
243
  import { ThreadMessageRecorder } from '../threadline/recordThreadMessage.js';
244
244
  import { recordInboundAck } from '../threadline/recordInboundAck.js';
245
- import { isAutoAckInbound, autoAckRecognition, looksLikeAutoAckText, createAckRateLimiter, runRelayAckStage, settleDeferredRelayAck } from '../threadline/autoAck.js';
245
+ import { isAutoAckInbound, autoAckRecognition, looksLikeAutoAckText, createAckRateLimiter, runRelayAckStage, createRelayAckSettler, describeUndeliveredRouterResult } from '../threadline/autoAck.js';
246
+ /** ACT-091: the session manager's observed paste → the topic handler's inject outcome (starts the busy watch lazily). */
247
+ function liveInjectOutcomeFromObservation(r) {
248
+ if (r.state === 'queued-busy')
249
+ return { state: 'queued-busy', settled: r.settle() };
250
+ return { state: r.state };
251
+ }
246
252
  import { createThreadlineTopicRespawn } from '../threadline/topicRespawnHook.js';
247
253
  import { WarrantsReplyGate, evaluateAndRecordInbound } from '../threadline/WarrantsReplyGate.js';
248
254
  import { CollaborationSurfacer } from '../threadline/CollaborationSurfacer.js';
@@ -3076,7 +3082,7 @@ getHubClassifierDeps) {
3076
3082
  },
3077
3083
  registeredSession: (topicId) => telegram.getSessionForTopic(topicId),
3078
3084
  isSessionAlive: (name) => sessionManager.isSessionAlive(name),
3079
- injectConfirmed: (name, text) => sessionManager.injectPasteNotificationConfirmed(name, text),
3085
+ injectConfirmed: async (name, text) => liveInjectOutcomeFromObservation(await sessionManager.injectPasteNotificationObserved(name, text)),
3080
3086
  leaseGate: (topicId) => g3ShouldSpawnLocally(topicId),
3081
3087
  classifyDeath: (name) => {
3082
3088
  try {
@@ -16907,9 +16913,11 @@ export async function startServer(options) {
16907
16913
  // Confirm the inject actually submitted (not left stuck at the prompt
16908
16914
  // by the paste-end race). A bare dispatch returning true is the A2 bug:
16909
16915
  // a stalled inject must NOT count as delivered, so the Telegram fallback
16910
- // surfaces the reply instead.
16916
+ // surfaces the reply instead. A paste into a MID-TURN session is
16917
+ // reported `queued-busy` (provisional) — the handler finishes the
16918
+ // route from the late verdict (ACT-091).
16911
16919
  try {
16912
- return await sessionManager.injectPasteNotificationConfirmed(sessionName, text);
16920
+ return liveInjectOutcomeFromObservation(await sessionManager.injectPasteNotificationObserved(sessionName, text));
16913
16921
  }
16914
16922
  catch {
16915
16923
  return false;
@@ -17452,11 +17460,15 @@ export async function startServer(options) {
17452
17460
  // the implicit delivery ack + the ledger disposition `no-reply` and the
17453
17461
  // handler RETURNS here, before the auto-ack send, the inbox append, the
17454
17462
  // Telegram mirror and the gate. Any other message gets our own ack
17455
- // (post-trust-verification, never for status messages, rate-limited).
17463
+ // (post-trust-verification, never for status messages, rate-limited)
17464
+ // once routing has an outcome — see relayAck below.
17456
17465
  const recordRelayDelivery = () => recordInboundAck({ a2aDeliveryTracker }, { threadId: msg.threadId, senderFingerprint, senderName: senderName ?? null });
17457
- // ACT-091: a topic-bound reply can reach no live session (its topic
17458
- // session was reaped), so its ack — which promises a reply — waits for
17459
- // the routing outcome (settleDeferredRelayAck below).
17466
+ // ACT-091: any hand-off can reach no live consumer (a reaped topic
17467
+ // session, a refused or queued spawn, a listener gone stale), so
17468
+ // our ack — which promises a reply — waits for the routing outcome on
17469
+ // EVERY path. Each terminal exit below calls relayAck.settle(...) just
17470
+ // before it returns; an exit that settles nothing (a throw) sends no
17471
+ // ack, which is the honest result when nothing received the message.
17460
17472
  const isTopicBoundReply = !!msg.threadId && threadResumeMap.get(msg.threadId)?.originTopicId !== undefined;
17461
17473
  const relayAckDeps = {
17462
17474
  autoAckEnabled: config.threadline?.autoAck !== false,
@@ -17467,8 +17479,9 @@ export async function startServer(options) {
17467
17479
  };
17468
17480
  const relayAckInput = {
17469
17481
  isAutoAck, trustLevel, msgType, senderFingerprint, threadId: msg.threadId, ticket: ledgerTicket,
17470
- deferSend: isTopicBoundReply,
17482
+ deferSend: true,
17471
17483
  };
17484
+ const relayAck = createRelayAckSettler(relayAckDeps, relayAckInput);
17472
17485
  const ackStage = runRelayAckStage(relayAckDeps, relayAckInput);
17473
17486
  if (ackStage === 'ack-consumed') {
17474
17487
  console.log(`[relay] delivery ack from ${senderName} (thread: ${msg.threadId ?? 'none'}, by ${autoAckRecognition({ type: msgType, text: textContent })}) recorded — not acked, not routed`);
@@ -17550,11 +17563,11 @@ export async function startServer(options) {
17550
17563
  else
17551
17564
  ledgerTicket?.recordNoReply();
17552
17565
  console.log(`[relay] warrants-reply gate suppressed reply (${decision.verdict.signal}) for ${senderName} thread ${gateThreadId.slice(0, 8)}`);
17553
- // ACT-091: a deferred ack on a terminal no-reply exit becomes the
17554
- // non-promising "Message received." — received, deliberately not
17555
- // answered — so the sender's tracker never reads a stuck channel.
17556
- if (relayAckInput.deferSend)
17557
- settleDeferredRelayAck(relayAckDeps, relayAckInput, { delivered: false, noReply: true });
17566
+ // ACT-091: the ack on a terminal no-reply exit (a no-reply verdict
17567
+ // or an exhausted loop budget) is the non-promising "Message
17568
+ // received." — received, deliberately not answered — so the
17569
+ // sender's tracker never reads a stuck channel.
17570
+ relayAck.settle('no-reply');
17558
17571
  // On budget exhaustion, surface ONE status notice — never silently
17559
17572
  // drop. CMT-519: route it to the SILENT Threadline hub (not a
17560
17573
  // per-event attention topic, not the parent topic the operator is
@@ -17592,8 +17605,9 @@ export async function startServer(options) {
17592
17605
  }
17593
17606
  // Phase 2a: Pipe-mode session for simple queries (lightweight, auto-exit)
17594
17607
  // Rapid-fire same-thread guard: if an active pipe session already exists for this
17595
- // thread, fall through to the listener/cold-spawn path so messages queue serially
17596
- // via ListenerSessionManager.writeToInbox instead of killing the prior tmux session.
17608
+ // thread, fall through to the listener/router path instead of killing the prior
17609
+ // tmux session. With no proven listener reader that is the router, which does not
17610
+ // know about the pipe session: a second session may answer on the same thread.
17597
17611
  if (pipeSpawner &&
17598
17612
  msg.threadId &&
17599
17613
  !threadResumeMap.get(msg.threadId) &&
@@ -17626,6 +17640,9 @@ export async function startServer(options) {
17626
17640
  if (result.spawned) {
17627
17641
  ledgerTicket?.recordHandoff('pipe');
17628
17642
  console.log(`[relay] Pipe session spawned for ${senderName} (thread: ${msg.threadId.slice(0, 8)})`);
17643
+ // ACT-091: the pipe session exists and the message is its
17644
+ // prompt — a live consumer has it, so the ack goes out now.
17645
+ relayAck.settle('pipe-spawned');
17629
17646
  return;
17630
17647
  }
17631
17648
  }
@@ -17636,6 +17653,9 @@ export async function startServer(options) {
17636
17653
  }
17637
17654
  }
17638
17655
  // Phase 2b: Route to warm listener if available and appropriate.
17656
+ // shouldUseListener is false unless a live inbox reader is PROVEN (a
17657
+ // fresh heartbeat); with none, the message falls through to the router
17658
+ // below instead of being written to a file nothing reads.
17639
17659
  // EXCEPT topic-bound replies: a reply on a thread bound to a Telegram
17640
17660
  // topic must reach handleInboundMessage → TopicLinkageHandler so it
17641
17661
  // surfaces in the bound topic. The listener inbox is a side-channel
@@ -17648,6 +17668,15 @@ export async function startServer(options) {
17648
17668
  listenerManager.writeToInbox({ from: senderFingerprint, senderName, trustLevel, threadId: msg.threadId ?? getSyntheticThreadId(senderFingerprint), text: ledgerNotice ? `[server notice: ${ledgerNotice}]\n\n${textContent}` : textContent });
17649
17669
  ledgerTicket?.recordHandoff('listener');
17650
17670
  console.log(`[relay] Routed to listener inbox from ${senderName} (trust: ${trustLevel})`);
17671
+ // ACT-091: shouldUseListener is true only while a reader's heartbeat
17672
+ // is fresh (ListenerSessionManager.hasLiveReader), so this hand-off
17673
+ // reaches a proven live reader and gets the promising ack. Re-read
17674
+ // after the write: if the heartbeat went stale in between, the write
17675
+ // is only a queue entry and the ack is withheld.
17676
+ const listenerAck = relayAck.settle(listenerManager.hasLiveReader() ? 'listener-delivered' : 'listener-queued');
17677
+ if (listenerAck === 'skipped-undelivered') {
17678
+ console.warn(`[relay] Auto-ack withheld for ${senderName} (thread: ${msg.threadId ?? 'none'}) — queued in the listener inbox; no live listener is proven`);
17679
+ }
17651
17680
  return;
17652
17681
  }
17653
17682
  // Route through ThreadlineRouter (cold-spawn path)
@@ -17688,13 +17717,33 @@ export async function startServer(options) {
17688
17717
  console.log(`[relay] Spawned session for ${senderName} (trust: ${trustLevel}, thread: ${result.threadId})`);
17689
17718
  if (result.resumed)
17690
17719
  console.log(`[relay] Resumed session for ${senderName} (thread: ${result.threadId})`);
17691
- if (result.handled && result.delivered === false) {
17720
+ if (result.handled && result.delivered === false && !result.deliveryPending) {
17692
17721
  console.warn(`[relay] Message from ${senderName} (thread: ${result.threadId}) accepted but not delivered to a live session${result.queued ? ' (queued)' : ''}`);
17693
17722
  }
17694
- if (relayAckInput.deferSend) {
17695
- const ackOutcome = settleDeferredRelayAck(relayAckDeps, relayAckInput, { delivered: result.delivered === true });
17723
+ // ACT-091: the last exit — the ack follows the router's own report of
17724
+ // a live hand-off (cold spawn, resumed/warm session, topic session).
17725
+ // A capacity refusal the spawn manager queued (`queued: true`) is
17726
+ // still undelivered: no ack, logged as queued — the decision and its
17727
+ // reasons are at describeUndeliveredRouterResult.
17728
+ if (result.deliveryPending) {
17729
+ // The reply was typed into a live session that is mid-turn, so it
17730
+ // sits as queued input: not delivered YET. The ack is settled from
17731
+ // the late verdict (bounded by the session manager's watch
17732
+ // ceiling), through the same one-shot settler — the promising ack
17733
+ // is sent only once the text has actually left the prompt.
17734
+ console.log(`[relay] Delivery to ${senderName} (thread: ${result.threadId}) pending — queued in a busy live session; the ack waits for the text to leave the prompt`);
17735
+ void result.deliveryPending.then(({ delivered, reason }) => {
17736
+ const late = relayAck.settle(delivered ? 'router-delivered' : 'router-undelivered');
17737
+ if (delivered)
17738
+ console.log(`[relay] Queued reply for ${senderName} (thread: ${result.threadId}) left the prompt (${reason}) — delivered${late === 'sent' ? ', ack sent' : ''}`);
17739
+ else
17740
+ console.warn(`[relay] Auto-ack withheld for ${senderName} (thread: ${result.threadId}) — queued text never left the prompt (${reason})`);
17741
+ });
17742
+ }
17743
+ else {
17744
+ const ackOutcome = relayAck.settle(result.delivered === true ? 'router-delivered' : 'router-undelivered');
17696
17745
  if (ackOutcome === 'skipped-undelivered') {
17697
- console.warn(`[relay] Auto-ack withheld for ${senderName} (thread: ${result.threadId}) — no live session received the message`);
17746
+ console.warn(`[relay] Auto-ack withheld for ${senderName} (thread: ${result.threadId}) — ${describeUndeliveredRouterResult(result)}`);
17698
17747
  }
17699
17748
  }
17700
17749
  }