instar 1.3.1345 → 1.3.1347

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 (55) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +159 -9
  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 +28 -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/SpawnAdmission.d.ts +1 -1
  12. package/dist/core/SpawnAdmission.d.ts.map +1 -1
  13. package/dist/core/SpawnAdmission.js.map +1 -1
  14. package/dist/core/SpawningTopicsRegistry.d.ts +13 -0
  15. package/dist/core/SpawningTopicsRegistry.d.ts.map +1 -1
  16. package/dist/core/SpawningTopicsRegistry.js +19 -0
  17. package/dist/core/SpawningTopicsRegistry.js.map +1 -1
  18. package/dist/data/standards-guard-index.json +1 -1
  19. package/dist/data/standards-guard-index.meta.json +2 -2
  20. package/dist/data/standards-registry.meta.json +1 -1
  21. package/dist/scaffold/templates.d.ts.map +1 -1
  22. package/dist/scaffold/templates.js +8 -0
  23. package/dist/scaffold/templates.js.map +1 -1
  24. package/dist/server/routes.js +1 -1
  25. package/dist/server/routes.js.map +1 -1
  26. package/dist/threadline/ListenerSessionManager.d.ts +35 -2
  27. package/dist/threadline/ListenerSessionManager.d.ts.map +1 -1
  28. package/dist/threadline/ListenerSessionManager.js +48 -4
  29. package/dist/threadline/ListenerSessionManager.js.map +1 -1
  30. package/dist/threadline/ThreadlineRouter.d.ts +12 -0
  31. package/dist/threadline/ThreadlineRouter.d.ts.map +1 -1
  32. package/dist/threadline/ThreadlineRouter.js +61 -16
  33. package/dist/threadline/ThreadlineRouter.js.map +1 -1
  34. package/dist/threadline/TopicLinkageHandler.d.ts +70 -4
  35. package/dist/threadline/TopicLinkageHandler.d.ts.map +1 -1
  36. package/dist/threadline/TopicLinkageHandler.js +79 -5
  37. package/dist/threadline/TopicLinkageHandler.js.map +1 -1
  38. package/dist/threadline/autoAck.d.ts +111 -0
  39. package/dist/threadline/autoAck.d.ts.map +1 -1
  40. package/dist/threadline/autoAck.js +98 -2
  41. package/dist/threadline/autoAck.js.map +1 -1
  42. package/dist/threadline/topicRespawnHook.d.ts +82 -0
  43. package/dist/threadline/topicRespawnHook.d.ts.map +1 -0
  44. package/dist/threadline/topicRespawnHook.js +103 -0
  45. package/dist/threadline/topicRespawnHook.js.map +1 -0
  46. package/package.json +1 -1
  47. package/src/data/builtin-manifest.json +64 -64
  48. package/src/data/standards-guard-index.json +1 -1
  49. package/src/data/standards-guard-index.meta.json +2 -2
  50. package/src/data/standards-registry.meta.json +1 -1
  51. package/src/scaffold/templates.ts +8 -0
  52. package/upgrades/1.3.1346.md +27 -0
  53. package/upgrades/1.3.1347.md +38 -0
  54. package/upgrades/side-effects/relay-ack-waits-for-delivery.md +147 -0
  55. package/upgrades/side-effects/threadline-reaped-topic-respawn.md +111 -0
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"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"}
@@ -242,7 +242,14 @@ 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 } 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
+ }
252
+ import { createThreadlineTopicRespawn } from '../threadline/topicRespawnHook.js';
246
253
  import { WarrantsReplyGate, evaluateAndRecordInbound } from '../threadline/WarrantsReplyGate.js';
247
254
  import { CollaborationSurfacer } from '../threadline/CollaborationSurfacer.js';
248
255
  import { ListenerSessionManager } from '../threadline/ListenerSessionManager.js';
@@ -586,6 +593,12 @@ let _sweptInboundStore = null;
586
593
  // The drain's local-delivery tail (§3.1 via:'drain') — assigned inside
587
594
  // wireTelegramRouting where the session primitives live in scope.
588
595
  let _drainLocalDeliver = null;
