switchroom 0.15.44 → 0.16.4
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/dist/agent-scheduler/index.js +122 -88
- package/dist/auth-broker/index.js +463 -177
- package/dist/cli/autoaccept-poll.js +4842 -35
- package/dist/cli/drive-write-pretool.mjs +17 -14
- package/dist/cli/notion-write-pretool.mjs +117 -86
- package/dist/cli/self-improve-apply-guard-pretool.mjs +626 -0
- package/dist/cli/self-improve-stop.mjs +428 -0
- package/dist/cli/skill-validate-pretool.mjs +72 -72
- package/dist/cli/switchroom.js +3249 -1241
- package/dist/cli/ui/index.html +1 -1
- package/dist/host-control/main.js +2833 -355
- package/dist/vault/approvals/kernel-server.js +7482 -7439
- package/dist/vault/broker/server.js +11315 -11272
- package/examples/minimal.yaml +1 -0
- package/examples/switchroom.yaml +1 -0
- package/package.json +3 -3
- package/profiles/_base/start.sh.hbs +88 -1
- package/profiles/_shared/execution-discipline.md.hbs +18 -0
- package/profiles/default/CLAUDE.md.hbs +3 -22
- package/telegram-plugin/.claude-plugin/plugin.json +2 -2
- package/telegram-plugin/answer-stream-flag.ts +12 -49
- package/telegram-plugin/answer-stream.ts +5 -150
- package/telegram-plugin/auth-snapshot-format.ts +280 -48
- package/telegram-plugin/auto-fallback-fleet.ts +44 -1
- package/telegram-plugin/context-exhaustion.ts +12 -0
- package/telegram-plugin/demo-mask.ts +154 -0
- package/telegram-plugin/dist/bridge/bridge.js +167 -124
- package/telegram-plugin/dist/gateway/gateway.js +3039 -1159
- package/telegram-plugin/dist/server.js +215 -172
- package/telegram-plugin/docs/waiting-ux-spec.md +2 -2
- package/telegram-plugin/draft-stream.ts +47 -410
- package/telegram-plugin/final-answer-detect.ts +17 -12
- package/telegram-plugin/fleet-fallback-resume.ts +131 -0
- package/telegram-plugin/format.ts +56 -19
- package/telegram-plugin/gateway/auth-add-flow.ts +332 -127
- package/telegram-plugin/gateway/auth-broker-client.ts +2 -2
- package/telegram-plugin/gateway/auth-command.ts +70 -14
- package/telegram-plugin/gateway/clean-shutdown-marker.ts +44 -0
- package/telegram-plugin/gateway/config-approval-handler.test.ts +91 -4
- package/telegram-plugin/gateway/config-approval-handler.ts +94 -13
- package/telegram-plugin/gateway/current-turn-map.ts +188 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +3 -1
- package/telegram-plugin/gateway/effort-command.ts +8 -3
- package/telegram-plugin/gateway/emission-authority.ts +369 -0
- package/telegram-plugin/gateway/feed-open-gate.ts +292 -0
- package/telegram-plugin/gateway/gateway.ts +1837 -291
- package/telegram-plugin/gateway/inject-handler.test.ts +2 -1
- package/telegram-plugin/gateway/ms365-write-approval.test.ts +4 -4
- package/telegram-plugin/gateway/represent-guard.ts +72 -0
- package/telegram-plugin/gateway/status-surface-log.test.ts +5 -4
- package/telegram-plugin/gateway/status-surface-log.ts +14 -3
- package/telegram-plugin/history.ts +33 -11
- package/telegram-plugin/hooks/repo-context-pretool.mjs +26 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +5 -0
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +8 -0
- package/telegram-plugin/hooks/tool-label-pretool.mjs +39 -15
- package/telegram-plugin/issues-card.ts +4 -0
- package/telegram-plugin/model-unavailable.ts +124 -0
- package/telegram-plugin/narrative-dedup.ts +69 -0
- package/telegram-plugin/over-ping-safety-net.ts +70 -4
- package/telegram-plugin/package.json +3 -3
- package/telegram-plugin/pending-work-progress.ts +12 -0
- package/telegram-plugin/permission-rule.ts +32 -5
- package/telegram-plugin/permission-title.ts +152 -9
- package/telegram-plugin/quota-check.ts +13 -0
- package/telegram-plugin/quota-watch.ts +135 -7
- package/telegram-plugin/registry/turns-schema.test.ts +24 -0
- package/telegram-plugin/registry/turns-schema.ts +9 -0
- package/telegram-plugin/runtime-metrics.ts +13 -0
- package/telegram-plugin/session-tail.ts +96 -11
- package/telegram-plugin/silence-poke.ts +170 -24
- package/telegram-plugin/slot-banner-driver.ts +3 -0
- package/telegram-plugin/status-no-truncate.ts +44 -0
- package/telegram-plugin/status-reactions.ts +20 -3
- package/telegram-plugin/stream-controller.ts +4 -23
- package/telegram-plugin/stream-reply-handler.ts +6 -24
- package/telegram-plugin/streaming-metrics.ts +91 -0
- package/telegram-plugin/subagent-watcher.ts +212 -66
- package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +47 -0
- package/telegram-plugin/tests/answer-stream-dedup.test.ts +9 -26
- package/telegram-plugin/tests/answer-stream-flag.test.ts +25 -58
- package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +41 -51
- package/telegram-plugin/tests/answer-stream.test.ts +2 -411
- package/telegram-plugin/tests/auth-add-flow.test.ts +488 -253
- package/telegram-plugin/tests/auth-command-format2.test.ts +71 -1
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +376 -6
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +120 -0
- package/telegram-plugin/tests/cross-turn-card-gate.test.ts +424 -0
- package/telegram-plugin/tests/demo-mask.test.ts +127 -0
- package/telegram-plugin/tests/draft-stream.test.ts +0 -827
- package/telegram-plugin/tests/emission-authority-card-drain-gate.test.ts +236 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +488 -0
- package/telegram-plugin/tests/emission-authority-open-gate.test.ts +179 -0
- package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +395 -0
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +177 -0
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +146 -0
- package/telegram-plugin/tests/feed-open-gate.test.ts +259 -0
- package/telegram-plugin/tests/feed-survival.test.ts +526 -0
- package/telegram-plugin/tests/fleet-fallback-resume.test.ts +197 -0
- package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +117 -0
- package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +4 -11
- package/telegram-plugin/tests/history.test.ts +60 -0
- package/telegram-plugin/tests/model-unavailable.test.ts +118 -0
- package/telegram-plugin/tests/narrative-dedup.test.ts +118 -0
- package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +285 -0
- package/telegram-plugin/tests/over-ping-final-answer-decoupling.test.ts +194 -0
- package/telegram-plugin/tests/over-ping-safety-net.test.ts +2 -2
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +373 -0
- package/telegram-plugin/tests/permission-card-origin-kill-switch.test.ts +42 -0
- package/telegram-plugin/tests/permission-rule.test.ts +17 -0
- package/telegram-plugin/tests/permission-title.test.ts +206 -17
- package/telegram-plugin/tests/quota-watch.test.ts +252 -9
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -1
- package/telegram-plugin/tests/repo-context-pretool.test.ts +62 -0
- package/telegram-plugin/tests/represent-guard.test.ts +162 -0
- package/telegram-plugin/tests/session-tail.test.ts +147 -3
- package/telegram-plugin/tests/silence-liveness-wiring.test.ts +18 -0
- package/telegram-plugin/tests/status-card-budget-parity.test.ts +72 -0
- package/telegram-plugin/tests/status-surface-log.test.ts +146 -0
- package/telegram-plugin/tests/subagent-watcher-clip-narrative.test.ts +58 -0
- package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +102 -0
- package/telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts +225 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +147 -0
- package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +597 -0
- package/telegram-plugin/tests/telegram-format.test.ts +101 -6
- package/telegram-plugin/tests/tool-activity-summary.test.ts +550 -15
- package/telegram-plugin/tests/tool-label-pretool.test.ts +73 -0
- package/telegram-plugin/tests/tool-label-sidecar.test.ts +44 -0
- package/telegram-plugin/tests/tool-labels.test.ts +67 -0
- package/telegram-plugin/tests/turn-liveness-floor.test.ts +196 -0
- package/telegram-plugin/tests/turn-liveness-invariant.test.ts +340 -0
- package/telegram-plugin/tests/welcome-text.test.ts +32 -3
- package/telegram-plugin/tests/worker-activity-feed.test.ts +470 -22
- package/telegram-plugin/tool-activity-summary.ts +375 -58
- package/telegram-plugin/turn-liveness-floor.ts +240 -0
- package/telegram-plugin/uat/assertions.ts +115 -0
- package/telegram-plugin/uat/driver.ts +68 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +119 -133
- package/telegram-plugin/uat/scenarios/jtbd-answer-pings.test.ts +94 -0
- package/telegram-plugin/uat/scenarios/jtbd-cross-turn-card-dm.test.ts +109 -0
- package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-thinkgap-dm.test.ts +478 -0
- package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-visibility-dm.test.ts +396 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-feed-open-dm.test.ts +202 -0
- package/telegram-plugin/uat/scenarios/jtbd-reply-is-last-dm.test.ts +202 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +93 -87
- package/telegram-plugin/welcome-text.ts +13 -1
- package/telegram-plugin/worker-activity-feed.ts +157 -82
- package/telegram-plugin/draft-transport.ts +0 -122
- package/telegram-plugin/tests/draft-retirement-wiring.test.ts +0 -82
- package/telegram-plugin/tests/draft-transport.test.ts +0 -211
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* entire server.ts top-level initialization.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { createDraftStream, type DraftStreamHandle
|
|
19
|
+
import { createDraftStream, type DraftStreamHandle } from './draft-stream.js'
|
|
20
20
|
import { htmlToPlainText } from './html-sanitize.js'
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -152,30 +152,15 @@ export interface StreamControllerConfig {
|
|
|
152
152
|
*/
|
|
153
153
|
log?: (msg: string) => void
|
|
154
154
|
/**
|
|
155
|
-
* Optional warning logger. Used for
|
|
155
|
+
* Optional warning logger. Used for fallback notices.
|
|
156
156
|
*/
|
|
157
157
|
warn?: (msg: string) => void
|
|
158
|
-
/**
|
|
159
|
-
* Transport selector passed to createDraftStream.
|
|
160
|
-
* - "auto" (default): use draft transport for DMs only
|
|
161
|
-
* - "draft": always prefer draft (if sendMessageDraft is available)
|
|
162
|
-
* - "message": always use sendMessage/editMessageText
|
|
163
|
-
*
|
|
164
|
-
* The gateway forces "message" for forum topics (threads), since
|
|
165
|
-
* sendMessageDraft does not support threaded chats.
|
|
166
|
-
*/
|
|
167
|
-
previewTransport?: 'auto' | 'message' | 'draft'
|
|
168
158
|
/**
|
|
169
159
|
* True when the chat is a private DM. Passed to createDraftStream so
|
|
170
|
-
*
|
|
160
|
+
* the throttle default (400 ms for DMs vs 1000 ms for groups) is applied
|
|
161
|
+
* correctly when no explicit throttleMs is set.
|
|
171
162
|
*/
|
|
172
163
|
isPrivateChat?: boolean
|
|
173
|
-
/**
|
|
174
|
-
* sendMessageDraft callback. When provided (and transport allows it),
|
|
175
|
-
* intermediate stream updates use the draft API. On finalize(), a real
|
|
176
|
-
* sendMessage is posted for push notification and the draft is cleared.
|
|
177
|
-
*/
|
|
178
|
-
sendMessageDraft?: StreamDraftFn
|
|
179
164
|
/**
|
|
180
165
|
* If set, the controller is initialized as if a previous send had
|
|
181
166
|
* landed with this `message_id`. The first `update()` invokes
|
|
@@ -214,9 +199,7 @@ export function createStreamController(cfg: StreamControllerConfig): DraftStream
|
|
|
214
199
|
quoteText,
|
|
215
200
|
protectContent,
|
|
216
201
|
replyMarkup,
|
|
217
|
-
previewTransport,
|
|
218
202
|
isPrivateChat,
|
|
219
|
-
sendMessageDraft,
|
|
220
203
|
initialMessageId,
|
|
221
204
|
} = cfg
|
|
222
205
|
|
|
@@ -314,9 +297,7 @@ export function createStreamController(cfg: StreamControllerConfig): DraftStream
|
|
|
314
297
|
...(idleMs != null ? { idleMs } : {}),
|
|
315
298
|
...(log != null ? { log } : {}),
|
|
316
299
|
...(warn != null ? { warn } : {}),
|
|
317
|
-
...(previewTransport != null ? { previewTransport } : {}),
|
|
318
300
|
...(isPrivateChat != null ? { isPrivateChat } : {}),
|
|
319
|
-
...(sendMessageDraft != null ? { sendMessageDraft } : {}),
|
|
320
301
|
...(initialMessageId != null ? { initialMessageId } : {}),
|
|
321
302
|
chatId,
|
|
322
303
|
},
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* wraps into an MCP content response.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import type { DraftStreamHandle
|
|
19
|
+
import type { DraftStreamHandle } from './draft-stream.js'
|
|
20
20
|
import {
|
|
21
21
|
createStreamController,
|
|
22
22
|
type StreamBotApi,
|
|
@@ -240,13 +240,6 @@ export interface StreamReplyDeps {
|
|
|
240
240
|
/** Error-path stderr. */
|
|
241
241
|
writeError: (line: string) => void
|
|
242
242
|
throttleMs?: number
|
|
243
|
-
/**
|
|
244
|
-
* sendMessageDraft callback. When provided, stream_reply uses the draft
|
|
245
|
-
* API for intermediate updates (DM transport). On done=true, a real
|
|
246
|
-
* sendMessage fires for push notification, then the draft is cleared.
|
|
247
|
-
* Optional — omit to keep the existing sendMessage/editMessageText path.
|
|
248
|
-
*/
|
|
249
|
-
sendMessageDraft?: StreamDraftFn
|
|
250
243
|
/**
|
|
251
244
|
* Idempotency hook for the duplicate-message class (issue #626).
|
|
252
245
|
*
|
|
@@ -275,12 +268,12 @@ export interface StreamReplyDeps {
|
|
|
275
268
|
}) => number | null | undefined
|
|
276
269
|
/**
|
|
277
270
|
* True when the current chat is a private DM. Passed to the stream
|
|
278
|
-
* controller so
|
|
271
|
+
* controller so the DM throttle default (400 ms) is applied instead of
|
|
272
|
+
* the group default (1000 ms) when no explicit throttleMs is set.
|
|
279
273
|
*/
|
|
280
274
|
isPrivateChat?: boolean
|
|
281
275
|
/**
|
|
282
|
-
* True when the current chat is a forum topic.
|
|
283
|
-
* support sendMessageDraft — this forces message transport.
|
|
276
|
+
* True when the current chat is a forum topic.
|
|
284
277
|
*/
|
|
285
278
|
isForumTopic?: boolean
|
|
286
279
|
/**
|
|
@@ -464,14 +457,6 @@ export async function handleStreamReply(
|
|
|
464
457
|
}
|
|
465
458
|
}
|
|
466
459
|
|
|
467
|
-
// Resolve draft-transport options. Forum topics force message transport
|
|
468
|
-
// because sendMessageDraft does not support threads.
|
|
469
|
-
const isForumTopic = deps.isForumTopic === true
|
|
470
|
-
const resolvedTransport: 'auto' | 'message' | 'draft' =
|
|
471
|
-
isForumTopic || deps.sendMessageDraft == null
|
|
472
|
-
? 'message'
|
|
473
|
-
: 'auto'
|
|
474
|
-
|
|
475
460
|
// Idempotency hook (#626): if an external authority (e.g. the
|
|
476
461
|
// gateway's pin manager) already knows the anchor message id for
|
|
477
462
|
// this lane+turn, initialize the stream with it so the next update
|
|
@@ -504,8 +489,8 @@ export async function handleStreamReply(
|
|
|
504
489
|
threadId,
|
|
505
490
|
parseMode,
|
|
506
491
|
disableLinkPreview: deps.disableLinkPreview,
|
|
507
|
-
//
|
|
508
|
-
//
|
|
492
|
+
// Pass undefined when caller didn't override, so draft-stream's
|
|
493
|
+
// DM/group throttle defaults apply (400 ms DMs, 1000 ms groups).
|
|
509
494
|
...(deps.throttleMs != null ? { throttleMs: deps.throttleMs } : {}),
|
|
510
495
|
retry: deps.retry,
|
|
511
496
|
...(replyToMessageId != null ? { replyToMessageId } : {}),
|
|
@@ -513,9 +498,7 @@ export async function handleStreamReply(
|
|
|
513
498
|
...(args.protect_content === true ? { protectContent: true } : {}),
|
|
514
499
|
...(args.disable_notification === true ? { disableNotification: true } : {}),
|
|
515
500
|
...(args.reply_markup != null ? { replyMarkup: args.reply_markup } : {}),
|
|
516
|
-
previewTransport: resolvedTransport,
|
|
517
501
|
isPrivateChat: deps.isPrivateChat === true,
|
|
518
|
-
...(deps.sendMessageDraft != null ? { sendMessageDraft: deps.sendMessageDraft } : {}),
|
|
519
502
|
...(initialMessageId != null ? { initialMessageId } : {}),
|
|
520
503
|
onSend: (messageId, charCount) =>
|
|
521
504
|
deps.logStreamingEvent({ kind: 'draft_send', chatId: chat_id, messageId, charCount }),
|
|
@@ -539,7 +522,6 @@ export async function handleStreamReply(
|
|
|
539
522
|
|| msg.startsWith('stream → edited')
|
|
540
523
|
|| msg.startsWith('stream → not modified')
|
|
541
524
|
|| msg.startsWith('stream finalized')
|
|
542
|
-
|| msg.startsWith('stream → draft')
|
|
543
525
|
|| msg.startsWith('stream → materialized')
|
|
544
526
|
) return
|
|
545
527
|
deps.writeError(`telegram channel: stream_reply ${msg}\n`)
|
|
@@ -93,6 +93,97 @@ export type StreamingEvent =
|
|
|
93
93
|
chatId: string
|
|
94
94
|
messageId: number | undefined
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Emitted when maybeEarlyAckReaction fires the 👀 pre-coalesce reaction
|
|
98
|
+
* for a private-chat inbound. Lets operators see how often the fast-ack
|
|
99
|
+
* path triggers vs. the regular StatusReactionController path (#553 F2).
|
|
100
|
+
*/
|
|
101
|
+
| {
|
|
102
|
+
kind: 'early_ack_reaction'
|
|
103
|
+
chatId: string
|
|
104
|
+
messageId: number
|
|
105
|
+
emoji: string
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Emitted when a fresh StatusReactionController is installed for a new turn
|
|
109
|
+
* (group / non-DM path where the controller manages the whole reaction lifecycle).
|
|
110
|
+
*/
|
|
111
|
+
| {
|
|
112
|
+
kind: 'status_reaction_install'
|
|
113
|
+
chatId: string
|
|
114
|
+
turnId: string
|
|
115
|
+
messageId: number
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Emitted on every emoji transition inside a StatusReactionController.
|
|
119
|
+
* Lets operators trace the full queued→thinking→tool→done lifecycle and
|
|
120
|
+
* see how many state changes occur in a silent turn.
|
|
121
|
+
*/
|
|
122
|
+
| {
|
|
123
|
+
kind: 'status_reaction_transition'
|
|
124
|
+
chatId: string
|
|
125
|
+
turnId: string
|
|
126
|
+
emoji: string
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Emitted when StatusReactionController.finalize() / setDone() runs
|
|
130
|
+
* (controller disposed at turn_end or disconnect-flush). Terminal event.
|
|
131
|
+
*/
|
|
132
|
+
| {
|
|
133
|
+
kind: 'status_reaction_dispose'
|
|
134
|
+
chatId: string
|
|
135
|
+
turnId: string
|
|
136
|
+
reason: 'done' | 'error' | 'disconnect' | 'undelivered'
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Emitted when the FIRST text reply (reply or stream_reply) of a turn is
|
|
140
|
+
* sent to the user. `timeToFirstTextReplyMs` is the wall-clock delta from
|
|
141
|
+
* the inbound-received timestamp to the moment this reply tool fires.
|
|
142
|
+
* Issue #2527 instrumentation: reveals when a turn is reaction-only.
|
|
143
|
+
*/
|
|
144
|
+
| {
|
|
145
|
+
kind: 'turn_reply_timing'
|
|
146
|
+
chatId: string
|
|
147
|
+
threadId: number | undefined
|
|
148
|
+
turnId: string
|
|
149
|
+
timeToFirstTextReplyMs: number
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Emitted at turn_end when the turn produced ZERO text replies (only
|
|
153
|
+
* reaction-emoji transitions). This is the primary observable for the
|
|
154
|
+
* #2527 failure mode — the user sees only an emoji and the turn is done.
|
|
155
|
+
*/
|
|
156
|
+
| {
|
|
157
|
+
kind: 'turn_no_reply_warn'
|
|
158
|
+
chatId: string
|
|
159
|
+
threadId: number | undefined
|
|
160
|
+
turnId: string
|
|
161
|
+
turnDurationMs: number
|
|
162
|
+
reactionCount: number
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Emitted when the silence-poke framework-fallback fires and sends its
|
|
166
|
+
* "still working…" ping. Records the silence duration so operators can
|
|
167
|
+
* correlate with reaction-only turns.
|
|
168
|
+
*/
|
|
169
|
+
| {
|
|
170
|
+
kind: 'silence_poke_fire'
|
|
171
|
+
chatId: string
|
|
172
|
+
threadId: number | undefined
|
|
173
|
+
silenceMs: number
|
|
174
|
+
fallbackKind: string
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Emitted when the silence-poke handler short-circuits because the turn
|
|
178
|
+
* already ended cleanly during the silence window (the late-fire race).
|
|
179
|
+
*/
|
|
180
|
+
| {
|
|
181
|
+
kind: 'silence_poke_skip'
|
|
182
|
+
chatId: string
|
|
183
|
+
threadId: number | undefined
|
|
184
|
+
silenceMs: number
|
|
185
|
+
skipReason: string
|
|
186
|
+
}
|
|
96
187
|
|
|
97
188
|
/**
|
|
98
189
|
* True iff the env gate is on. Re-read on every call so tests can toggle
|
|
@@ -42,7 +42,8 @@ import { basename, join } from 'path'
|
|
|
42
42
|
import { homedir } from 'os'
|
|
43
43
|
import { projectSubagentLine, sanitizeCwdToProjectName, detectErrorInTranscriptLine } from './session-tail.js'
|
|
44
44
|
import { sanitiseToolArg } from './fleet-state.js'
|
|
45
|
-
import { describeToolUse } from './tool-activity-summary.js'
|
|
45
|
+
import { clipNarrative, describeToolUse } from './tool-activity-summary.js'
|
|
46
|
+
import { REPLY_TOOLS, isDraftOfReply } from './narrative-dedup.js'
|
|
46
47
|
import { escapeHtml, truncate } from './card-format.js'
|
|
47
48
|
import { bumpSubagentActivity, recordSubagentStall, recordSubagentResume, recordSubagentEnd, reapStuckRunningRows, countRunningBackgroundSubagents } from './registry/subagents-schema.js'
|
|
48
49
|
import { touchTurnActiveMarker } from './gateway/turn-active-marker.js'
|
|
@@ -158,6 +159,27 @@ export interface WorkerEntry {
|
|
|
158
159
|
* failed handback's "what it reported before failing" slot when the
|
|
159
160
|
* worker left no narrative result of its own. */
|
|
160
161
|
errorDetail?: string
|
|
162
|
+
/**
|
|
163
|
+
* Narrative-dedup gate state (JSONL-text-narrative primitive). A
|
|
164
|
+
* `sub_agent_text` block is held here for ONE lookahead step so the next
|
|
165
|
+
* `sub_agent_tool_use` / `sub_agent_turn_end` can decide draft-then-send
|
|
166
|
+
* (SUPPRESS — it duplicates the worker's reply) vs working-narration (SHOW
|
|
167
|
+
* — fire `onProgress({latestSummary})`). Null when nothing is pending. The
|
|
168
|
+
* pure decision lives in narrative-dedup.ts; this slot is the per-entry
|
|
169
|
+
* cursor. Mirrors the gateway's `turn.pendingNarrative`.
|
|
170
|
+
*/
|
|
171
|
+
pendingNarrative?: { text: string } | null
|
|
172
|
+
/**
|
|
173
|
+
* NIT 3 (sub-agent turn_end symmetry). Most-recently-seen
|
|
174
|
+
* reply/stream_reply `input.text` for this sub-agent — the actual answer a
|
|
175
|
+
* FOREGROUND sub-agent delivered. `sub_agent_turn_end` resolves a trailing
|
|
176
|
+
* `sub_agent_text` block against THIS so a draft of the just-delivered
|
|
177
|
+
* answer is suppressed the same way main-agent step 3 does (conservative
|
|
178
|
+
* dedup). Undefined for background workers that never call a reply tool —
|
|
179
|
+
* their trailing narration still SHOWs, unchanged. Mirrors the gateway's
|
|
180
|
+
* `turn.lastReplyText`.
|
|
181
|
+
*/
|
|
182
|
+
lastReplyText?: string
|
|
161
183
|
}
|
|
162
184
|
|
|
163
185
|
export interface SubagentWatcherConfig {
|
|
@@ -503,14 +525,20 @@ interface FsLike {
|
|
|
503
525
|
* Backfill `jsonl_agent_id` for a sub-agent row that was inserted by the
|
|
504
526
|
* PreToolUse hook (keyed on tool_use_id) but didn't yet know the JSONL stem.
|
|
505
527
|
*
|
|
506
|
-
* Strategy: read the `agent-<id>.meta.json` sibling Claude Code
|
|
507
|
-
* to each sub-agent JSONL. It carries
|
|
508
|
-
*
|
|
509
|
-
*
|
|
528
|
+
* Strategy: read the `agent-<id>.meta.json` sibling that the Claude Code
|
|
529
|
+
* binary writes next to each sub-agent JSONL. It carries `{ agentType,
|
|
530
|
+
* description, toolUseId }` where `toolUseId` is the primary key of the
|
|
531
|
+
* `subagents` row — the same `event.tool_use_id` value the pretool hook
|
|
532
|
+
* (`subagent-tracker-pretool.mjs`) uses when it inserts the DB row. We use
|
|
533
|
+
* the direct `toolUseId` lookup first (exact PK match, race-safe); fall back
|
|
534
|
+
* to the fuzzy `(agentType, description)` match only when `toolUseId` is
|
|
535
|
+
* absent (older Claude Code versions that pre-date this field in the meta).
|
|
510
536
|
*
|
|
511
537
|
* Edge cases:
|
|
512
538
|
* - meta.json missing or unreadable: no-op (the row stays unlinked; liveness
|
|
513
539
|
* writes from this agent's JSONL won't land, but the system stays correct).
|
|
540
|
+
* - `toolUseId` present but no matching row (hook crashed / race): fall
|
|
541
|
+
* through to the fuzzy match so the link is still attempted.
|
|
514
542
|
* - Multiple in-flight rows with identical (agent_type, description): the
|
|
515
543
|
* most recently started one wins (FIFO matches dispatch order in practice).
|
|
516
544
|
* - Row already linked to a different agentId: SQL `WHERE jsonl_agent_id IS
|
|
@@ -526,7 +554,7 @@ export function backfillJsonlAgentId(
|
|
|
526
554
|
log?: (msg: string) => void,
|
|
527
555
|
): void {
|
|
528
556
|
const metaPath = jsonlPath.replace(/\.jsonl$/, '.meta.json')
|
|
529
|
-
let meta: { agentType?: string; description?: string }
|
|
557
|
+
let meta: { agentType?: string; description?: string; toolUseId?: string } | null
|
|
530
558
|
try {
|
|
531
559
|
const raw = readFileSync(metaPath, 'utf8')
|
|
532
560
|
meta = JSON.parse(raw)
|
|
@@ -534,8 +562,8 @@ export function backfillJsonlAgentId(
|
|
|
534
562
|
log?.(`subagent-watcher: backfill skip ${agentId} — meta.json not readable at ${metaPath}`)
|
|
535
563
|
return
|
|
536
564
|
}
|
|
537
|
-
if (!meta.agentType && !meta.description) {
|
|
538
|
-
log?.(`subagent-watcher: backfill skip ${agentId} — meta.json has no agentType/description`)
|
|
565
|
+
if (!meta || (!meta.agentType && !meta.description && !meta.toolUseId)) {
|
|
566
|
+
log?.(`subagent-watcher: backfill skip ${agentId} — meta.json has no agentType/description/toolUseId`)
|
|
539
567
|
return
|
|
540
568
|
}
|
|
541
569
|
|
|
@@ -545,27 +573,51 @@ export function backfillJsonlAgentId(
|
|
|
545
573
|
.get(agentId)
|
|
546
574
|
if (already != null) return
|
|
547
575
|
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
576
|
+
// Primary path (Bug 1 fix): direct PK lookup via the toolUseId Claude Code
|
|
577
|
+
// writes to meta.json. The pretool hook inserts the row with `id =
|
|
578
|
+
// event.tool_use_id`, so this is an exact match with no ambiguity — no
|
|
579
|
+
// race, no description-collision, no fuzzy-match false-negative.
|
|
580
|
+
let candidateId: string | null = null
|
|
581
|
+
if (meta.toolUseId) {
|
|
582
|
+
const direct = db
|
|
583
|
+
.prepare('SELECT id FROM subagents WHERE id = ? AND jsonl_agent_id IS NULL LIMIT 1')
|
|
584
|
+
.get(meta.toolUseId) as { id: string } | null
|
|
585
|
+
if (direct != null) {
|
|
586
|
+
candidateId = direct.id
|
|
587
|
+
log?.(`subagent-watcher: backfill direct-key match ${agentId} → ${candidateId} (toolUseId=${meta.toolUseId})`)
|
|
588
|
+
} else {
|
|
589
|
+
log?.(`subagent-watcher: backfill direct-key miss ${agentId} toolUseId=${meta.toolUseId} — falling back to fuzzy match`)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Fallback path: fuzzy (agentType, description) match for older Claude Code
|
|
594
|
+
// versions whose meta.json predates the toolUseId field.
|
|
595
|
+
if (candidateId == null && (meta.agentType || meta.description)) {
|
|
596
|
+
const fuzzy = db
|
|
597
|
+
.prepare(`
|
|
598
|
+
SELECT id FROM subagents
|
|
599
|
+
WHERE jsonl_agent_id IS NULL
|
|
600
|
+
AND agent_type IS ?
|
|
601
|
+
AND description IS ?
|
|
602
|
+
ORDER BY started_at DESC
|
|
603
|
+
LIMIT 1
|
|
604
|
+
`)
|
|
605
|
+
.get(meta.agentType ?? null, meta.description ?? null) as { id: string } | null
|
|
606
|
+
if (fuzzy != null) {
|
|
607
|
+
candidateId = fuzzy.id
|
|
608
|
+
log?.(`subagent-watcher: backfill fuzzy match ${agentId} → ${candidateId} (type=${meta.agentType} desc=${meta.description})`)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (candidateId == null) {
|
|
613
|
+
log?.(`subagent-watcher: backfill no candidate for ${agentId} (toolUseId=${meta.toolUseId} type=${meta.agentType} desc=${meta.description})`)
|
|
562
614
|
return
|
|
563
615
|
}
|
|
564
616
|
|
|
565
617
|
db
|
|
566
618
|
.prepare('UPDATE subagents SET jsonl_agent_id = ? WHERE id = ?')
|
|
567
|
-
.run(agentId,
|
|
568
|
-
log?.(`subagent-watcher: backfill linked ${agentId} → ${
|
|
619
|
+
.run(agentId, candidateId)
|
|
620
|
+
log?.(`subagent-watcher: backfill linked ${agentId} → ${candidateId}`)
|
|
569
621
|
|
|
570
622
|
// Backfill parent_turn_key (gateway-side). The PreToolUse hook can't know
|
|
571
623
|
// the gateway-minted Telegram turn_key (a chat+topic+turn key) — it only
|
|
@@ -588,7 +640,7 @@ export function backfillJsonlAgentId(
|
|
|
588
640
|
try {
|
|
589
641
|
const linkedRow = db
|
|
590
642
|
.prepare('SELECT started_at, parent_turn_key FROM subagents WHERE id = ?')
|
|
591
|
-
.get(
|
|
643
|
+
.get(candidateId) as { started_at: number; parent_turn_key: string | null } | null
|
|
592
644
|
if (linkedRow != null && linkedRow.parent_turn_key == null) {
|
|
593
645
|
const turn = db
|
|
594
646
|
.prepare(
|
|
@@ -600,12 +652,12 @@ export function backfillJsonlAgentId(
|
|
|
600
652
|
if (turn?.turn_key != null) {
|
|
601
653
|
db
|
|
602
654
|
.prepare('UPDATE subagents SET parent_turn_key = ? WHERE id = ?')
|
|
603
|
-
.run(turn.turn_key,
|
|
604
|
-
log?.(`subagent-watcher: backfill parent_turn_key ${
|
|
655
|
+
.run(turn.turn_key, candidateId)
|
|
656
|
+
log?.(`subagent-watcher: backfill parent_turn_key ${candidateId} → ${turn.turn_key}`)
|
|
605
657
|
}
|
|
606
658
|
}
|
|
607
659
|
} catch (err) {
|
|
608
|
-
log?.(`subagent-watcher: parent_turn_key backfill skipped for ${
|
|
660
|
+
log?.(`subagent-watcher: parent_turn_key backfill skipped for ${candidateId} — ${(err as Error).message}`)
|
|
609
661
|
}
|
|
610
662
|
}
|
|
611
663
|
|
|
@@ -743,6 +795,62 @@ export function readSubTail(
|
|
|
743
795
|
if (errInfo.detail) entry.errorDetail = errInfo.detail.slice(0, SUBAGENT_RESULT_TEXT_MAX)
|
|
744
796
|
}
|
|
745
797
|
const events = projectSubagentLine(line, entry.agentId, startState)
|
|
798
|
+
// Narrative-dedup gate (JSONL-text-narrative primitive) — fire the
|
|
799
|
+
// narrative progress cue for a SHOWN sub_agent_text block. Identical
|
|
800
|
+
// shape to the inline #1720 onProgress below; factored out so the gate
|
|
801
|
+
// (stage-on-text, resolve-on-tool/turn_end) can replay a previously
|
|
802
|
+
// pending block exactly once. `latestSummary` carries the worker's
|
|
803
|
+
// narrative result (entry.lastResultText), never tool labels.
|
|
804
|
+
const fireNarrativeProgress = (): void => {
|
|
805
|
+
if (onProgress == null || entry.state !== 'running' || entry.historical) return
|
|
806
|
+
try {
|
|
807
|
+
onProgress({
|
|
808
|
+
agentId: entry.agentId,
|
|
809
|
+
description: entry.description,
|
|
810
|
+
latestSummary: entry.lastResultText,
|
|
811
|
+
elapsedMs: now - entry.dispatchedAt,
|
|
812
|
+
prevBucketIdx: entry.lastProgressBucketIdx,
|
|
813
|
+
setBucketIdx: (b: number) => {
|
|
814
|
+
entry.lastProgressBucketIdx = b
|
|
815
|
+
},
|
|
816
|
+
lastTool: entry.lastTool,
|
|
817
|
+
toolCount: entry.toolCount,
|
|
818
|
+
})
|
|
819
|
+
} catch (cbErr) {
|
|
820
|
+
log?.(`subagent-watcher: onProgress callback error ${entry.agentId}: ${(cbErr as Error).message}`)
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
// Resolve a pending sub-agent narrative against a lookahead event.
|
|
824
|
+
// SUPPRESS only when the pending block drafts a reply/stream_reply
|
|
825
|
+
// tool's text; otherwise SHOW (fire the cue). See narrative-dedup.ts §2b.
|
|
826
|
+
//
|
|
827
|
+
// Two lookahead shapes:
|
|
828
|
+
// - sub_agent_tool_use: `toolName`/`toolInput` are the tool — suppress
|
|
829
|
+
// a draft of THIS tool's reply text.
|
|
830
|
+
// - sub_agent_turn_end: `toolName` is null. NIT 3 (turn_end symmetry):
|
|
831
|
+
// a FOREGROUND sub-agent that called stream_reply/reply as its final
|
|
832
|
+
// tool then emitted a trailing text block would, under the old
|
|
833
|
+
// unconditional SHOW, surface a draft of the delivered answer. So at
|
|
834
|
+
// turn_end we apply the SAME conservative dedup as main-agent step 3:
|
|
835
|
+
// compare the trailing block against the worker's last reply text
|
|
836
|
+
// (`entry.lastReplyText`) and suppress a draft. Background workers
|
|
837
|
+
// never set lastReplyText, so their trailing narration still SHOWs.
|
|
838
|
+
const resolvePendingSubNarrative = (
|
|
839
|
+
toolName: string | null,
|
|
840
|
+
toolInput: Record<string, unknown> | undefined,
|
|
841
|
+
): void => {
|
|
842
|
+
if (entry.pendingNarrative == null) return
|
|
843
|
+
const pending = entry.pendingNarrative
|
|
844
|
+
entry.pendingNarrative = null
|
|
845
|
+
if (toolName != null && REPLY_TOOLS.has(toolName)) {
|
|
846
|
+
const replyText = typeof toolInput?.text === 'string' ? (toolInput.text as string) : ''
|
|
847
|
+
if (isDraftOfReply(pending.text, replyText)) return // draft of the reply → SUPPRESS
|
|
848
|
+
} else if (toolName == null && entry.lastReplyText != null && entry.lastReplyText.length > 0) {
|
|
849
|
+
// turn_end path: suppress a trailing draft of the delivered answer.
|
|
850
|
+
if (isDraftOfReply(pending.text, entry.lastReplyText)) return
|
|
851
|
+
}
|
|
852
|
+
fireNarrativeProgress()
|
|
853
|
+
}
|
|
746
854
|
for (const ev of events) {
|
|
747
855
|
const idleSecBeforeBump = Math.round((now - entry.lastActivityAt) / 1000)
|
|
748
856
|
entry.lastActivityAt = now
|
|
@@ -783,6 +891,17 @@ export function readSubTail(
|
|
|
783
891
|
log?.(`subagent-watcher: stall cleared for ${entry.agentId} (activity resumed after ${idleSecBeforeBump}s — re-arming detection)`)
|
|
784
892
|
}
|
|
785
893
|
if (ev.kind === 'sub_agent_tool_use') {
|
|
894
|
+
// Narrative-dedup gate step 2: a sub_agent_text block was pending;
|
|
895
|
+
// this tool is the lookahead that decides it (SHOW unless it drafts
|
|
896
|
+
// a reply tool's text). Runs before the tool's own progress cue so
|
|
897
|
+
// a working preamble surfaces just ahead of its tool step.
|
|
898
|
+
resolvePendingSubNarrative(ev.toolName, ev.input)
|
|
899
|
+
// NIT 3: capture a foreground sub-agent's actual reply text so the
|
|
900
|
+
// turn_end path can suppress a trailing draft of it (see
|
|
901
|
+
// resolvePendingSubNarrative). Only REPLY_TOOLS carry the answer.
|
|
902
|
+
if (REPLY_TOOLS.has(ev.toolName) && typeof ev.input?.text === 'string') {
|
|
903
|
+
entry.lastReplyText = ev.input.text as string
|
|
904
|
+
}
|
|
786
905
|
entry.toolCount++
|
|
787
906
|
// P0 of #662: surface the most recent tool name + sanitised
|
|
788
907
|
// arg so the driver's fleet-state shadow can render the
|
|
@@ -830,7 +949,7 @@ export function readSubTail(
|
|
|
830
949
|
// set at dispatch time (from the parent Agent/Task tool_use input)
|
|
831
950
|
// and must remain stable. Overwriting it with the sub-agent's first
|
|
832
951
|
// narrative line caused a race-condition-dependent display (issue #352).
|
|
833
|
-
entry.lastSummaryLine = ev.text
|
|
952
|
+
entry.lastSummaryLine = clipNarrative(ev.text)
|
|
834
953
|
// Retain the full text of the most recent narrative emission —
|
|
835
954
|
// for a worker the final such line before turn_end IS its
|
|
836
955
|
// result summary (the worker prompt asks it to "return a
|
|
@@ -841,29 +960,28 @@ export function readSubTail(
|
|
|
841
960
|
// args or file content — consistent with the watcher's
|
|
842
961
|
// "descriptions only" privacy posture.
|
|
843
962
|
entry.lastResultText = ev.text.trim().slice(0, SUBAGENT_RESULT_TEXT_MAX)
|
|
844
|
-
// #1720
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
elapsedMs: now - entry.dispatchedAt,
|
|
855
|
-
prevBucketIdx: entry.lastProgressBucketIdx,
|
|
856
|
-
setBucketIdx: (b: number) => {
|
|
857
|
-
entry.lastProgressBucketIdx = b
|
|
858
|
-
},
|
|
859
|
-
lastTool: entry.lastTool,
|
|
860
|
-
toolCount: entry.toolCount,
|
|
861
|
-
})
|
|
862
|
-
} catch (cbErr) {
|
|
863
|
-
log?.(`subagent-watcher: onProgress callback error ${entry.agentId}: ${(cbErr as Error).message}`)
|
|
864
|
-
}
|
|
963
|
+
// #1720 + JSONL-text-narrative gate step 1: stage this block for
|
|
964
|
+
// one lookahead step instead of firing the progress cue
|
|
965
|
+
// immediately. A previously-pending block had nothing reply-shaped
|
|
966
|
+
// after it (pure narration) → flush it as SHOWN now; then stage
|
|
967
|
+
// THIS block. Its eventual SHOW/SUPPRESS is decided by the next
|
|
968
|
+
// sub_agent_tool_use / sub_agent_turn_end. `lastResultText` /
|
|
969
|
+
// `lastSummaryLine` above already updated unconditionally — the
|
|
970
|
+
// handback payload is independent of the progress-cue decision.
|
|
971
|
+
if (entry.pendingNarrative != null) {
|
|
972
|
+
fireNarrativeProgress() // prior pending was pure narration → SHOW
|
|
865
973
|
}
|
|
974
|
+
entry.pendingNarrative = { text: ev.text }
|
|
866
975
|
} else if (ev.kind === 'sub_agent_turn_end') {
|
|
976
|
+
// Narrative-dedup gate step 3: a trailing sub_agent_text block with
|
|
977
|
+
// nothing after it. SUPPRESS only when it drafts the foreground
|
|
978
|
+
// sub-agent's delivered reply (entry.lastReplyText, set above on a
|
|
979
|
+
// REPLY_TOOL tool_use) — symmetric with main-agent step 3; otherwise
|
|
980
|
+
// SHOW. Background workers never set lastReplyText, so their trailing
|
|
981
|
+
// narration still SHOWs. The worker's result is carried separately
|
|
982
|
+
// via lastResultText/onFinish, so a SHOWN trailing cue here is purely
|
|
983
|
+
// the transient liveness beat.
|
|
984
|
+
resolvePendingSubNarrative(null, undefined)
|
|
867
985
|
if (entry.state === 'running') {
|
|
868
986
|
entry.state = 'done'
|
|
869
987
|
// Bug 2 fix (#333): mark the DB row completed via watcher's turn_end
|
|
@@ -1456,25 +1574,53 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
1456
1574
|
const subagentsPath = join(projectPath, sDir, 'subagents')
|
|
1457
1575
|
if (!fs.existsSync(subagentsPath)) continue
|
|
1458
1576
|
|
|
1459
|
-
// Watch
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
const
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1577
|
+
// Watch a single flat subagents dir and scan its agent-*.jsonl files.
|
|
1578
|
+
// Reused for both the base subagents/ dir and each workflow sub-dir.
|
|
1579
|
+
const watchAndScan = (dirPath: string): void => {
|
|
1580
|
+
if (!dirWatchers.has(dirPath)) {
|
|
1581
|
+
try {
|
|
1582
|
+
const w = fs.watch(dirPath, (_event, filename) => {
|
|
1583
|
+
if (!filename || !filename.toString().startsWith('agent-') || !filename.toString().endsWith('.jsonl')) return
|
|
1584
|
+
const filePath = join(dirPath, filename.toString())
|
|
1585
|
+
if (!knownFiles.has(filePath)) {
|
|
1586
|
+
scanSubagentsDir(dirPath)
|
|
1587
|
+
}
|
|
1588
|
+
})
|
|
1589
|
+
dirWatchers.set(dirPath, w)
|
|
1590
|
+
log?.(`subagent-watcher: watching dir ${dirPath}`)
|
|
1591
|
+
} catch (err) {
|
|
1592
|
+
log?.(`subagent-watcher: dir watch failed ${dirPath}: ${(err as Error).message}`)
|
|
1593
|
+
}
|
|
1473
1594
|
}
|
|
1595
|
+
scanSubagentsDir(dirPath)
|
|
1474
1596
|
}
|
|
1475
1597
|
|
|
1476
|
-
//
|
|
1477
|
-
|
|
1598
|
+
// Register the base subagents dir
|
|
1599
|
+
watchAndScan(subagentsPath)
|
|
1600
|
+
|
|
1601
|
+
// Workflow sub-agents (spawned by the Workflow tool) write to:
|
|
1602
|
+
// subagents/workflows/wf_<id>/agent-<id>.jsonl
|
|
1603
|
+
// The flat readdir above misses these because it only sees the
|
|
1604
|
+
// "workflows" directory entry (not matching agent-*.jsonl). Descend
|
|
1605
|
+
// one level so each wf_*/ dir gets the same watch+scan treatment.
|
|
1606
|
+
const workflowsPath = join(subagentsPath, 'workflows')
|
|
1607
|
+
if (fs.existsSync(workflowsPath)) {
|
|
1608
|
+
let wfDirs: string[]
|
|
1609
|
+
try {
|
|
1610
|
+
wfDirs = fs.readdirSync(workflowsPath) as string[]
|
|
1611
|
+
} catch { continue }
|
|
1612
|
+
for (const wfDir of wfDirs) {
|
|
1613
|
+
try {
|
|
1614
|
+
const wfPath = join(workflowsPath, wfDir)
|
|
1615
|
+
// Only descend into actual directories. statSync succeeds on
|
|
1616
|
+
// regular files too (e.g. a stray journal.jsonl or lock file
|
|
1617
|
+
// sitting directly in workflows/), so check isDirectory()
|
|
1618
|
+
// explicitly rather than relying on a throw that never comes.
|
|
1619
|
+
if (!fs.statSync(wfPath).isDirectory()) continue
|
|
1620
|
+
watchAndScan(wfPath)
|
|
1621
|
+
} catch { /* skip entries we can't stat or watch */ }
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1478
1624
|
}
|
|
1479
1625
|
}
|
|
1480
1626
|
}
|