instar 1.3.563 → 1.3.565

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 (35) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +144 -29
  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 +18 -0
  6. package/dist/core/PostUpdateMigrator.js.map +1 -1
  7. package/dist/core/SlackForwardBridge.d.ts +44 -0
  8. package/dist/core/SlackForwardBridge.d.ts.map +1 -0
  9. package/dist/core/SlackForwardBridge.js +57 -0
  10. package/dist/core/SlackForwardBridge.js.map +1 -0
  11. package/dist/core/WorkEvidence.d.ts +18 -0
  12. package/dist/core/WorkEvidence.d.ts.map +1 -1
  13. package/dist/core/WorkEvidence.js +22 -0
  14. package/dist/core/WorkEvidence.js.map +1 -1
  15. package/dist/core/types.d.ts +14 -0
  16. package/dist/core/types.d.ts.map +1 -1
  17. package/dist/core/types.js.map +1 -1
  18. package/dist/monitoring/ResumeQueue.d.ts +12 -1
  19. package/dist/monitoring/ResumeQueue.d.ts.map +1 -1
  20. package/dist/monitoring/ResumeQueue.js +26 -3
  21. package/dist/monitoring/ResumeQueue.js.map +1 -1
  22. package/dist/monitoring/ResumeQueueDrainer.d.ts +49 -0
  23. package/dist/monitoring/ResumeQueueDrainer.d.ts.map +1 -1
  24. package/dist/monitoring/ResumeQueueDrainer.js +112 -3
  25. package/dist/monitoring/ResumeQueueDrainer.js.map +1 -1
  26. package/dist/scaffold/templates.d.ts.map +1 -1
  27. package/dist/scaffold/templates.js +1 -0
  28. package/dist/scaffold/templates.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/data/builtin-manifest.json +19 -19
  31. package/src/scaffold/templates.ts +1 -0
  32. package/upgrades/1.3.564.md +40 -0
  33. package/upgrades/1.3.565.md +29 -0
  34. package/upgrades/side-effects/resume-queue-stale-emergency-pause.md +212 -0
  35. package/upgrades/side-effects/slack-pool-dispatch-to-owner.md +127 -0
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAkCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAuBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAiH7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAy3CD,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,GACpD,IAAI,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA26btE;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":"AAAA;;;;;;;;GAQG;AAkCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAuBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAkH7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAg4CD,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,GACpD,IAAI,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAshctE;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"}
@@ -80,6 +80,7 @@ import { SessionMonitor } from '../monitoring/SessionMonitor.js';
80
80
  import { SessionRecovery } from '../monitoring/SessionRecovery.js';
81
81
  import { MultiMachineCoordinator } from '../core/MultiMachineCoordinator.js';
82
82
  import { isRemotelyHandled } from '../core/SessionRouter.js';
83
+ import { isSlackSessionKey, reconstructSlackMessage } from '../core/SlackForwardBridge.js';
83
84
  import { formatForwardedTopicContext } from '../core/ForwardedTopicContext.js';
84
85
  import { resolveAdvertisedMeshUrl, advertiseSelfMeshUrl } from '../core/MeshUrlAdvertiser.js';
85
86
  import { relayOutbound } from '../core/TelegramRelay.js';
@@ -556,6 +557,13 @@ let _projectDir = process.cwd();
556
557
  let _sharedIntelligence = null;
557
558
  let _selfKnowledgeTree = null;
558
559
  let _slackAdapter = null;
560
+ // WS1.1 dispatch-to-owner (Slack arm): the owner-side bridge reconstructs a Slack
561
+ // inbound Message from a forwarded mesh deliverMessage and replays it through the
562
+ // SAME local dispatch the live inbound path uses. Set once in startServer's Slack
563
+ // block; null when Slack is not configured (the owner-side Slack branch then
564
+ // no-ops — a forwarded Slack key on a Slack-less owner is a misconfiguration the
565
+ // router's placement should never produce, and falling through is harmless).
566
+ let _slackInboundDispatch = null;
559
567
  // SessionRefresh — agent-initiated respawn. Module-scope so onRestartSession
