switchroom 0.18.6 → 0.18.8

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 (116) hide show
  1. package/dist/agent-scheduler/index.js +1 -0
  2. package/dist/auth-broker/index.js +1 -0
  3. package/dist/cli/autoaccept-poll.js +140 -33
  4. package/dist/cli/notion-write-pretool.mjs +1 -0
  5. package/dist/cli/switchroom.js +1172 -812
  6. package/dist/host-control/main.js +2 -1
  7. package/dist/vault/approvals/kernel-server.js +1 -0
  8. package/dist/vault/broker/server.js +1 -0
  9. package/package.json +3 -3
  10. package/profiles/_base/cron-session.sh.hbs +55 -16
  11. package/profiles/_base/start.sh.hbs +146 -50
  12. package/profiles/default/CLAUDE.md.hbs +1 -1
  13. package/skills/switchroom-runtime/SKILL.md +2 -0
  14. package/telegram-plugin/dist/bridge/bridge.js +22 -0
  15. package/telegram-plugin/dist/gateway/gateway.js +2965 -862
  16. package/telegram-plugin/dist/server.js +24 -0
  17. package/telegram-plugin/flood-circuit-breaker.ts +123 -0
  18. package/telegram-plugin/gateway/activity-card-store.ts +63 -18
  19. package/telegram-plugin/gateway/always-allow-persist-queue.ts +438 -0
  20. package/telegram-plugin/gateway/approval-timeout-inbound-builders.ts +150 -0
  21. package/telegram-plugin/gateway/boot-card.ts +27 -0
  22. package/telegram-plugin/gateway/busy-ack.ts +106 -0
  23. package/telegram-plugin/gateway/clean-shutdown-marker.ts +68 -20
  24. package/telegram-plugin/gateway/gateway.ts +1618 -198
  25. package/telegram-plugin/gateway/inbound-spool.ts +2 -1
  26. package/telegram-plugin/gateway/inject-handler.test.ts +19 -0
  27. package/telegram-plugin/gateway/inject-handler.ts +17 -0
  28. package/telegram-plugin/gateway/ipc-protocol.ts +44 -2
  29. package/telegram-plugin/gateway/ipc-server.ts +40 -0
  30. package/telegram-plugin/gateway/mental-model-propose-diff.ts +61 -5
  31. package/telegram-plugin/gateway/model-command.ts +227 -54
  32. package/telegram-plugin/gateway/pending-card-expiry.ts +98 -0
  33. package/telegram-plugin/gateway/pending-card-store.ts +173 -0
  34. package/telegram-plugin/gateway/pending-inbound-buffer.ts +12 -2
  35. package/telegram-plugin/gateway/resume-inbound-builder.ts +240 -2
  36. package/telegram-plugin/gateway/session-model-file.ts +198 -0
  37. package/telegram-plugin/gateway/session-model-source.ts +73 -0
  38. package/telegram-plugin/gateway/status-pin-store.ts +82 -22
  39. package/telegram-plugin/gateway/worker-feed-dispatch.ts +24 -1
  40. package/telegram-plugin/gateway/worker-pin-reaper.ts +114 -0
  41. package/telegram-plugin/hooks/hooks.json +10 -10
  42. package/telegram-plugin/hooks/run-hook.sh +84 -0
  43. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +30 -7
  44. package/telegram-plugin/model-label.ts +69 -0
  45. package/telegram-plugin/model-unavailable.ts +26 -0
  46. package/telegram-plugin/operator-events.ts +24 -0
  47. package/telegram-plugin/permission-diff.ts +128 -0
  48. package/telegram-plugin/pty-partial-handler.ts +39 -0
  49. package/telegram-plugin/registry/subagents-schema.ts +80 -1
  50. package/telegram-plugin/registry/subagents.test.ts +90 -0
  51. package/telegram-plugin/render/rich-render.ts +79 -1
  52. package/telegram-plugin/retry-api-call.ts +62 -0
  53. package/telegram-plugin/session-tail.ts +28 -0
  54. package/telegram-plugin/shared/bot-runtime.ts +8 -1
  55. package/telegram-plugin/silence-poke.ts +14 -0
  56. package/telegram-plugin/silent-end.ts +49 -4
  57. package/telegram-plugin/stream-controller.ts +156 -38
  58. package/telegram-plugin/subagent-watcher.ts +222 -37
  59. package/telegram-plugin/tests/activity-card-store.test.ts +47 -2
  60. package/telegram-plugin/tests/always-allow-persist-queue.test.ts +529 -0
  61. package/telegram-plugin/tests/approval-card-restart-outcome.test.ts +218 -0
  62. package/telegram-plugin/tests/approval-timeout-inbound-builders.test.ts +94 -0
  63. package/telegram-plugin/tests/boot-card-flood-suppress.test.ts +111 -0
  64. package/telegram-plugin/tests/busy-ack-wiring.test.ts +118 -0
  65. package/telegram-plugin/tests/busy-ack.test.ts +121 -0
  66. package/telegram-plugin/tests/button-tap-turn-gated.test.ts +263 -0
  67. package/telegram-plugin/tests/flood-circuit-breaker.test.ts +74 -0
  68. package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +85 -27
  69. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +179 -25
  70. package/telegram-plugin/tests/ipc-server-query-pending-permission.test.ts +157 -0
  71. package/telegram-plugin/tests/mental-model-name-entity-corruption.test.ts +119 -0
  72. package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -5
  73. package/telegram-plugin/tests/model-command.test.ts +203 -43
  74. package/telegram-plugin/tests/model-label.test.ts +64 -0
  75. package/telegram-plugin/tests/model-unavailable.test.ts +41 -0
  76. package/telegram-plugin/tests/operator-events.test.ts +1 -0
  77. package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +202 -0
  78. package/telegram-plugin/tests/pending-card-expiry.test.ts +190 -0
  79. package/telegram-plugin/tests/pending-card-store.test.ts +173 -0
  80. package/telegram-plugin/tests/permission-diff.test.ts +111 -0
  81. package/telegram-plugin/tests/pty-partial-handler.test.ts +56 -0
  82. package/telegram-plugin/tests/render/render-outbound-chunks.test.ts +98 -0
  83. package/telegram-plugin/tests/resume-inbound-builder.test.ts +286 -0
  84. package/telegram-plugin/tests/retry-api-call.test.ts +59 -0
  85. package/telegram-plugin/tests/run-hook-wrapper.test.ts +132 -0
  86. package/telegram-plugin/tests/session-model-file.test.ts +132 -0
  87. package/telegram-plugin/tests/session-model-source.test.ts +67 -0
  88. package/telegram-plugin/tests/session-tail.test.ts +64 -0
  89. package/telegram-plugin/tests/silent-end.test.ts +46 -1
  90. package/telegram-plugin/tests/slot-banner-boot-recovery.test.ts +3 -3
  91. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +3 -3
  92. package/telegram-plugin/tests/status-pin-store.test.ts +62 -6
  93. package/telegram-plugin/tests/stream-controller-chunk-cap.test.ts +122 -0
  94. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +39 -0
  95. package/telegram-plugin/tests/subagent-watcher-boot-promotion-replay.test.ts +107 -4
  96. package/telegram-plugin/tests/subagent-watcher-handback-gaps.test.ts +42 -4
  97. package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +47 -0
  98. package/telegram-plugin/tests/subagent-watcher-terminated-ids-cap.test.ts +150 -0
  99. package/telegram-plugin/tests/subagent-watcher.test.ts +54 -0
  100. package/telegram-plugin/tests/tool-activity-summary.test.ts +37 -0
  101. package/telegram-plugin/tests/typing-wrap.test.ts +23 -0
  102. package/telegram-plugin/tests/voice-send.test.ts +308 -0
  103. package/telegram-plugin/tests/worker-activity-feed.test.ts +11 -0
  104. package/telegram-plugin/tests/worker-feed-dispatch.test.ts +126 -0
  105. package/telegram-plugin/tests/worker-pin-reaper.test.ts +132 -0
  106. package/telegram-plugin/tool-activity-summary.ts +22 -2
  107. package/telegram-plugin/typing-wrap.ts +72 -25
  108. package/telegram-plugin/uat/scenarios/jtbd-deliberate-restart-resumes-dm.test.ts +118 -0
  109. package/telegram-plugin/uat/scenarios/jtbd-midflight-busy-ack-dm.test.ts +201 -0
  110. package/telegram-plugin/uat/scenarios/jtbd-worker-pin-lifecycle-dm.test.ts +208 -0
  111. package/telegram-plugin/uat/scenarios/vault-card-survives-gateway-restart-dm.test.ts +140 -0
  112. package/telegram-plugin/uat/scenarios/vault-deny-resumes-turn-dm.test.ts +84 -0
  113. package/telegram-plugin/uat/scenarios/vault-timeout-wakes-agent-dm.test.ts +91 -0
  114. package/telegram-plugin/voice-ondemand.ts +25 -1
  115. package/telegram-plugin/voice-send.ts +154 -0
  116. package/telegram-plugin/worker-activity-feed.ts +9 -0
