cli-jaw 2.17.46 → 2.17.47
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/README.md +4 -0
- package/dist/bin/commands/browser-web-ai.js +29 -10
- package/dist/bin/commands/browser-web-ai.js.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/src/agent/claude-runtime-run.js +2 -1
- package/dist/src/agent/claude-runtime-run.js.map +1 -1
- package/dist/src/agent/lifecycle-handler.js +15 -4
- package/dist/src/agent/lifecycle-handler.js.map +1 -1
- package/dist/src/agent/pi-runtime.js +103 -86
- package/dist/src/agent/pi-runtime.js.map +1 -1
- package/dist/src/agent/spawn/exit-settle.js +74 -0
- package/dist/src/agent/spawn/exit-settle.js.map +1 -0
- package/dist/src/agent/spawn/kill-reason.js +38 -0
- package/dist/src/agent/spawn/kill-reason.js.map +1 -0
- package/dist/src/agent/spawn/queue.js +8 -0
- package/dist/src/agent/spawn/queue.js.map +1 -1
- package/dist/src/agent/spawn/steer-event.js +33 -0
- package/dist/src/agent/spawn/steer-event.js.map +1 -0
- package/dist/src/agent/spawn.js +157 -173
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/agent/watchdog.js +9 -4
- package/dist/src/agent/watchdog.js.map +1 -1
- package/dist/src/browser/adaptive-fetch/browser-escalation.js +18 -1
- package/dist/src/browser/adaptive-fetch/browser-escalation.js.map +1 -1
- package/dist/src/browser/adaptive-fetch/browser-flow.js +36 -9
- package/dist/src/browser/adaptive-fetch/browser-flow.js.map +1 -1
- package/dist/src/browser/adaptive-fetch/browser-session.js +10 -1
- package/dist/src/browser/adaptive-fetch/browser-session.js.map +1 -1
- package/dist/src/browser/adaptive-fetch/camoufox-session.js +16 -1
- package/dist/src/browser/adaptive-fetch/camoufox-session.js.map +1 -1
- package/dist/src/browser/adaptive-fetch/defuddle-provenance.js +49 -0
- package/dist/src/browser/adaptive-fetch/defuddle-provenance.js.map +1 -0
- package/dist/src/browser/adaptive-fetch/endpoint-resolvers.js +61 -31
- package/dist/src/browser/adaptive-fetch/endpoint-resolvers.js.map +1 -1
- package/dist/src/browser/adaptive-fetch/fetcher.js +12 -1
- package/dist/src/browser/adaptive-fetch/fetcher.js.map +1 -1
- package/dist/src/browser/adaptive-fetch/live-smoke-manifest.js +12 -18
- package/dist/src/browser/adaptive-fetch/live-smoke-manifest.js.map +1 -1
- package/dist/src/browser/adaptive-fetch/public-endpoint-normalizers.js +85 -1
- package/dist/src/browser/adaptive-fetch/public-endpoint-normalizers.js.map +1 -1
- package/dist/src/browser/adaptive-fetch/safety.js +5 -3
- package/dist/src/browser/adaptive-fetch/safety.js.map +1 -1
- package/dist/src/browser/adaptive-fetch/scheduler.js +34 -5
- package/dist/src/browser/adaptive-fetch/scheduler.js.map +1 -1
- package/dist/src/browser/adaptive-fetch/tls-fetch.js +54 -33
- package/dist/src/browser/adaptive-fetch/tls-fetch.js.map +1 -1
- package/dist/src/browser/adaptive-fetch/vendor/README.md +33 -1
- package/dist/src/browser/adaptive-fetch/ytdlp-reader.js +54 -9
- package/dist/src/browser/adaptive-fetch/ytdlp-reader.js.map +1 -1
- package/dist/src/browser/web-ai/capability-observation-presets.js +12 -10
- package/dist/src/browser/web-ai/capability-observation-presets.js.map +1 -1
- package/dist/src/browser/web-ai/chatgpt-model.js +80 -7
- package/dist/src/browser/web-ai/chatgpt-model.js.map +1 -1
- package/dist/src/browser/web-ai/gemini-model.js +18 -0
- package/dist/src/browser/web-ai/gemini-model.js.map +1 -1
- package/dist/src/browser/web-ai/grok-model.js +64 -15
- package/dist/src/browser/web-ai/grok-model.js.map +1 -1
- package/dist/src/code-mode/providers/codex-app.js +83 -0
- package/dist/src/code-mode/providers/codex-app.js.map +1 -1
- package/dist/src/core/config.js +77 -73
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/db.js +227 -151
- package/dist/src/core/db.js.map +1 -1
- package/dist/src/core/settings-merge.js +78 -102
- package/dist/src/core/settings-merge.js.map +1 -1
- package/dist/src/discord/bot.js +37 -54
- package/dist/src/discord/bot.js.map +1 -1
- package/dist/src/discord/discord-file.js +29 -3
- package/dist/src/discord/discord-file.js.map +1 -1
- package/dist/src/discord/send-only-client.js +56 -8
- package/dist/src/discord/send-only-client.js.map +1 -1
- package/dist/src/manager/routes/telegram-hub.js +8 -1
- package/dist/src/manager/routes/telegram-hub.js.map +1 -1
- package/dist/src/manager/telegram-hub/hub-bot.js +7 -1
- package/dist/src/manager/telegram-hub/hub-bot.js.map +1 -1
- package/dist/src/messaging/delivery-outcome.js +30 -0
- package/dist/src/messaging/delivery-outcome.js.map +1 -1
- package/dist/src/messaging/file-receipt.js +49 -0
- package/dist/src/messaging/file-receipt.js.map +1 -0
- package/dist/src/messaging/native-body.js +31 -0
- package/dist/src/messaging/native-body.js.map +1 -0
- package/dist/src/messaging/queue-notice-record.js +48 -0
- package/dist/src/messaging/queue-notice-record.js.map +1 -0
- package/dist/src/messaging/send.js +13 -2
- package/dist/src/messaging/send.js.map +1 -1
- package/dist/src/messaging/target-reply-guard.js +66 -0
- package/dist/src/messaging/target-reply-guard.js.map +1 -0
- package/dist/src/orchestrator/collect.js +18 -3
- package/dist/src/orchestrator/collect.js.map +1 -1
- package/dist/src/orchestrator/gateway.js +3 -1
- package/dist/src/orchestrator/gateway.js.map +1 -1
- package/dist/src/orchestrator/pipeline.js +7 -1
- package/dist/src/orchestrator/pipeline.js.map +1 -1
- package/dist/src/orchestrator/session-lanes.js +2 -2
- package/dist/src/orchestrator/session-lanes.js.map +1 -1
- package/dist/src/prompt/builder.js +2 -2
- package/dist/src/prompt/builder.js.map +1 -1
- package/dist/src/routes/i18n.js +9 -4
- package/dist/src/routes/i18n.js.map +1 -1
- package/dist/src/routes/instance.js +8 -4
- package/dist/src/routes/instance.js.map +1 -1
- package/dist/src/routes/jaw-memory.js +5 -5
- package/dist/src/routes/jaw-memory.js.map +1 -1
- package/dist/src/routes/link-preview.js +5 -1
- package/dist/src/routes/link-preview.js.map +1 -1
- package/dist/src/routes/memory.js +9 -7
- package/dist/src/routes/memory.js.map +1 -1
- package/dist/src/routes/messaging.js +21 -10
- package/dist/src/routes/messaging.js.map +1 -1
- package/dist/src/routes/orchestrate.js +127 -118
- package/dist/src/routes/orchestrate.js.map +1 -1
- package/dist/src/routes/settings.js +7 -7
- package/dist/src/routes/settings.js.map +1 -1
- package/dist/src/routes/system.js +9 -4
- package/dist/src/routes/system.js.map +1 -1
- package/dist/src/slack/bot.js +199 -132
- package/dist/src/slack/bot.js.map +1 -1
- package/dist/src/slack/events.js +31 -8
- package/dist/src/slack/events.js.map +1 -1
- package/dist/src/slack/ingress.js +3 -1
- package/dist/src/slack/ingress.js.map +1 -1
- package/dist/src/slack/progress-activity.js +24 -3
- package/dist/src/slack/progress-activity.js.map +1 -1
- package/dist/src/slack/progress-lifecycle.js +29 -0
- package/dist/src/slack/progress-lifecycle.js.map +1 -1
- package/dist/src/slack/progress.js +1 -1
- package/dist/src/slack/progress.js.map +1 -1
- package/dist/src/slack/send-only-client.js +16 -3
- package/dist/src/slack/send-only-client.js.map +1 -1
- package/dist/src/slack/slack-file.js +5 -3
- package/dist/src/slack/slack-file.js.map +1 -1
- package/dist/src/slack/workflow.js +134 -0
- package/dist/src/slack/workflow.js.map +1 -0
- package/dist/src/telegram/bot.js +60 -55
- package/dist/src/telegram/bot.js.map +1 -1
- package/dist/src/telegram/rich-message.js +28 -20
- package/dist/src/telegram/rich-message.js.map +1 -1
- package/dist/src/telegram/telegram-file.js +45 -9
- package/dist/src/telegram/telegram-file.js.map +1 -1
- package/dist/src/trace/activity-journal.js +3 -6
- package/dist/src/trace/activity-journal.js.map +1 -1
- package/dist/src/trace/activity-retention.js +29 -0
- package/dist/src/trace/activity-retention.js.map +1 -1
- package/package.json +1 -1
- package/public/dist/.vite/manifest.json +20 -20
- package/public/dist/assets/{ChannelEnablementControl-DwUDKjX9.js → ChannelEnablementControl-egwTtO9-.js} +8 -8
- package/public/dist/assets/{ChannelSetupEntry-DH0AuNNX.js → ChannelSetupEntry-CCrUYd2-.js} +1 -1
- package/public/dist/assets/{ChannelsDiscord-7NmoTjR7.js → ChannelsDiscord-B6rJAsQG.js} +1 -1
- package/public/dist/assets/{ChannelsSlack-Ba0ax6iT.js → ChannelsSlack-CrSIw4Wd.js} +1 -1
- package/public/dist/assets/{ChannelsTelegram-DKA9atf2.js → ChannelsTelegram-DPrdLEnT.js} +1 -1
- package/public/dist/assets/CodeCanvas-D5jp8-t5.js +2 -0
- package/public/dist/assets/{manager-iX7KosWI.js → manager-CFwOAoe0.js} +2 -2
- package/public/dist/assets/{settings-embedding-DIqST-91.js → settings-embedding-DspkJ6sr.js} +1 -1
- package/public/dist/assets/{settings-KIPNUOIV.js → settings-qkC0NuQ7.js} +1 -1
- package/public/dist/manager/index.html +1 -1
- package/public/dist/settings/index.html +1 -1
- package/public/locales/en.json +9 -3
- package/public/locales/ja.json +9 -3
- package/public/locales/ko.json +9 -3
- package/public/locales/zh.json +9 -3
- package/public/manager/src/code/CodeWorkbench.tsx +11 -4
- package/public/manager/src/code/code-controller-draft-storage.ts +7 -3
- package/public/manager/src/code/code-controller-drafts.ts +9 -2
- package/public/manager/src/code/code-controller-runtime.ts +125 -12
- package/public/manager/src/code/code-controller-types.ts +2 -0
- package/public/manager/src/code/pending-user-item.ts +5 -0
- package/scripts/ci/windows-unit-manifest.txt +6 -0
- package/public/dist/assets/CodeCanvas-DWsx6sv5.js +0 -2
package/dist/src/slack/bot.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { verifiedSlackWorkspace } from './verified-workspace.js';
|
|
2
2
|
import { slackCredentialKey } from './tool-context.js';
|
|
3
3
|
import { isSlackMention, matchesTrustedBotTrigger, readTrustedBotTriggers } from './events.js';
|
|
4
|
+
import { captureSlackWorkflow, prepareSlackWorkflow, renderSlackWorkflow, isWorkflowReplyUnconfirmed, workflowDiagnosticText } from './workflow.js';
|
|
4
5
|
// ─── Slack Bot ───────────────────────────────────────
|
|
5
6
|
// Slack transport implementation for the cli-jaw messaging runtime.
|
|
6
7
|
// Mirrors src/discord/bot.ts structurally: init/shutdown lifecycle, an inbound
|
|
7
8
|
// handler that gates then dispatches into submitMessage/orchestrateAndCollect,
|
|
8
9
|
// and a forwarder for non-Slack-origin agent output.
|
|
9
|
-
import { JAW_HOME, isSettingsPersistenceBlocked, readPersistedSlackAttachPort, saveSettings, settings } from '../core/config.js';
|
|
10
|
+
import { JAW_HOME, SKILLS_DIR, isSettingsPersistenceBlocked, readPersistedSlackAttachPort, saveSettings, settings } from '../core/config.js';
|
|
10
11
|
import { withSessionScope } from '../core/session-context.js';
|
|
11
12
|
import { log } from '../core/logger.js';
|
|
12
13
|
import { t, normalizeLocale } from '../core/i18n.js';
|
|
@@ -43,6 +44,9 @@ import { createSlackProgressRestorer } from './progress-restore.js';
|
|
|
43
44
|
import { createSlackForwarder, relaySlackImages } from './forwarder.js';
|
|
44
45
|
import { nextDeliverySeq, wasSelfDelivered } from '../messaging/turn-delivery.js';
|
|
45
46
|
import { shouldSkipForwarding } from '../messaging/forwarder-origin.js';
|
|
47
|
+
import { requiresNativeBodyDelivery } from '../messaging/native-body.js';
|
|
48
|
+
import { createQueueNoticeRecorder } from '../messaging/queue-notice-record.js';
|
|
49
|
+
import { admitTargetReply } from '../messaging/target-reply-guard.js';
|
|
46
50
|
import { handleSlackSlashCommand } from './commands.js';
|
|
47
51
|
import { logErrorText, redactOutboundText } from '../messaging/redact.js';
|
|
48
52
|
import { downloadAndSaveSlackFiles } from './inbound-file.js';
|
|
@@ -155,34 +159,11 @@ function hasPendingQueueWaiter(requestId) {
|
|
|
155
159
|
return pendingQueueRequestIds.has(requestId);
|
|
156
160
|
}
|
|
157
161
|
// ─── Durable notice records (#418) ──────────────────
|
|
158
|
-
// The handles above are process-local;
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
getQueueNoticeStore()?.reserve({ requestId, channel: 'slack', target });
|
|
165
|
-
}
|
|
166
|
-
catch (e) {
|
|
167
|
-
log.info('[slack:queue-notice] reserve failed', logErrorText(e));
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
function attachSlackNoticeRecord(requestId, ts) {
|
|
171
|
-
try {
|
|
172
|
-
getQueueNoticeStore()?.attachMessageId(requestId, ts);
|
|
173
|
-
}
|
|
174
|
-
catch (e) {
|
|
175
|
-
log.info('[slack:queue-notice] attach failed', logErrorText(e));
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
function closeSlackNoticeRecord(requestId) {
|
|
179
|
-
try {
|
|
180
|
-
getQueueNoticeStore()?.close(requestId);
|
|
181
|
-
}
|
|
182
|
-
catch (e) {
|
|
183
|
-
log.info('[slack:queue-notice] close failed', logErrorText(e));
|
|
184
|
-
}
|
|
185
|
-
}
|
|
162
|
+
// The handles above are process-local; this wraps the store that outlives the
|
|
163
|
+
// process. Best-effort by contract: a durable write is a convenience for the
|
|
164
|
+
// NEXT boot, and letting it throw here would fail the turn the user is actually
|
|
165
|
+
// waiting on. The contract is shared with the other two bots (#699).
|
|
166
|
+
const slackNoticeRecord = createQueueNoticeRecorder('slack', '[slack:queue-notice]');
|
|
186
167
|
/**
|
|
187
168
|
* Close a queue notice as ANSWERED from a path that never held the live handle.
|
|
188
169
|
*
|
|
@@ -206,7 +187,7 @@ async function closeSlackNoticeAsAnsweredByRequestId(requestId, token, signal, g
|
|
|
206
187
|
await createSlackNoticeTransport(token, record.target.targetId, record.messageId).delete(signal);
|
|
207
188
|
}
|
|
208
189
|
if (current())
|
|
209
|
-
|
|
190
|
+
slackNoticeRecord.close(requestId);
|
|
210
191
|
}
|
|
211
192
|
catch (e) {
|
|
212
193
|
log.info('[slack:queue-notice] answered-close failed', logErrorText(e));
|
|
@@ -378,16 +359,14 @@ function buildSlackTarget(event) {
|
|
|
378
359
|
* carried through the queue rather than on whoever spoke most recently.
|
|
379
360
|
*/
|
|
380
361
|
let targetReplyForwarderInstalled = false;
|
|
381
|
-
function requiresNativeBodyDelivery(data) {
|
|
382
|
-
return (data['runtimeFinality'] === 'present' || data['runtimeFinality'] === 'absent')
|
|
383
|
-
&& (data['runtimeStatus'] === 'done' || data['runtimeStatus'] === 'error' || data['runtimeStatus'] === 'stopped');
|
|
384
|
-
}
|
|
385
362
|
function bodyProgressOutcome(data, delivered, latched, stopping) {
|
|
386
363
|
const native = requiresNativeBodyDelivery(data);
|
|
387
364
|
if ((native && data['runtimeStatus'] === 'stopped') || (!native && data['executionInterrupted'] === true) || latched === 'cancelled')
|
|
388
365
|
return 'cancelled';
|
|
389
366
|
if (data['collectionFailure'] === 'timeout')
|
|
390
367
|
return 'expired';
|
|
368
|
+
if (data['workflowUnconfirmed'] === true)
|
|
369
|
+
return 'error';
|
|
391
370
|
if ((native && data['runtimeStatus'] === 'error') || (!native && data['executionFailed'] === true)
|
|
392
371
|
|| data['collectionFailure'] === 'error' || (!native && data['error'] === true) || latched === 'error')
|
|
393
372
|
return 'error';
|
|
@@ -604,6 +583,7 @@ function trackSlackReply(options) {
|
|
|
604
583
|
return sessionLanes.runDetachedTurn(session.scope, async () => {
|
|
605
584
|
let delivered = false;
|
|
606
585
|
let confirmedDelivery = false;
|
|
586
|
+
let outcomeData = data;
|
|
607
587
|
try {
|
|
608
588
|
if (!current()) {
|
|
609
589
|
await Promise.allSettled([display.finish(executionOutcome ?? 'expired'), settleAck('failure')]);
|
|
@@ -611,51 +591,33 @@ function trackSlackReply(options) {
|
|
|
611
591
|
}
|
|
612
592
|
const requireBodyDelivery = requiresNativeBodyDelivery(data);
|
|
613
593
|
const rawText = String(data['text'] ?? '');
|
|
614
|
-
const
|
|
615
|
-
|
|
594
|
+
const unconfirmed = data['workflowUnconfirmed'] === true || Boolean(options.workflow && isWorkflowReplyUnconfirmed(rawText, data));
|
|
595
|
+
if (unconfirmed)
|
|
596
|
+
outcomeData = { ...data, workflowUnconfirmed: true };
|
|
597
|
+
const text = unconfirmed ? workflowDiagnosticText(rawText, t('slack.workflow.unconfirmed', {}, locale))
|
|
598
|
+
: data['error'] === true && !requireBodyDelivery ? t('slack.progress.failure', {}, locale)
|
|
599
|
+
: requireBodyDelivery && !rawText.trim() ? '' : rawText;
|
|
616
600
|
display.phase('delivering');
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
confirmedDelivery = Boolean(alreadyDelivered) || (sent.ok && Boolean(sent.ts));
|
|
632
|
-
}
|
|
633
|
-
finally {
|
|
634
|
-
outbound.done();
|
|
635
|
-
}
|
|
636
|
-
const outcome = bodyProgressOutcome(data, delivered, executionOutcome, !current());
|
|
637
|
-
if (delivered && target.threadId)
|
|
638
|
-
markThreadParticipated(target.targetId, target.threadId);
|
|
639
|
-
await settleAck(delivered && outcome === 'complete' ? 'success' : 'failure');
|
|
640
|
-
await display.finish(outcome, { bodyDelivered: confirmedDelivery });
|
|
641
|
-
if (text && current()) {
|
|
642
|
-
const relay = slackOutboundRegistry.start(signal);
|
|
643
|
-
try {
|
|
644
|
-
await relaySlackImages(token, target, text, { signal: relay.signal });
|
|
645
|
-
}
|
|
646
|
-
catch (error) {
|
|
647
|
-
log.error('[slack:queue-relay]', logErrorText(error));
|
|
648
|
-
}
|
|
649
|
-
finally {
|
|
650
|
-
relay.done();
|
|
651
|
-
}
|
|
652
|
-
}
|
|
601
|
+
// Same rule as the direct path, and now literally the same code:
|
|
602
|
+
// the queued/steer turn differs only in where its text and anchor
|
|
603
|
+
// come from, and in keeping the image relay off the body's
|
|
604
|
+
// cancellation domain (#686).
|
|
605
|
+
await deliverSlackTurnBody({
|
|
606
|
+
token, target, text, data: outcomeData, signal, display, settleAck, current,
|
|
607
|
+
executionOutcome: () => executionOutcome,
|
|
608
|
+
since: anchor ?? observedAnchor,
|
|
609
|
+
skipSendIfEmpty: true,
|
|
610
|
+
relayMode: 'isolated',
|
|
611
|
+
relayWhenTextEmpty: false,
|
|
612
|
+
report: (sentOk, confirmed) => { delivered = sentOk; confirmedDelivery = confirmed; },
|
|
613
|
+
onRelayError: error => log.error('[slack:queue-relay]', logErrorText(error)),
|
|
614
|
+
});
|
|
653
615
|
}
|
|
654
616
|
catch (error) {
|
|
655
617
|
log.error('[slack:queue-send]', logErrorText(error));
|
|
656
618
|
await Promise.allSettled([
|
|
657
619
|
settleAck('failure'),
|
|
658
|
-
display.finish(bodyProgressOutcome(
|
|
620
|
+
display.finish(bodyProgressOutcome(outcomeData, delivered, executionOutcome, !current()), { bodyDelivered: confirmedDelivery }),
|
|
659
621
|
]);
|
|
660
622
|
}
|
|
661
623
|
});
|
|
@@ -666,14 +628,15 @@ function trackSlackReply(options) {
|
|
|
666
628
|
});
|
|
667
629
|
}, true);
|
|
668
630
|
}
|
|
669
|
-
|
|
631
|
+
slackNoticeRecord.reserve(requestId, target);
|
|
670
632
|
display = createSlackProgressLifecycle({
|
|
671
633
|
token, target, requestId, scope: session.scope, sessionId: session.chatSessionId, locale,
|
|
634
|
+
workflowResponse: Boolean(options.workflow),
|
|
672
635
|
...(options.workingDir ? { workingDir: options.workingDir } : {}),
|
|
673
636
|
...(recipientUserId ? { recipientUserId } : {}),
|
|
674
637
|
registerTeardown: registerSlackProgressTeardown,
|
|
675
|
-
onPosted: ts =>
|
|
676
|
-
onTerminalConfirmed: () =>
|
|
638
|
+
onPosted: ts => slackNoticeRecord.attach(requestId, ts),
|
|
639
|
+
onTerminalConfirmed: () => slackNoticeRecord.close(requestId),
|
|
677
640
|
onExecutionOutcome: outcome => { executionOutcome = outcome; },
|
|
678
641
|
onActivity: () => { if (started)
|
|
679
642
|
resetTimer(); },
|
|
@@ -703,8 +666,6 @@ function installSlackTargetReplyForwarder() {
|
|
|
703
666
|
targetReplyForwarderInstalled = true;
|
|
704
667
|
addBroadcastListener((type, data) => {
|
|
705
668
|
observeSlackReplyControl(type, data);
|
|
706
|
-
if (slackStopping || type !== 'orchestrate_done' || data["origin"] !== 'slack')
|
|
707
|
-
return;
|
|
708
669
|
// Only captured queued/restart delivery authority can use this fallback.
|
|
709
670
|
// Ordinary direct turns retain their awaiting dispatch owner.
|
|
710
671
|
//
|
|
@@ -714,24 +675,30 @@ function installSlackTargetReplyForwarder() {
|
|
|
714
675
|
// A steered turn is the same shape of orphan as a queued one: ingress
|
|
715
676
|
// returns early unless the disposition is new_run, so a mid-run steer leaves
|
|
716
677
|
// NO waiter for the follow-up answer (#655). Accept all three identities.
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
678
|
+
//
|
|
679
|
+
// `observeSlackReplyControl` above stays OUTSIDE this admission: it acts on
|
|
680
|
+
// steer/queue control events, not on `orchestrate_done`, and folding it in
|
|
681
|
+
// would silence boot/orphan start proof and steer tracking.
|
|
682
|
+
const admitted = admitTargetReply(type, data, {
|
|
683
|
+
channel: 'slack',
|
|
684
|
+
event: 'orchestrate_done',
|
|
685
|
+
accept: ['fromQueue', 'fromSteer', 'replyViaTarget'],
|
|
686
|
+
hasPendingWaiter: hasPendingQueueWaiter,
|
|
687
|
+
stopping: () => slackStopping,
|
|
688
|
+
});
|
|
689
|
+
if (!admitted)
|
|
726
690
|
return;
|
|
691
|
+
const { target } = admitted;
|
|
727
692
|
const token = getSlackSendClient().token;
|
|
728
693
|
if (!token)
|
|
729
694
|
return;
|
|
730
695
|
const generation = lifecycleGeneration;
|
|
731
|
-
const requireBodyDelivery =
|
|
732
|
-
const text = data['
|
|
733
|
-
? t('slack.
|
|
734
|
-
|
|
696
|
+
const requireBodyDelivery = admitted.requireBodyDelivery;
|
|
697
|
+
const text = data['workflowUnconfirmed'] === true
|
|
698
|
+
? workflowDiagnosticText(String(data['text'] ?? ''), t('slack.workflow.unconfirmed', {}, currentLocale()))
|
|
699
|
+
: data['error'] === true && !requireBodyDelivery
|
|
700
|
+
? t('slack.progress.failure', {}, currentLocale()) : String(data['text'] ?? '');
|
|
701
|
+
const requestId = admitted.requestId;
|
|
735
702
|
const scope = slackReplyDelivery.scope(requestId, target)
|
|
736
703
|
?? (typeof data['scope'] === 'string' ? data['scope'] : 'default');
|
|
737
704
|
void slackReplyDelivery.deliver(requestId, target, anchor => sessionLanes.runDetachedTurn(scope, async () => {
|
|
@@ -804,6 +771,7 @@ async function slackOrchestrate(target, prompt, displayMsg, signal, dedupe = {})
|
|
|
804
771
|
try {
|
|
805
772
|
result = admitSlackRun({
|
|
806
773
|
target, prompt, displayText: displayMsg, chatId,
|
|
774
|
+
...(dedupe.workflow ? { workflow: dedupe.workflow } : {}),
|
|
807
775
|
...(dedupe.toolSource ? { toolSource: dedupe.toolSource } : {}),
|
|
808
776
|
...(dedupe.preResolvedScope !== undefined
|
|
809
777
|
? { preResolvedScope: dedupe.preResolvedScope } : {}),
|
|
@@ -825,14 +793,15 @@ async function slackOrchestrate(target, prompt, displayMsg, signal, dedupe = {})
|
|
|
825
793
|
if (!current())
|
|
826
794
|
return;
|
|
827
795
|
if (ctx.requestId)
|
|
828
|
-
|
|
796
|
+
slackNoticeRecord.reserve(ctx.requestId, target);
|
|
829
797
|
const display = createSlackProgressLifecycle({
|
|
830
798
|
token, target, requestId: ctx.requestId, scope: ctx.scope, sessionId: ctx.chatSessionId, locale,
|
|
799
|
+
workflowResponse: Boolean(dedupe.workflow),
|
|
831
800
|
...(workingDir ? { workingDir } : {}),
|
|
832
801
|
...(dedupe.recipientUserId ? { recipientUserId: dedupe.recipientUserId } : {}),
|
|
833
802
|
registerTeardown: registerSlackProgressTeardown,
|
|
834
|
-
onPosted: ts =>
|
|
835
|
-
onTerminalConfirmed: () =>
|
|
803
|
+
onPosted: ts => slackNoticeRecord.attach(ctx.requestId, ts),
|
|
804
|
+
onTerminalConfirmed: () => slackNoticeRecord.close(ctx.requestId),
|
|
836
805
|
onExecutionOutcome: outcome => { executionOutcome = outcome; },
|
|
837
806
|
onSeal: () => { shutdownSealed = true; void settleAck('failure'); },
|
|
838
807
|
});
|
|
@@ -847,6 +816,7 @@ async function slackOrchestrate(target, prompt, displayMsg, signal, dedupe = {})
|
|
|
847
816
|
try {
|
|
848
817
|
const collected = await withSessionScope({ scope: ctx.scope, chatSessionId: ctx.chatSessionId }, () => orchestrateAndCollectData(prompt, {
|
|
849
818
|
origin: 'slack', target, chatId, requestId: ctx.requestId,
|
|
819
|
+
...(dedupe.workflow ? { slackWorkflow: dedupe.workflow } : {}),
|
|
850
820
|
...(dedupe.toolSource ? { _strictRequestOwnership: true } : {}),
|
|
851
821
|
...(ctx.remoteKey ? { remoteKey: ctx.remoteKey } : {}),
|
|
852
822
|
chatSessionId: ctx.chatSessionId, scope: ctx.scope, _skipInsert: true,
|
|
@@ -862,39 +832,27 @@ async function slackOrchestrate(target, prompt, displayMsg, signal, dedupe = {})
|
|
|
862
832
|
await settleAck('success');
|
|
863
833
|
return;
|
|
864
834
|
}
|
|
865
|
-
const
|
|
866
|
-
|
|
835
|
+
const unconfirmed = resultData['workflowUnconfirmed'] === true || Boolean(dedupe.workflow && isWorkflowReplyUnconfirmed(collected.text, resultData));
|
|
836
|
+
if (unconfirmed)
|
|
837
|
+
resultData = { ...resultData, workflowUnconfirmed: true };
|
|
838
|
+
const text = unconfirmed ? workflowDiagnosticText(collected.text, t('slack.workflow.unconfirmed', {}, locale))
|
|
839
|
+
: collected.data.collectionFailure === 'error' ? t('slack.progress.failure', {}, locale)
|
|
840
|
+
: collected.data.collectionFailure === 'timeout' ? t('tg.timeout', {}, locale) : collected.text;
|
|
867
841
|
display.phase('delivering');
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
},
|
|
883
|
-
});
|
|
884
|
-
bodySucceeded = sendResult.ok;
|
|
885
|
-
bodyConfirmed = alreadyDelivered || (sendResult.ok && Boolean(sendResult.ts));
|
|
886
|
-
const outcome = bodyProgressOutcome(resultData, bodySucceeded, executionOutcome, !current());
|
|
887
|
-
if (bodySucceeded && target.threadId)
|
|
888
|
-
markThreadParticipated(target.targetId, target.threadId);
|
|
889
|
-
await settleAck(bodySucceeded && outcome === 'complete' ? 'success' : 'failure');
|
|
890
|
-
await display.finish(outcome, { bodyDelivered: bodyConfirmed });
|
|
891
|
-
if (current())
|
|
892
|
-
await relaySlackImages(token, target, text, { signal: outbound.signal });
|
|
893
|
-
log.info(`[slack:out${alreadyDelivered ? ':skipped-self-delivered' : ''}] ${target.targetId}: ${redactOutboundText(text).slice(0, 80)}`);
|
|
894
|
-
}
|
|
895
|
-
finally {
|
|
896
|
-
outbound.done();
|
|
897
|
-
}
|
|
842
|
+
await deliverSlackTurnBody({
|
|
843
|
+
token, target, text, data: resultData, signal, display, settleAck, current,
|
|
844
|
+
executionOutcome: () => executionOutcome,
|
|
845
|
+
since: turnStartedAt,
|
|
846
|
+
// The direct path posts even an empty body: sendSlackText owns
|
|
847
|
+
// the empty_message rule, and the collector already replaced a
|
|
848
|
+
// truly empty answer with its no-response placeholder.
|
|
849
|
+
skipSendIfEmpty: false,
|
|
850
|
+
relayMode: 'same-outbound',
|
|
851
|
+
relayWhenTextEmpty: true,
|
|
852
|
+
onAttempt: () => { bodyAttempted = true; },
|
|
853
|
+
report: (delivered, confirmed) => { bodySucceeded = delivered; bodyConfirmed = confirmed; },
|
|
854
|
+
onSent: alreadyDelivered => log.info(`[slack:out${alreadyDelivered ? ':skipped-self-delivered' : ''}] ${target.targetId}: ${redactOutboundText(text).slice(0, 80)}`),
|
|
855
|
+
});
|
|
898
856
|
}
|
|
899
857
|
catch (err) {
|
|
900
858
|
log.error('[slack:error]', logErrorText(err));
|
|
@@ -941,6 +899,7 @@ async function slackOrchestrate(target, prompt, displayMsg, signal, dedupe = {})
|
|
|
941
899
|
return;
|
|
942
900
|
}
|
|
943
901
|
const options = {
|
|
902
|
+
...(dedupe.workflow ? { workflow: dedupe.workflow } : {}),
|
|
944
903
|
token, target, requestId, session: { ...result.sessionContext }, locale: currentLocale(),
|
|
945
904
|
generation: lifecycleGeneration, signal, ack, ...(workingDir ? { workingDir } : {}), initialPhase: 'queued',
|
|
946
905
|
...(dedupe.recipientUserId ? { recipientUserId: dedupe.recipientUserId } : {}),
|
|
@@ -963,6 +922,93 @@ async function slackOrchestrate(target, prompt, displayMsg, signal, dedupe = {})
|
|
|
963
922
|
return;
|
|
964
923
|
}
|
|
965
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* The one place a Slack turn's answer reaches the wire (#686).
|
|
927
|
+
*
|
|
928
|
+
* The direct dispatch path and the queued/steer tracker each carried their own
|
|
929
|
+
* copy of send -> early ACK -> progress finish -> image relay. Every delivery
|
|
930
|
+
* fix therefore had to land twice, and the ones that did not became bugs only
|
|
931
|
+
* the queued path kept (#655, #673).
|
|
932
|
+
*
|
|
933
|
+
* The two paths do genuinely disagree, so each disagreement is a PARAMETER
|
|
934
|
+
* rather than a branch inside the spine: the caller derives its own text from
|
|
935
|
+
* its own payload, brings its own delivery-ledger anchor, decides whether an
|
|
936
|
+
* empty body is posted at all, and chooses whether the image relay shares the
|
|
937
|
+
* body's cancellation domain. Collapsing those would silently change live
|
|
938
|
+
* behaviour, which is why this is an extraction and not a merge.
|
|
939
|
+
*
|
|
940
|
+
* Deliberately NOT folded in: the orphan forwarder. It has no ACK handle and no
|
|
941
|
+
* progress card, because it exists precisely for turns whose waiter is gone.
|
|
942
|
+
*/
|
|
943
|
+
async function deliverSlackTurnBody(options) {
|
|
944
|
+
const { token, target, text, data, signal, display, settleAck, current, executionOutcome } = options;
|
|
945
|
+
const requireBodyDelivery = requiresNativeBodyDelivery(data);
|
|
946
|
+
let bodySucceeded = false;
|
|
947
|
+
let bodyConfirmed = false;
|
|
948
|
+
const report = () => options.report?.(bodySucceeded, bodyConfirmed);
|
|
949
|
+
const outbound = slackOutboundRegistry.start(signal);
|
|
950
|
+
let outboundClosed = false;
|
|
951
|
+
const closeOutbound = () => { if (!outboundClosed) {
|
|
952
|
+
outboundClosed = true;
|
|
953
|
+
outbound.done();
|
|
954
|
+
} };
|
|
955
|
+
options.onAttempt?.();
|
|
956
|
+
try {
|
|
957
|
+
const skipped = options.skipSendIfEmpty && !text;
|
|
958
|
+
const since = options.since;
|
|
959
|
+
const alreadyDelivered = !skipped && since !== undefined
|
|
960
|
+
&& wasSelfDelivered({ target, text, since });
|
|
961
|
+
const sent = skipped ? { ok: false }
|
|
962
|
+
: alreadyDelivered ? { ok: true }
|
|
963
|
+
: await sendSlackText(token, target, text, {
|
|
964
|
+
signal: outbound.signal, ...(requireBodyDelivery ? { requireBodyDelivery: true } : {}),
|
|
965
|
+
onPosted: async () => {
|
|
966
|
+
// Every chunk is posted, so the answer is visible. Settle the
|
|
967
|
+
// ACK here: readback verification can take seconds per chunk
|
|
968
|
+
// and must not hold the reaction on running (#417).
|
|
969
|
+
bodySucceeded = true;
|
|
970
|
+
report();
|
|
971
|
+
const early = bodyProgressOutcome(data, true, executionOutcome(), !current());
|
|
972
|
+
await settleAck(early === 'complete' ? 'success' : 'failure');
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
bodySucceeded = sent.ok;
|
|
976
|
+
bodyConfirmed = alreadyDelivered || (sent.ok && Boolean(sent.ts));
|
|
977
|
+
report();
|
|
978
|
+
// The queued path closes the body registration before settling, so a late
|
|
979
|
+
// image upload cannot keep the turn's outbound slot open.
|
|
980
|
+
if (options.relayMode === 'isolated')
|
|
981
|
+
closeOutbound();
|
|
982
|
+
const outcome = bodyProgressOutcome(data, bodySucceeded, executionOutcome(), !current());
|
|
983
|
+
if (bodySucceeded && target.threadId)
|
|
984
|
+
markThreadParticipated(target.targetId, target.threadId);
|
|
985
|
+
await settleAck(bodySucceeded && outcome === 'complete' ? 'success' : 'failure');
|
|
986
|
+
await display.finish(outcome, { bodyDelivered: bodyConfirmed });
|
|
987
|
+
if (current() && (options.relayWhenTextEmpty || Boolean(text))) {
|
|
988
|
+
if (options.relayMode === 'isolated') {
|
|
989
|
+
const relay = slackOutboundRegistry.start(signal);
|
|
990
|
+
try {
|
|
991
|
+
await relaySlackImages(token, target, text, { signal: relay.signal });
|
|
992
|
+
}
|
|
993
|
+
catch (error) {
|
|
994
|
+
options.onRelayError?.(error);
|
|
995
|
+
}
|
|
996
|
+
finally {
|
|
997
|
+
relay.done();
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
await relaySlackImages(token, target, text, { signal: outbound.signal });
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
if (!skipped)
|
|
1005
|
+
options.onSent?.(alreadyDelivered);
|
|
1006
|
+
return { delivered: bodySucceeded, confirmed: bodyConfirmed, outcome };
|
|
1007
|
+
}
|
|
1008
|
+
finally {
|
|
1009
|
+
closeOutbound();
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
966
1012
|
function buildSlackFileFailureWarning(failed, allFailed = false) {
|
|
967
1013
|
if (!failed.length)
|
|
968
1014
|
return null;
|
|
@@ -993,6 +1039,19 @@ async function runSlackMessageEvent(event, target, text, signal, opts, commitPre
|
|
|
993
1039
|
const files = event.files || [];
|
|
994
1040
|
let prompt = text;
|
|
995
1041
|
let displayText = text;
|
|
1042
|
+
const workflow = await prepareSlackWorkflow(event, gateConfig(), SKILLS_DIR, opts.workflowSelection);
|
|
1043
|
+
if (signal.aborted)
|
|
1044
|
+
return;
|
|
1045
|
+
if (workflow.kind === 'blocked') {
|
|
1046
|
+
log.warn('[slack:workflow] blocked', { code: workflow.code, channelId: target.targetId });
|
|
1047
|
+
const token = getSlackSendClient().token;
|
|
1048
|
+
if (token) {
|
|
1049
|
+
const sent = await sendSlackText(token, target, t('slack.workflow.unavailable', {}, currentLocale()));
|
|
1050
|
+
if (sent.ok && opts.eventKey)
|
|
1051
|
+
commitSlackEvent(opts.eventKey);
|
|
1052
|
+
}
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
996
1055
|
// Start identity resolution alongside the downloads. Running them in series
|
|
997
1056
|
// would add a round trip to every attachment message.
|
|
998
1057
|
const identityPromise = resolveSenderIdentity(event, { signal });
|
|
@@ -1028,7 +1087,7 @@ async function runSlackMessageEvent(event, target, text, signal, opts, commitPre
|
|
|
1028
1087
|
// (gateway.ts:234). A sender line in front of "계속" stops it being a
|
|
1029
1088
|
// continuation, so control text travels undecorated. Reset is already
|
|
1030
1089
|
// intercepted upstream and never reaches here.
|
|
1031
|
-
if (!isContinueIntent(prompt)) {
|
|
1090
|
+
if (workflow.kind === 'ready' || !isContinueIntent(prompt)) {
|
|
1032
1091
|
const block = await buildInboundContextBlock(event, identity, signal, opts, commitPrefetch);
|
|
1033
1092
|
// An empty block lands EXACTLY on the previous behavior: config off and
|
|
1034
1093
|
// total lookup failure must be indistinguishable from before this
|
|
@@ -1040,6 +1099,10 @@ async function runSlackMessageEvent(event, target, text, signal, opts, commitPre
|
|
|
1040
1099
|
// the message, and the conversation is already obvious in Slack's own UI.
|
|
1041
1100
|
displayText = buildSenderDisplay(identity, displayText);
|
|
1042
1101
|
}
|
|
1102
|
+
if (workflow.kind === 'ready') {
|
|
1103
|
+
prompt = renderSlackWorkflow(workflow, prompt);
|
|
1104
|
+
log.info('[slack:workflow] routed', workflow.metadata);
|
|
1105
|
+
}
|
|
1043
1106
|
const sourceToken = getSlackSendClient().token;
|
|
1044
1107
|
const workspace = sourceToken && opts.socketTeamId ? await verifiedSlackWorkspace(sourceToken).catch(() => null) : null;
|
|
1045
1108
|
if (signal.aborted)
|
|
@@ -1048,6 +1111,7 @@ async function runSlackMessageEvent(event, target, text, signal, opts, commitPre
|
|
|
1048
1111
|
? { teamId: workspace.teamId, actorId: event.user, destination: target, credentialKey: slackCredentialKey(sourceToken),
|
|
1049
1112
|
...(typeof event.action_token === 'string' ? { actionToken: event.action_token } : {}) } : undefined;
|
|
1050
1113
|
await slackOrchestrate(target, prompt, displayText, signal, {
|
|
1114
|
+
...(workflow.kind === 'ready' ? { workflow: workflow.metadata } : {}),
|
|
1051
1115
|
...(toolSource ? { toolSource } : {}),
|
|
1052
1116
|
...(opts.eventKey ? { eventKey: opts.eventKey } : {}),
|
|
1053
1117
|
...(opts.reservationGeneration !== undefined
|
|
@@ -1272,14 +1336,16 @@ export async function handleSlackEnvelope(envelope, approvalTransport = slackApp
|
|
|
1272
1336
|
const event = payload?.event;
|
|
1273
1337
|
if (!event)
|
|
1274
1338
|
return;
|
|
1275
|
-
const
|
|
1339
|
+
const receiveGate = gateConfig();
|
|
1340
|
+
const workflowSelection = captureSlackWorkflow(event, receiveGate);
|
|
1341
|
+
const approval = workflowSelection.kind === 'none' ? handleApprovalCommand(approvalTransport, {
|
|
1276
1342
|
...event,
|
|
1277
1343
|
__jawSelf: Boolean(event.user && event.user === getSlackSelfUserId()),
|
|
1278
|
-
}, String(event.text || ''));
|
|
1344
|
+
}, String(event.text || '')) : { handled: false };
|
|
1279
1345
|
if (approval.handled)
|
|
1280
1346
|
return;
|
|
1281
1347
|
const target = buildSlackTarget(event);
|
|
1282
|
-
const decision = shouldProcessSlackEvent(event,
|
|
1348
|
+
const decision = shouldProcessSlackEvent(event, receiveGate, envelope.type);
|
|
1283
1349
|
if (!decision.process) {
|
|
1284
1350
|
log.info(`[slack:in] skipped (${decision.reason})`);
|
|
1285
1351
|
return;
|
|
@@ -1362,7 +1428,7 @@ export async function handleSlackEnvelope(envelope, approvalTransport = slackApp
|
|
|
1362
1428
|
return;
|
|
1363
1429
|
if (text)
|
|
1364
1430
|
log.info(`[slack:in] ${event.channel}: ${redactOutboundText(text).slice(0, 80)}`);
|
|
1365
|
-
if (!hasFiles && isResetIntent(text)) {
|
|
1431
|
+
if (!hasFiles && workflowSelection.kind === 'none' && isResetIntent(text)) {
|
|
1366
1432
|
const client = getSlackSendClient();
|
|
1367
1433
|
const result = submitMessage(text, { origin: 'slack', target });
|
|
1368
1434
|
if (client.token) {
|
|
@@ -1373,6 +1439,7 @@ export async function handleSlackEnvelope(envelope, approvalTransport = slackApp
|
|
|
1373
1439
|
return;
|
|
1374
1440
|
}
|
|
1375
1441
|
prefetchHandedOff = enqueueSlackIngress(slackIngressLaneKey(target), signal => processSlackMessageEvent(event, target, text, signal, {
|
|
1442
|
+
workflowSelection,
|
|
1376
1443
|
...(typeof envelope.payload?.['team_id'] === 'string' ? { socketTeamId: envelope.payload['team_id'] } : {}),
|
|
1377
1444
|
prefetchToken,
|
|
1378
1445
|
...(prefetchOwner ? { prefetchOwner } : {}),
|
|
@@ -1496,7 +1563,7 @@ async function runSlackInit(ctx) {
|
|
|
1496
1563
|
if (declaredTriggers > 0) {
|
|
1497
1564
|
const usable = readTrustedBotTriggers(sc.trustedBotTriggers).length;
|
|
1498
1565
|
if (usable === 0) {
|
|
1499
|
-
log.warn(`[slack:triggers] ${declaredTriggers} trusted bot trigger(s) configured but the list is invalid, so none of them can start a turn — every rule needs
|
|
1566
|
+
log.warn(`[slack:triggers] ${declaredTriggers} trusted bot trigger(s) configured but the list is invalid, so none of them can start a turn — every rule needs channelId (C/G), botId (B), userId (U/W), an uppercase textMarker and, optionally, a valid workflowSkill ID`);
|
|
1500
1567
|
}
|
|
1501
1568
|
else {
|
|
1502
1569
|
log.info(`[slack:triggers] ${usable} trusted bot trigger(s) active`);
|