596
+ // ACT-091: wake a topic's dead/reaped session for a topic-bound Threadline
597
+ // reply — assigned inside wireTelegramRouting where the respawn primitives
598
+ // (and the per-topic spawn guard) live in scope. Null ⇒ not wired.
599
+ let _threadlineTopicRespawn = null;
600
+ /** ACT-091: "is a spawn for this topic in flight?" — the spawningTopics read, for the handler. */
601
+ let _threadlineTopicSpawning = null;
589
602
  // Emergency-stop integration (§3.6): marks a topic stopped for the drain's
590
603
  // pass/batch/chokepoint consults AND settles its custody (terminal
591
604
  // operator-stop + PIS cleanup + loss report). Set with the engine.
@@ -3049,6 +3062,72 @@ getHubClassifierDeps) {
3049
3062
  spawningTopics.clear(topicId, _spawnTokD);
3050
3063
  }
3051
3064
  };
3065
+ // ── ACT-091: topic-bound Threadline reply into a dead/reaped topic session ──
3066
+ // The decision lives in createThreadlineTopicRespawn (testable); this binds
3067
+ // it to the REAL primitives the Telegram dead-session path uses: ownership,
3068
+ // the G3 lease gate, the death classification, SpawnAdmission (its own
3069
+ // journal-only callsite), the spawningTopics guard, respawnSessionForTopic
3070
+ // (--resume) / spawnSessionForTopic, each awaited through the initial
3071
+ // injection. A spawn already in flight is WAITED for, then the reply is typed
3072
+ // into the live session — a second reply inside one respawn is never lost.
3073
+ {
3074
+ _threadlineTopicSpawning = (topicId) => spawningTopics.has(topicId);
3075
+ _threadlineTopicRespawn = createThreadlineTopicRespawn({
3076
+ spawning: spawningTopics,
3077
+ ownedElsewhere: (topicId) => {
3078
+ if (!_ownershipReadForDrain || !_meshSelfId)
3079
+ return false;
3080
+ const ownRec = _ownershipReadForDrain(String(topicId));
3081
+ return !!ownRec && ownRec.status === 'active' && ownRec.ownerMachineId !== _meshSelfId;
3082
+ },
3083
+ registeredSession: (topicId) => telegram.getSessionForTopic(topicId),
3084
+ isSessionAlive: (name) => sessionManager.isSessionAlive(name),
3085
+ injectConfirmed: async (name, text) => liveInjectOutcomeFromObservation(await sessionManager.injectPasteNotificationObserved(name, text)),
3086
+ leaseGate: (topicId) => g3ShouldSpawnLocally(topicId),
3087
+ classifyDeath: (name) => {
3088
+ try {
3089
+ const output = sessionManager.captureOutput(name, 100);
3090
+ if (!output)
3091
+ return null;
3092
+ const death = classifySessionDeath(output, quotaTracker?.getState() ?? null);
3093
+ if (death.cause === 'quota_exhaustion' && death.confidence !== 'low')
3094
+ return 'quota';
3095
+ if (death.cause === 'context_exhausted')
3096
+ return 'context-exhausted';
3097
+ return 'other';
3098
+ }
3099
+ catch {
3100
+ return null; /* @silent-fallback-ok: classification failed — respawn as the Telegram dead path does */
3101
+ }
3102
+ },
3103
+ admit: (topicId) => {
3104
+ if (!_spawnAdmission)
3105
+ return { allow: true };
3106
+ try {
3107
+ const d = _spawnAdmission.admit({ sessionKey: String(topicId), callsite: 'threadline-topic-respawn' });
3108
+ if (d.allow)
3109
+ _ladderDryRunConsult(d, String(topicId), topicId);
3110
+ return { allow: d.allow, reason: d.reason };
3111
+ }
3112
+ catch (err) {
3113
+ // Same posture as the Telegram seam: an admission ERROR fails open.
3114
+ console.warn(`[spawn-admission] threadline-topic-respawn admit error (fail-open): ${err instanceof Error ? err.message : String(err)}`);
3115
+ return { allow: true };
3116
+ }
3117
+ },
3118
+ respawn: async (name, topicId, text, fresh) => {
3119
+ if (fresh)
3120
+ _topicResumeMap?.remove(topicId);
3121
+ await respawnSessionForTopic(sessionManager, telegram, name, topicId, text, topicMemory, undefined, undefined, { silent: true, awaitInitialInjection: true });
3122
+ },
3123
+ coldSpawn: async (topicId, text) => {
3124
+ const topicName = telegram.getTopicName(topicId) || `topic-${topicId}`;
3125
+ const newSessionName = await spawnSessionForTopic(sessionManager, telegram, topicName, topicId, text, topicMemory, undefined, undefined, undefined, { awaitInitialInjection: true });
3126
+ telegram.registerTopicSession(topicId, newSessionName, topicName);
3127
+ return newSessionName;
3128
+ },
3129
+ });
3130
+ }
3052
3131
  }