@@ -88,7 +88,8 @@ export function spoolId(msg: InboundMessage): string {
88
88
  // given turn can only be one or the other.
89
89
  if (
90
90
  (msg.meta?.source === 'resume_interrupted' ||
91
- msg.meta?.source === 'resume_watchdog_timeout') &&
91
+ msg.meta?.source === 'resume_watchdog_timeout' ||
92
+ msg.meta?.source === 'resume_deferred') &&
92
93
  typeof msg.meta?.resume_turn_key === 'string' &&
93
94
  msg.meta.resume_turn_key.length > 0
94
95
  ) {
@@ -104,6 +104,25 @@ describe('handleInjectCommand — guards', () => {
104
104
  expect(replies[0].text).toContain('Usage')
105
105
  expect(replies[0].text).toContain('/cost')
106
106
  })
107
+
108
+ it('refuses `/inject /model <name>` and points at the real /model driver', async () => {
109
+ // Bug 7c: /inject /model <arg> bypasses the whole /model driver (no busy
110
+ // gate, no sr-* carrier/base-URL repoint, no session-override recording).
111
+ const inject = vi.fn()
112
+ const { deps, replies } = makeDeps({ getArgs: () => '/model sonnet', inject })
113
+ await handleInjectCommand(fakeCtx(), deps)
114
+ expect(inject).not.toHaveBeenCalled()
115
+ expect(replies).toHaveLength(1)
116
+ expect(replies[0].text).toContain('/model sonnet')
117
+ expect(replies[0].text).toContain('own driver')
118
+ })
119
+
120
+ it('still allows bare `/inject /model` (read-only picker) through', async () => {
121
+ const inject = vi.fn().mockResolvedValue(okResult('/model', 'picker output'))
122
+ const { deps } = makeDeps({ getArgs: () => '/model', inject })
123
+ await handleInjectCommand(fakeCtx(), deps)
124
+ expect(inject).toHaveBeenCalledWith('gymbro', '/model')
125
+ })
107
126
  })