560
568
  // (defined outside startServer) can delegate to it once startServer wires it.
561
569
  // Null until startServer constructs it; the Telegram /restart handler falls
@@ -5529,7 +5537,14 @@ export async function startServer(options) {
5529
5537
  console.warn('[slack] ambient gate wiring skipped:', e.message);
5530
5538
  }
5531
5539
  // Wire message handler — inject Slack messages into sessions
5532
- slackAdapter.onMessage(async (message) => {
5540
+ // ── WS1.1 dispatch-to-owner (Slack arm) ──────────────────────────────
5541
+ // The actual channel→session dispatch for a Slack inbound message. This is
5542
+ // the body the live inbound `onMessage` handler runs AFTER pool routing has
5543
+ // decided the message belongs to THIS machine — AND the body the owner-side
5544
+ // mesh bridge replays when a Slack message was forwarded to this machine
5545
+ // because it owns the conversation. Sharing one function is "Structure >
5546
+ // Willpower": the forwarded path and the live path can never drift.
5547
+ const slackInboundDispatch = async (message) => {
5533
5548
  const channelId = message.channel.identifier;
5534
5549
  const isDM = message.metadata?.isDM;
5535
5550
  const senderName = message.metadata?.senderName || 'User';
@@ -5547,33 +5562,6 @@ export async function startServer(options) {
5547
5562
  const isThreadSession = slackAdapter.isThreadRoutingKey(routingKey);
5548
5563
  // The thread_ts to thread replies under (only when this is a thread session).
5549
5564
  const replyThreadTs = isThreadSession ? threadTs : undefined;
5550
- // Sentinel intercept — classify message for emergency stop/pause
5551
- if (sentinel) {
5552
- try {
5553
- const classification = await sentinel.classify(message.content);
5554
- if (classification.category === 'emergency-stop') {
5555
- // Kill all sessions
5556
- const sessions = sessionManager.listRunningSessions();
5557
- for (const s of sessions) {
5558
- try {
5559
- sessionManager.killSession(s.id);
5560
- }
5561
- catch { /* ok */ }
5562
- }
5563
- slackAdapter.sendToChannel(channelId, '🛑 Emergency stop — all sessions killed.').catch(() => { });
5564
- return;
5565
- }
5566
- else if (classification.category === 'pause') {
5567
- const existingSession = slackAdapter.getSessionForChannel(routingKey);
5568
- if (existingSession) {
5569
- sessionManager.sendKey(existingSession, 'Escape');
5570
- slackAdapter.sendToChannel(channelId, '⏸️ Session paused.').catch(() => { });
5571
- }
5572
- return;
5573
- }
5574
- }
5575
- catch { /* fail-open — if Sentinel errors, process message normally */ }
5576
- }
5577
5565
  // Build injection tag with sender info (matches Telegram's buildInjectionTag pattern)
5578
5566
  const slackUserId = message.metadata?.slackUserId;
5579
5567
  // Sanitize sender name at injection boundary (prevents injection attacks)
@@ -5730,6 +5718,88 @@ export async function startServer(options) {
5730
5718
  catch (err) {
5731
5719
  console.error(`[slack] Session spawn failed: ${err instanceof Error ? err.message : err}`);
5732
5720
  }
5721
+ };
5722
+ // Expose to the owner-side mesh bridge (a forwarded Slack message replays
5723
+ // through the same dispatch on the machine that owns the conversation).
5724
+ _slackInboundDispatch = slackInboundDispatch;
5725
+ slackAdapter.onMessage(async (message) => {
5726
+ const channelId = message.channel.identifier;
5727
+ const messageTs = message.metadata?.ts;
5728
+ const threadTs = message.metadata?.threadTs;
5729
+ const routingKey = slackAdapter.resolveRoutingKey(channelId, threadTs, messageTs);
5730
+ // Sentinel intercept — classify message for emergency stop/pause. Runs on
5731
+ // the machine the message arrived at (these are local-process actions);
5732
+ // never forwarded.
5733
+ if (sentinel) {
5734
+ try {
5735
+ const classification = await sentinel.classify(message.content);
5736
+ if (classification.category === 'emergency-stop') {
5737
+ // Kill all sessions
5738
+ const sessions = sessionManager.listRunningSessions();
5739
+ for (const s of sessions) {
5740
+ try {
5741
+ sessionManager.killSession(s.id);
5742
+ }
5743
+ catch { /* ok */ }
5744
+ }
5745
+ slackAdapter.sendToChannel(channelId, '🛑 Emergency stop — all sessions killed.').catch(() => { });
5746
+ return;
5747
+ }
5748
+ else if (classification.category === 'pause') {
5749
+ const existingSession = slackAdapter.getSessionForChannel(routingKey);
5750
+ if (existingSession) {
5751
+ sessionManager.sendKey(existingSession, 'Escape');
5752
+ slackAdapter.sendToChannel(channelId, '⏸️ Session paused.').catch(() => { });
5753
+ }
5754
+ return;
5755
+ }
5756
+ }
5757
+ catch { /* fail-open — if Sentinel errors, process message normally */ }
5758
+ }
5759
+ // ── Multi-Machine Session Pool (§L4 / WS1.1): route through the pool ──
5760
+ // Mirrors the Telegram inbound dispatch. When the rollout stage is past
5761
+ // 'dark', consult the SessionRouter on the Slack routingKey: it may
5762
+ // forward this conversation's message to the machine that OWNS the session
5763
+ // (over the mesh) instead of binding it to whatever local session happens
5764
+ // to be running. DARK (the default) skips this block entirely → the Slack
5765
+ // inbound path is byte-identical to today's local-only dispatch. Any error
5766
+ // falls back to the local dispatch below (fail-safe). This closes the bug
5767
+ // where a Slack channel pinned/transferred to a peer machine still injected
5768
+ // the next message into the already-running LOCAL session (Telegram's
5769
+ // inbound path already followed the transfer; Slack's never did).
5770
+ if (_sessionRouter && _sessionPoolStage() !== 'dark') {
5771
+ try {
5772
+ const outcome = await _sessionRouter.route({
5773
+ sessionKey: routingKey,
5774
+ messageId: String(message.id),
5775
+ payload: message.content,
5776
+ topicMetadata: _topicPinStore?.asTopicMetadata(routingKey),
5777
+ senderEnvelope: {
5778
+ userId: message.metadata?.slackUserId || undefined,
5779
+ firstName: message.metadata?.senderName || undefined,
5780
+ },
5781
+ });
5782
+ console.log(`[session-pool] slack route key=${routingKey} → action=${outcome.action} owner=${outcome.owner ?? '?'} self=${_meshSelfId ?? '?'} acked=${outcome.acked}`);
5783
+ if (isRemotelyHandled(outcome, _meshSelfId)) {
5784
+ console.log(`[session-pool] slack key ${routingKey} handled by owner ${outcome.owner ?? '?'} (${outcome.action}) — not dispatching locally`);
5785
+ return;
5786
+ }
5787
+ // Custody-ack short-circuit (§2.2, mirrors the Telegram path): a
5788
+ // queued/placement-blocked verdict whose enqueue COMMITTED (acked) is
5789
+ // the durable queue's message now — no local fall-through (which would
5790
+ // double-handle). Un-custodied (refused/off/dry-run) falls through.
5791
+ if ((outcome.action === 'queued' || outcome.action === 'placement-blocked') && outcome.acked) {
5792
+ console.log(`[session-pool] slack key ${routingKey} in durable custody (${outcome.detail ?? outcome.action}) — drain will deliver`);
5793
+ return;
5794
+ }
5795
+ // 'handled-locally' / self 'spawned'/'owner-dead-replaced' / un-acked
5796
+ // 'queued'/'placement-blocked' → fall through to local dispatch below.
5797
+ }
5798
+ catch (err) {
5799
+ console.warn(`[session-pool] slack route error for key ${routingKey} — falling back to local dispatch: ${err instanceof Error ? err.message : String(err)}`);
5800
+ }
5801
+ }
5802
+ await slackInboundDispatch(message);
5733
5803
  });
5734
5804
  await slackAdapter.start();
5735
5805
  _slackAdapter = slackAdapter;
@@ -6396,6 +6466,13 @@ export async function startServer(options) {
6396
6466
  breakerThreshold: rqCfg.breakerThreshold ?? 3,
6397
6467
  breakerCooldownMin: rqCfg.breakerCooldownMin ?? 30,
6398
6468
  tier1Check: rqCfg.tier1Check ?? true,
6469
+ // Stale-emergency-pause auto-recovery (spec:
6470
+ // resume-queue-stale-emergency-pause.md). CODE-defaulted like the
6471
+ // other resumeQueue.* keys (never frozen into ConfigDefaults — the
6472
+ // fleet flip stays in code). Layer 1 (paused-with-waiting alert) is
6473
+ // always on; autoResumeStalePause gates only Layer 2.
6474
+ staleEmergencyPauseAutoResumeMin: rqCfg.staleEmergencyPauseAutoResumeMin ?? 60,
6475
+ autoResumeStalePause: rqCfg.autoResumeStalePause ?? true,
6399
6476
  });
6400
6477
  resumeDrainer.start();
6401
6478
  console.log(pc.green(` ResumeQueue started (${(rqCfg.dryRun ?? !resolveDevAgentGate(undefined, config)) ? 'dry-run observe-only' : 'LIVE'}; drainer ${rqCfg.drainIntervalSec ?? 60}s tick)`));
@@ -13690,7 +13767,45 @@ export async function startServer(options) {
13690
13767
  if (Number.isFinite(wsTopic))
13691
13768
  _topicProfileCarrier?.onTopicAcquired(wsTopic);
13692
13769
  }
13693
- if (_sessionPoolStage() === 'dark' || !telegram)
13770
+ if (_sessionPoolStage() === 'dark')
13771
+ return;
13772
+ // ── WS1.1 Slack arm (owner-side bridge) ──────────────────────────
13773
+ // A Slack routing key is a non-numeric string (`C…` channel id, or
13774
+ // `C…:<thread_ts>` for a thread session); a Telegram topic key is a
13775
+ // pure number. When the forwarded session key isn't numeric, this is a
13776
+ // Slack conversation that was forwarded here because THIS machine owns
13777
+ // it: reconstruct the inbound Message and replay it through the SAME
13778
+ // local Slack dispatch the live path uses (which itself handles
13779
+ // inject-into-live-session vs spawn). Fire-and-forget: the durable
13780
+ // receipt is already recorded + ACKed before this runs.
13781
+ const slackKey = cmd.session;
13782
+ if (isSlackSessionKey(slackKey)) {
13783
+ if (!_slackInboundDispatch)
13784
+ return; // Slack not configured here
13785
+ const sText = typeof cmd.payload === 'string'
13786
+ ? cmd.payload
13787
+ : (cmd.payload && typeof cmd.payload === 'object' && 'text' in cmd.payload)
13788
+ ? String(cmd.payload.text)
13789
+ : '';
13790
+ const envUid = cmd.senderEnvelope?.userId;
13791
+ const sMessage = reconstructSlackMessage({
13792
+ sessionKey: slackKey,
13793
+ messageId: cmd.messageId,
13794
+ text: sText,
13795
+ senderUserId: envUid != null ? String(envUid) : undefined,
13796
+ });
13797
+ void _slackInboundDispatch(sMessage)
13798
+ .then(() => {
13799
+ console.log(pc.green(` [session-pool] owner-side Slack dispatch for forwarded key ${slackKey}`));
13800
+ _inboundQueue?.markRemoteInjected(cmd.session, cmd.messageId);
13801
+ })
13802
+ .catch((err) => {
13803
+ console.warn(` [session-pool] owner-side Slack dispatch failed for key ${slackKey}: ${err instanceof Error ? err.message : String(err)}`);
13804
+ _inboundQueue?.reportPeerInjectError(cmd.session, cmd.messageId, err instanceof Error ? err.message : String(err));
13805
+ });
13806
+ return;
13807
+ }
13808
+ if (!telegram)
13694
13809
  return;
13695
13810
  const tg = telegram;
13696
13811
  const topicId = Number(cmd.session);