3053
3132
  /**
3054
3133
  * Wire WhatsApp message routing: incoming messages → Claude sessions.
@@ -16834,9 +16913,11 @@ export async function startServer(options) {
16834
16913
  // Confirm the inject actually submitted (not left stuck at the prompt
16835
16914
  // by the paste-end race). A bare dispatch returning true is the A2 bug:
16836
16915
  // a stalled inject must NOT count as delivered, so the Telegram fallback
16837
- // 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).
16838
16919
  try {
16839
- return await sessionManager.injectPasteNotificationConfirmed(sessionName, text);
16920
+ return liveInjectOutcomeFromObservation(await sessionManager.injectPasteNotificationObserved(sessionName, text));
16840
16921
  }
16841
16922
  catch {
16842
16923
  return false;
@@ -16856,6 +16937,12 @@ export async function startServer(options) {
16856
16937
  getSessionForTopic: telegram
16857
16938
  ? (topicId) => telegram.getSessionForTopic(topicId)
16858
16939
  : undefined,
16940
+ // ACT-091: a reply into a reaped topic session wakes it (late-bound —
16941
+ // Telegram routing is wired after this handler is constructed).
16942
+ respawnTopicSession: (args) => _threadlineTopicRespawn
16943
+ ? _threadlineTopicRespawn(args)
16944
+ : Promise.resolve({ delivered: false, reason: 'telegram-routing-not-wired' }),
16945
+ isTopicSpawning: (topicId) => _threadlineTopicSpawning?.(topicId) === true,
16859
16946
  // A2A cross-machine route §5: a topic-bound reply is typed into the
16860
16947
  // topic's live session on whichever machine has it. Late-bound: the
16861
16948
  // mesh client exists only after the multi-machine wiring below.
@@ -17373,15 +17460,29 @@ export async function startServer(options) {
17373
17460
  // the implicit delivery ack + the ledger disposition `no-reply` and the
17374
17461
  // handler RETURNS here, before the auto-ack send, the inbox append, the
17375
17462
  // Telegram mirror and the gate. Any other message gets our own ack
17376
- // (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.
17377
17465
  const recordRelayDelivery = () => recordInboundAck({ a2aDeliveryTracker }, { threadId: msg.threadId, senderFingerprint, senderName: senderName ?? null });
17378
- const ackStage = runRelayAckStage({
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.
17472
+ const isTopicBoundReply = !!msg.threadId && threadResumeMap.get(msg.threadId)?.originTopicId !== undefined;
17473
+ const relayAckDeps = {
17379
17474
  autoAckEnabled: config.threadline?.autoAck !== false,
17380
17475
  autoAckMessage: config.threadline?.autoAckMessage,
17381
17476
  isAckRateLimited,
17382
17477
  sendAck: (to, text, threadId) => { threadlineRelayClient.sendAck(to, text, threadId); },
17383
17478
  recordDelivery: recordRelayDelivery,
17384
- }, { isAutoAck, trustLevel, msgType, senderFingerprint, threadId: msg.threadId, ticket: ledgerTicket });
17479
+ };
17480
+ const relayAckInput = {
17481
+ isAutoAck, trustLevel, msgType, senderFingerprint, threadId: msg.threadId, ticket: ledgerTicket,
17482
+ deferSend: true,
17483
+ };
17484
+ const relayAck = createRelayAckSettler(relayAckDeps, relayAckInput);
17485
+ const ackStage = runRelayAckStage(relayAckDeps, relayAckInput);
17385
17486
  if (ackStage === 'ack-consumed') {
17386
17487
  console.log(`[relay] delivery ack from ${senderName} (thread: ${msg.threadId ?? 'none'}, by ${autoAckRecognition({ type: msgType, text: textContent })}) recorded — not acked, not routed`);
17387
17488
  return;
@@ -17462,6 +17563,11 @@ export async function startServer(options) {
17462
17563
  else
17463
17564
  ledgerTicket?.recordNoReply();
17464
17565
  console.log(`[relay] warrants-reply gate suppressed reply (${decision.verdict.signal}) for ${senderName} thread ${gateThreadId.slice(0, 8)}`);
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');
17465
17571
  // On budget exhaustion, surface ONE status notice — never silently
17466
17572
  // drop. CMT-519: route it to the SILENT Threadline hub (not a
17467
17573
  // per-event attention topic, not the parent topic the operator is
@@ -17499,8 +17605,9 @@ export async function startServer(options) {
17499
17605
  }
17500
17606
  // Phase 2a: Pipe-mode session for simple queries (lightweight, auto-exit)
17501
17607
  // Rapid-fire same-thread guard: if an active pipe session already exists for this
17502
- // thread, fall through to the listener/cold-spawn path so messages queue serially
17503
- // 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.
17504
17611
  if (pipeSpawner &&
17505
17612
  msg.threadId &&
17506
17613
  !threadResumeMap.get(msg.threadId) &&
@@ -17533,6 +17640,9 @@ export async function startServer(options) {
17533
17640
  if (result.spawned) {
17534
17641
  ledgerTicket?.recordHandoff('pipe');
17535
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');
17536
17646
  return;
17537
17647
  }
17538
17648
  }
@@ -17543,6 +17653,9 @@ export async function startServer(options) {
17543
17653
  }
17544
17654
  }
17545
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.
17546
17659
  // EXCEPT topic-bound replies: a reply on a thread bound to a Telegram
17547
17660
  // topic must reach handleInboundMessage → TopicLinkageHandler so it
17548
17661
  // surfaces in the bound topic. The listener inbox is a side-channel
@@ -17551,11 +17664,19 @@ export async function startServer(options) {
17551
17664
  // for the user even though transport succeeded. (The pipe-spawn branch
17552
17665
  // above is already excluded for topic-bound threads via its
17553
17666
  // `!threadResumeMap.get(...)` guard, now that get() no longer nulls them.)
17554
- const isTopicBoundReply = !!msg.threadId && threadResumeMap.get(msg.threadId)?.originTopicId !== undefined;
17555
17667
  if (listenerManager && listenerManager.shouldUseListener(trustLevel, textContent.length) && !isTopicBoundReply) {
17556
17668
  listenerManager.writeToInbox({ from: senderFingerprint, senderName, trustLevel, threadId: msg.threadId ?? getSyntheticThreadId(senderFingerprint), text: ledgerNotice ? `[server notice: ${ledgerNotice}]\n\n${textContent}` : textContent });
17557
17669
  ledgerTicket?.recordHandoff('listener');
17558
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
+ }
17559
17680
  return;
17560
17681
  }
17561
17682
  // Route through ThreadlineRouter (cold-spawn path)
@@ -17596,6 +17717,35 @@ export async function startServer(options) {
17596
17717
  console.log(`[relay] Spawned session for ${senderName} (trust: ${trustLevel}, thread: ${result.threadId})`);
17597
17718
  if (result.resumed)
17598
17719
  console.log(`[relay] Resumed session for ${senderName} (thread: ${result.threadId})`);
17720
+ if (result.handled && result.delivered === false && !result.deliveryPending) {
17721
+ console.warn(`[relay] Message from ${senderName} (thread: ${result.threadId}) accepted but not delivered to a live session${result.queued ? ' (queued)' : ''}`);
17722
+ }
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');
17745
+ if (ackOutcome === 'skipped-undelivered') {
17746
+ console.warn(`[relay] Auto-ack withheld for ${senderName} (thread: ${result.threadId}) — ${describeUndeliveredRouterResult(result)}`);
17747
+ }
17748
+ }
17599
17749
  }
17600
17750
  // Relay client is passed to AgentServer → RouteContext for the /threadline/relay-send endpoint
17601
17751
  // Report the client's ACTUAL state. This line used to say "connected"