108
127
 
109
128
  describe('handleInjectCommand — outcome=ok', () => {
@@ -146,6 +146,23 @@ export async function handleInjectCommand(ctx: Context, deps: InjectDeps): Promi
146
146
  return
147
147
  }
148
148
  const slashCommand = arg.startsWith('/') ? arg : `/${arg}`
149
+
150
+ // `/inject /model <name>` would type `/model <name>` straight into the pane,
151
+ // bypassing the entire /model driver — no busy gate, no sr-* carrier/base-URL
152
+ // repoint, no session-override recording. That path silently fails for sr-*
153
+ // ids and leaves /status stale for Claude ones. Route the operator to the real
154
+ // command. Bare `/inject /model` (no arg) still opens claude's read-only picker.
155
+ const injectTokens = slashCommand.split(/\s+/)
156
+ if (injectTokens[0]?.toLowerCase() === '/model' && injectTokens.length > 1) {
157
+ const modelArg = injectTokens.slice(1).join(' ')
158
+ await deps.reply(
159
+ ctx,
160
+ `\`/model\` has its own driver — use \`/model ${deps.escapeHtml(modelArg)}\` directly (not via \`/inject\`) so the switch is gated, recorded, and \`/status\` stays honest.`,
161
+ { html: true },
162
+ )
163
+ return
164
+ }
165
+
149
166
  const agentName = deps.getAgentName()
150
167
 
151
168
  let result: InjectResult
@@ -134,6 +134,25 @@ export interface ConfigApprovalResolvedEvent {
134
134
  denySource?: "operator" | "dispatch_failure";
135
135
  }
136
136
 
137
+ /**
138
+ * Issue #2971 — the wedge-watchdog's permission branch answer to
139
+ * `query_pending_permission`. Read-only reply sourced from the gateway's
140
+ * `pendingPermissions` map: `pending: true` means a live Telegram approval
141
+ * card (and the #2724 TTL reaper) is already racing the TUI prompt for this
142
+ * agent, so the watchdog must NOT Esc — the reaper's channel-delivered deny
143
+ * dismisses the TUI cleanly at TTL. `pending: false` means no card exists
144
+ * (bridge down, a `requiresUserInteraction` tool, or genuinely no live
145
+ * request) — the watchdog's Esc fallback is safe and expected.
146
+ */
147
+ export interface PendingPermissionStatusEvent {
148
+ type: "pending_permission_status";
149
+ /** Echoes the correlationId from the originating query. */
150
+ correlationId: string;
151
+ pending: boolean;
152
+ /** The live `pendingPermissions` key, when `pending: true`. */
153
+ requestId?: string;
154
+ }
155
+
137
156
  export type GatewayToClient =
138
157
  | InboundMessage
139
158
  | PermissionEvent
@@ -143,7 +162,8 @@ export type GatewayToClient =
143
162
  | DriveApprovalPostedEvent
144
163
  | Ms365ApprovalPostedEvent
145
164
  | ConfigApprovalResolvedEvent
146
- | RolloutStatusPostedEvent;
165
+ | RolloutStatusPostedEvent
166
+ | PendingPermissionStatusEvent;
147
167
 
148
168
  // === Bridge (Client) -> Gateway messages ===
149
169
 
@@ -555,6 +575,27 @@ export interface RolloutStatusPostedEvent {
555
575
  reason?: string;
556
576
  }
557
577
 
578
+ /**
579
+ * Issue #2971 — sent by the wedge-watchdog (autoaccept-poll sidecar) over
580
+ * the gateway UDS, BEFORE it would otherwise Esc a shape-persistent
581
+ * per-tool permission prompt. Read-only: asks whether a live Telegram
582
+ * approval card (`pendingPermissions`) already exists for this agent's
583
+ * in-flight permission request, so the watchdog can defer to the card /
584
+ * the #2724 TTL reaper instead of racing it with a keystroke.
585
+ *
586
+ * Trust model: same as quota_wall_detected — the socket is per-agent
587
+ * inside the container; `agentName` is validated server-side (and, in the
588
+ * gateway handler, checked against the gateway's own SWITCHROOM_AGENT_NAME)
589
+ * but this message can never mutate state — it only reads
590
+ * `pendingPermissions.size`/entries.
591
+ */
592
+ export interface QueryPendingPermissionMessage {
593
+ type: "query_pending_permission";
594
+ agentName: string;
595
+ /** Caller-generated correlation id, echoed in the reply. */
596
+ correlationId: string;
597
+ }
598
+
558
599
  export type ClientToGateway =
559
600
  | RegisterMessage
560
601
  | ToolCallMessage
@@ -574,4 +615,5 @@ export type ClientToGateway =
574
615
  | SendOutboundMessage
575
616
  | PostSkillProposalMessage
576
617
  | RolloutStatusPostMessage
577
- | RolloutStatusEditMessage;
618
+ | RolloutStatusEditMessage
619
+ | QueryPendingPermissionMessage;
@@ -6,6 +6,7 @@ import type {
6
6
  InjectInboundMessage,
7
7
  SendOutboundMessage,
8
8
  QuotaWallDetectedMessage,
9
+ QueryPendingPermissionMessage,
9
10
  PostSkillProposalMessage,
10
11
  OperatorEventForward,
11
12
  PermissionRequestForward,
@@ -67,6 +68,18 @@ export interface IpcServerOptions {
67
68
  * don't run failover simply ignore it.
68
69
  */
69
70
  onQuotaWallDetected?: (client: IpcClient, msg: QuotaWallDetectedMessage) => void;
71
+ /**
72
+ * Issue #2971 — read-only wedge-watchdog query: is there a live pending
73
+ * permission request (Telegram approval card) for this agent right now?
74
+ * The handler MUST reply on the same connection with a
75
+ * `pending_permission_status` event (matching `correlationId`) sourced
76
+ * from the gateway's `pendingPermissions` map. Optional: when no handler
77
+ * is wired (older gateway build, or a test fixture that doesn't need it),
78
+ * the server replies `pending: false` immediately — the watchdog's
79
+ * existing Esc fallback then behaves exactly as it did before this
80
+ * feature (mixed-version safety).
81
+ */
82
+ onQueryPendingPermission?: (client: IpcClient, msg: QueryPendingPermissionMessage) => void;
70
83
  /**
71
84
  * #2670 one-tap self-improvement — persist a skill-improvement proposal
72
85
  * and post its Approve/Dismiss card. Handler persists the draft bundle to
@@ -330,6 +343,15 @@ export function validateClientMessage(msg: unknown): msg is ClientToGateway {
330
343
  && (typeof m.resetAt !== "number" || !Number.isFinite(m.resetAt as number))) return false;
331
344
  return true;
332
345
  }
346
+ case "query_pending_permission": {
347
+ // Issue #2971 — read-only wedge-watchdog probe. Wire shape only.
348
+ if (typeof m.agentName !== "string"
349
+ || !AGENT_NAME_RE.test(m.agentName as string)) return false;
350
+ if (typeof m.correlationId !== "string"
351
+ || (m.correlationId as string).length === 0
352
+ || (m.correlationId as string).length > 64) return false;
353
+ return true;
354
+ }
333
355
  case "request_config_approval": {
334
356
  // #1623 — hostd-initiated config-edit approval card. Wire shape
335
357
  // only; the handler module validates the diff content.
@@ -449,6 +471,7 @@ export function createIpcServer(options: IpcServerOptions): IpcServer {
449
471
  onInjectInbound,
450
472
  onSendOutbound,
451
473
  onQuotaWallDetected,
474
+ onQueryPendingPermission,
452
475
  onPostSkillProposal,
453
476
  onRequestDriveApproval,
454
477
  onRequestMs365Approval,
@@ -569,6 +592,23 @@ export function createIpcServer(options: IpcServerOptions): IpcServer {
569
592
  case "quota_wall_detected":
570
593
  if (onQuotaWallDetected) onQuotaWallDetected(client, msg as QuotaWallDetectedMessage);
571
594
  break;
595
+ case "query_pending_permission":
596
+ if (onQueryPendingPermission) {
597
+ onQueryPendingPermission(client, msg as QueryPendingPermissionMessage);
598
+ } else {
599
+ // No handler wired — fail closed to "not pending" so the caller's
600
+ // Esc fallback fires (byte-identical to pre-#2971 behaviour).
601
+ try {
602
+ client.send({
603
+ type: "pending_permission_status",
604
+ correlationId: (msg as QueryPendingPermissionMessage).correlationId,
605
+ pending: false,
606
+ });
607
+ } catch {
608
+ /* best effort */
609
+ }
610
+ }
611
+ break;
572
612
  case "request_drive_approval":
573
613
  if (onRequestDriveApproval) {
574
614
  // Handler is async — fire-and-forget here; the handler
@@ -27,6 +27,49 @@
27
27
  import { parseDocument } from "yaml";
28
28
  import { generateUnifiedDiff } from "../../src/web/config-diff.js";
29
29
 
30
+ /**
31
+ * Decode the canonical six HTML/XML entities to their literal characters — the
32
+ * config-write-boundary layer of the #2976 defense-in-depth.
33
+ *
34
+ * SCOPE / reachability (be honest about which half is load-bearing):
35
+ * - `source_query` decode is the REACHABLE half. A model can copy escaped
36
+ * entities out of its Telegram-HTML-rendered context into a proposal's
37
+ * free-form `source_query`, and undecoded it would steer recall on `R&amp;D`
38
+ * instead of `R&D` — a durable, silent corruption of a memory-integrity
39
+ * field. Normalizing here means an escaped query can never LAND in
40
+ * `memory.mental_models[]`.
41
+ * - `name` decode is REDUNDANT belt-and-suspenders. The `mental_model_propose`
42
+ * gateway tool already slug-validates `name` against
43
+ * /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/ BEFORE this runs, so an entity-bearing
44
+ * name is rejected upstream and can't reach here. It's kept only so this
45
+ * write boundary is self-contained if that gate ever moves.
46
+ * - The observed in-NAME corruption (`Nutrition Protocol &amp; Deficit Status`,
47
+ * klanker 2026-07-06) actually arrived via the DIRECT `create_mental_model`
48
+ * Hindsight tool (`ensureMentalModel`, undecoded), NOT this propose path.
49
+ * That vector is OUT OF SCOPE here — covered by the steering.ts context-
50
+ * hygiene + in-service `Dockerfile.hindsight` normalization follow-ups.
51
+ *
52
+ * SINGLE PASS by design (matches issue #2976's success criteria): a
53
+ * double-escaped `R&amp;amp;D` decodes ONE layer to `R&amp;D`, not all the way
54
+ * to `R&D` — we only undo the escaping switchroom itself applied when it
55
+ * rendered the model's prior output, never the user's literal intent. `&amp;`
56
+ * is decoded LAST so `&amp;lt;` collapses to `&lt;` (one layer), not `<`.
57
+ *
58
+ * Accepted tradeoff: a `source_query` a human genuinely intended to contain a
59
+ * literal `&amp;` / `&lt;` / etc. (rare — a reflection question, not markup)
60
+ * will have that one layer decoded. Steering queries reading as escaped HTML is
61
+ * the far more common and more harmful case, so we optimize for it.
62
+ */
63
+ export function decodeCanonicalEntities(s: string): string {
64
+ return s
65
+ .replace(/&lt;/g, "<")
66
+ .replace(/&gt;/g, ">")
67
+ .replace(/&quot;/g, '"')
68
+ .replace(/&apos;/g, "'")
69
+ .replace(/&#39;/g, "'")
70
+ .replace(/&amp;/g, "&");
71
+ }
72
+
30
73
  /**
31
74
  * A proposed mental model, in the same snake_case shape the
32
75
  * `memory.mental_models[]` schema (#2874) accepts. `name` + `source_query`
@@ -125,20 +168,33 @@ export function buildMentalModelAppendDiff(args: {
125
168
  }
126
169
 
127
170
  // Duplicate-name guard (defense-in-depth; the executor also checks up front
128
- // so it never even posts a card for a dupe).
171
+ // so it never even posts a card for a dupe). Compare on the DECODED name so a
172
+ // re-propose of an entity-escaped variant collides with the already-stored
173
+ // literal (#2976) rather than sneaking in a corrupted twin.
174
+ const decodedName = decodeCanonicalEntities(spec.name);
129
175
  const existing = readDeclaredMentalModelNames(configText, agentName);
130
- if (existing.includes(spec.name)) {
176
+ if (existing.includes(decodedName)) {
131
177
  return {
132
178
  ok: false,
133
179
  error: "duplicate",
134
- detail: `mental model "${spec.name}" is already declared for ${agentName}`,
180
+ detail: `mental model "${decodedName}" is already declared for ${agentName}`,
135
181
  };
136
182
  }
137
183
 
184
+ // #2976 write-boundary normalization: decode any HTML/XML entities the model
185
+ // may have copied out of its own escaped context so the LITERAL characters
186
+ // land in config — never `&amp;` / `&lt;` etc. The load-bearing target is the
187
+ // free-form `source_query`; the `name` decode is redundant with the gateway
188
+ // slug gate (an entity-bearing name is rejected upstream) and kept only so
189
+ // this boundary is self-contained. Duplicate-name guard runs on the decoded
190
+ // name above.
191
+ const name = decodedName;
192
+ const source_query = decodeCanonicalEntities(spec.source_query);
193
+
138
194
  // Assemble the minimal, schema-clean declaration node.
139
195
  const item: Record<string, unknown> = {
140
- name: spec.name,
141
- source_query: spec.source_query,
196
+ name,
197
+ source_query,
142
198
  };
143
199
  if (spec.refresh_after_consolidation !== undefined) {
144
200
  item.refresh_after_consolidation = spec.refresh_after_consolidation;