deepline 0.3.297 → 0.3.298

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 (90) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/shared_libs/monitors/quickstart-prompts.ts +15 -0
  3. package/dist/bundling-sources/shared_libs/monitors/starter-templates.ts +9 -98
  4. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +3 -1
  5. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +80 -33
  6. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +115 -59
  7. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +15 -3
  8. package/dist/cli/auth-AQCORSG5.js +31 -0
  9. package/dist/cli/{auth-FOHTQKQV.mjs → auth-TJGQB4RK.mjs} +4 -4
  10. package/dist/cli/{billing-ZY3KZNZD.mjs → billing-BUHGVESC.mjs} +4 -4
  11. package/dist/cli/{billing-NRKIBTKJ.js → billing-HSBXETYG.js} +70 -70
  12. package/dist/cli/{chunk-OSUS6K5I.js → chunk-2Q4LGPVP.js} +4 -4
  13. package/dist/cli/{chunk-FHO5PO6Q.js → chunk-4TOJTO4S.js} +6 -6
  14. package/dist/cli/{chunk-R2COLUYE.mjs → chunk-6IOQ43VG.mjs} +1 -1
  15. package/dist/cli/{chunk-QIJ32UEW.js → chunk-7ZA3OMYT.js} +6 -6
  16. package/dist/cli/{chunk-B52G7BYN.js → chunk-B5R4IGNU.js} +6 -6
  17. package/dist/cli/{chunk-IJGJKU4L.js → chunk-BUKTFUM7.js} +1 -1
  18. package/dist/cli/{chunk-V24XJVLX.js → chunk-BXYKLY5T.js} +48 -48
  19. package/dist/cli/{chunk-FSPE246W.js → chunk-CVNG2LB7.js} +15 -15
  20. package/dist/cli/{chunk-PYXPP7NX.mjs → chunk-FJTNI2TE.mjs} +1 -1
  21. package/dist/cli/{chunk-R5BISWXF.mjs → chunk-FT4JXVAD.mjs} +1 -1
  22. package/dist/cli/{chunk-QPBTAYNK.mjs → chunk-IZKS7OGL.mjs} +2 -2
  23. package/dist/cli/{chunk-PLKA4WFH.mjs → chunk-MJWD6SJR.mjs} +3 -3
  24. package/dist/cli/{chunk-VIOM5OEQ.js → chunk-NHNFNVLD.js} +58 -58
  25. package/dist/cli/{chunk-YH2XATMF.js → chunk-OXCFNXS5.js} +26 -26
  26. package/dist/cli/{chunk-DBZX2IWV.mjs → chunk-RES6RQXX.mjs} +2 -2
  27. package/dist/cli/{chunk-YCCDPYIV.mjs → chunk-S5CEMBUJ.mjs} +4 -4
  28. package/dist/cli/{chunk-34A5RSXF.js → chunk-TOAXEXTC.js} +6 -6
  29. package/dist/cli/{chunk-SZJM7D7W.mjs → chunk-U6G52KUV.mjs} +1 -1
  30. package/dist/cli/{chunk-AC3SC3AC.mjs → chunk-XZNFOO5U.mjs} +1 -1
  31. package/dist/cli/{chunk-YFPOHUWQ.mjs → chunk-YE5PJMFK.mjs} +1 -1
  32. package/dist/cli/{chunk-ZXJWXNGA.mjs → chunk-YFNB2GPD.mjs} +3 -3
  33. package/dist/cli/{chunk-4NYMO67X.js → chunk-YJX7FY4F.js} +2 -2
  34. package/dist/cli/{client-65MCTEMR.mjs → client-2TV2OKUH.mjs} +3 -3
  35. package/dist/cli/client-QKZ6CTWN.js +20 -0
  36. package/dist/cli/{csv-TJWBSMR2.mjs → csv-7XFZ333H.mjs} +3 -3
  37. package/dist/cli/{csv-SHCW3IJL.js → csv-N2CI4OLO.js} +12 -12
  38. package/dist/cli/{db-K7QXXM5C.mjs → db-R37F3UHM.mjs} +4 -4
  39. package/dist/cli/{db-DN6YHQTC.js → db-UXTCDLUO.js} +20 -20
  40. package/dist/cli/{executions-2POJWTCH.js → executions-AVGJESD2.js} +6 -6
  41. package/dist/cli/{executions-FRQO6SJF.mjs → executions-SQ3CYCNF.mjs} +4 -4
  42. package/dist/cli/{feedback-EEYZOH3S.js → feedback-DY5GZCHY.js} +6 -6
  43. package/dist/cli/{feedback-A6232HB5.mjs → feedback-VRUQVLR2.mjs} +3 -3
  44. package/dist/cli/git-72HXNB6W.js +23 -0
  45. package/dist/cli/{git-FRVNZNIA.mjs → git-YCT5WDUM.mjs} +4 -4
  46. package/dist/cli/index.js +73 -73
  47. package/dist/cli/index.mjs +30 -30
  48. package/dist/cli/{legacy-noops-NREP3YMQ.js → legacy-noops-EC6NUDQG.js} +7 -7
  49. package/dist/cli/{legacy-noops-WEN2VQKS.mjs → legacy-noops-TPKXHLEJ.mjs} +4 -4
  50. package/dist/cli/{monitors-2F7GLG24.mjs → monitors-BVIPCPTM.mjs} +4 -4
  51. package/dist/cli/{monitors-EHHO4IGT.js → monitors-RAJW7CTX.js} +85 -85
  52. package/dist/cli/{org-TTFZKSMD.js → org-CYBSXWOL.js} +60 -60
  53. package/dist/cli/{org-JONFO5PV.mjs → org-FQ6DJMPU.mjs} +4 -4
  54. package/dist/cli/{play-Y7U2XU2K.mjs → play-5KQLW4UV.mjs} +6 -6
  55. package/dist/cli/{play-N3JMRLKG.js → play-W5UAFX62.js} +200 -200
  56. package/dist/cli/{providers-I4V4QCOM.js → providers-2YN7WGZW.js} +7 -7
  57. package/dist/cli/{providers-CWPT5L3U.mjs → providers-NPP7DKNJ.mjs} +4 -4
  58. package/dist/cli/{quickstart-2DDUIWC5.js → quickstart-F3UEWVYN.js} +5 -5
  59. package/dist/cli/{quickstart-UBONJ6EY.mjs → quickstart-NRCBIY5D.mjs} +3 -3
  60. package/dist/cli/{search-BHCCYLF2.mjs → search-3HZ7PFXE.mjs} +5 -5
  61. package/dist/cli/{search-Z72FUFXL.js → search-HOHTWO7A.js} +6 -6
  62. package/dist/cli/{secrets-BGD4PNTG.mjs → secrets-44DTR7F7.mjs} +4 -4
  63. package/dist/cli/{secrets-AE22MX42.js → secrets-ROFLUSZW.js} +13 -13
  64. package/dist/cli/{sessions-7SIBBFHF.js → sessions-U7T5MEHM.js} +5 -5
  65. package/dist/cli/{sessions-JYSG6SUN.mjs → sessions-ZGKRSQ7U.mjs} +4 -4
  66. package/dist/cli/{settings-LCA42CL3.js → settings-SAFOFNX3.js} +74 -74
  67. package/dist/cli/{settings-4XWN4JFT.mjs → settings-WDAIAP72.mjs} +7 -7
  68. package/dist/cli/{setup-NID2M4GQ.js → setup-SO5Y5PPR.js} +41 -41
  69. package/dist/cli/{setup-WMOKDWHV.mjs → setup-VZWFB35G.mjs} +6 -6
  70. package/dist/cli/skills-PVOBZRLZ.js +28 -0
  71. package/dist/cli/{skills-6IL3LQUJ.mjs → skills-YUNBWLQ2.mjs} +4 -4
  72. package/dist/cli/{tools-BOIRKQA4.mjs → tools-HSBS6CL6.mjs} +5 -5
  73. package/dist/cli/{tools-TZLX4WAL.js → tools-HYYND5GY.js} +50 -50
  74. package/dist/cli/update-KTAZKIHP.js +28 -0
  75. package/dist/cli/{update-VF3BB3R7.mjs → update-MENMYHFK.mjs} +6 -6
  76. package/dist/cli/{workflow-RUTOLKOS.js → workflow-JZIB3YKI.js} +15 -15
  77. package/dist/cli/{workflow-UZKD5KU2.mjs → workflow-TWGYS6II.mjs} +5 -5
  78. package/dist/index.js +6 -6
  79. package/dist/index.mjs +3 -3
  80. package/dist/install-integrity.json +68 -68
  81. package/dist/release.d.mts +1 -1
  82. package/dist/release.d.ts +1 -1
  83. package/dist/release.js +2 -2
  84. package/dist/release.mjs +1 -1
  85. package/package.json +1 -1
  86. package/dist/cli/auth-DLGUNY3B.js +0 -31
  87. package/dist/cli/client-QLERARAF.js +0 -20
  88. package/dist/cli/git-SZHVHZWU.js +0 -23
  89. package/dist/cli/skills-AYCZ4UXS.js +0 -28
  90. package/dist/cli/update-EEFUC76J.js +0 -28
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.297',
203
+ version: '0.3.298',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -1,20 +1,35 @@
1
1
  export type MonitorQuickstartPrompt = {
2
+ id: string;
3
+ title: string;
4
+ description: string;
2
5
  prompt: string;
3
6
  icon: 'job-change' | 'new-hires' | 'hiring-signal';
4
7
  };
5
8
 
9
+ /** Shared by the first-run card, template gallery, and template discovery API. */
6
10
  export const MONITOR_QUICKSTART_PROMPTS: MonitorQuickstartPrompt[] = [
7
11
  {
12
+ id: 'new-leadership-hires',
13
+ title: 'New leadership hires',
14
+ description:
15
+ 'Watch target accounts for new VP Sales, Marketing, and Revenue hires.',
8
16
  prompt:
9
17
  '/deepline-gtm Monitor my target accounts for new VP Sales, Marketing, and Revenue hires.',
10
18
  icon: 'new-hires',
11
19
  },
12
20
  {
21
+ id: 'champion-job-change',
22
+ title: 'Job changes',
23
+ description: 'Find when people at your target accounts change jobs.',
13
24
  prompt:
14
25
  '/deepline-gtm Monitor when people at my target accounts change jobs.',
15
26
  icon: 'job-change',
16
27
  },
17
28
  {
29
+ id: 'job-post-keywords',
30
+ title: 'Job openings',
31
+ description:
32
+ 'Watch target accounts for hiring signals in the roles you sell to.',
18
33
  prompt:
19
34
  '/deepline-gtm Monitor my target accounts for job openings in the roles I sell to.',
20
35
  icon: 'hiring-signal',
@@ -1,3 +1,5 @@
1
+ import { MONITOR_QUICKSTART_PROMPTS } from './quickstart-prompts';
2
+
1
3
  export type MonitorStarterTemplate = {
2
4
  id: string;
3
5
  title: string;
@@ -5,101 +7,10 @@ export type MonitorStarterTemplate = {
5
7
  prompt: string;
6
8
  };
7
9
 
8
- export const MONITOR_STARTER_TEMPLATES: MonitorStarterTemplate[] = [
9
- {
10
- id: 'job-post-keywords',
11
- title: 'Job post keyword monitor',
12
- description:
13
- 'Watch target accounts for hiring signals that imply new budget, new projects, or urgency.',
14
- prompt: `Create a Deepline definePlay() workflow for a job post keyword monitor.
15
-
16
- Input: a list of target accounts, job title or description keywords, optional regions, and optional owner routing.
17
- Behavior: check each account for recent open roles that match the keywords, dedupe against prior matches, enrich each match with company context and job URL, and rank the signal by relevance.
18
- Output: one concise rep-ready message per owner with account name, matched role, why it matters, job URL, and recommended next step. Include Slack Block Kit formatting and an email fallback.
19
- Verification: add a small dry-run input, run deepline plays check, run one sample execution against a scratch output destination, and report exactly what changed and what was not published.`,
20
- },
21
- {
22
- id: 'champion-job-change',
23
- title: 'Champion job change monitor',
24
- description:
25
- 'Find when a known buyer, user, or champion joins a new account and route a warm-path action.',
26
- prompt: `Create a Deepline definePlay() workflow for a champion job change monitor.
27
-
28
- Input: known champions or contacts from CRM, their prior companies, owner routing rules, and optional target-account filters.
29
- Behavior: detect likely role or company changes, validate the new company and title, suppress weak matches, and connect the signal back to the owning rep or CSM.
30
- Output: one routed message per owner with champion name, old company, new company, new title, relationship context, source URL, and suggested outreach angle. Include Slack Block Kit formatting and an email fallback.
31
- Verification: add a small dry-run input, run deepline plays check, run one sample execution against a scratch output destination, and report exactly what changed and what was not published.`,
32
- },
33
- {
34
- id: 'website-page-change',
35
- title: 'Website page change monitor',
36
- description:
37
- 'Track pricing, docs, security, integrations, or product pages for GTM-relevant changes.',
38
- prompt: `Create a Deepline definePlay() workflow for a website page change monitor.
39
-
40
- Input: a list of account URLs, page categories to watch, a minimum change threshold, and owner routing rules.
41
- Behavior: fetch current page content, compare it with the prior snapshot, classify meaningful GTM changes, ignore noisy cosmetic edits, and store the new snapshot only after a successful run.
42
- Output: one routed message per owner with account name, changed URL, before/after summary, why the change matters, confidence, and suggested next step. Include Slack Block Kit formatting and an email fallback.
43
- Verification: add a small dry-run input, run deepline plays check, run one sample execution against a scratch output destination, and report exactly what changed and what was not published.`,
44
- },
45
- {
46
- id: 'website-visitor-intent',
47
- title: 'Website visitor intent monitor',
48
- description:
49
- 'Turn repeat high-value website visits into account-specific follow-up for the right owner.',
50
- prompt: `Create a Deepline definePlay() workflow for a website visitor intent monitor.
51
-
52
- Input: visitor or account intent events, high-intent page definitions, account matching rules, suppression rules, and owner routing.
53
- Behavior: group events by account, score intent from page depth and recency, suppress existing active opportunities when configured, and explain why the account is worth actioning.
54
- Output: one routed message per owner with account name, intent score, pages visited, timing, CRM context, and recommended next step. Include Slack Block Kit formatting and an email fallback.
55
- Verification: add a small dry-run input, run deepline plays check, run one sample execution against a scratch output destination, and report exactly what changed and what was not published.`,
56
- },
57
- {
58
- id: 'tech-stack-change',
59
- title: 'Tech stack change monitor',
60
- description:
61
- 'Detect install, removal, or competitor stack changes that create a concrete sales motion.',
62
- prompt: `Create a Deepline definePlay() workflow for a tech stack change monitor.
63
-
64
- Input: target accounts, technologies to watch, competitor or complementary-product tags, owner routing, and optional exclusion rules.
65
- Behavior: detect meaningful install or removal events, confirm with available evidence, classify the motion as displacement, expansion, integration, or renewal risk, and dedupe prior alerts.
66
- Output: one routed message per owner with account name, detected technology change, evidence, GTM interpretation, and suggested next step. Include Slack Block Kit formatting and an email fallback.
67
- Verification: add a small dry-run input, run deepline plays check, run one sample execution against a scratch output destination, and report exactly what changed and what was not published.`,
68
- },
69
- {
70
- id: 'social-community-mention',
71
- title: 'Social and community mention monitor',
72
- description:
73
- 'Catch relevant public mentions, buying questions, complaints, or competitor comparisons.',
74
- prompt: `Create a Deepline definePlay() workflow for a social and community mention monitor.
75
-
76
- Input: keywords, target accounts or personas, source allowlist, sentiment or category filters, and owner routing rules.
77
- Behavior: find recent relevant mentions, classify each mention as buying intent, competitor comparison, complaint, hiring, integration request, or general awareness, and suppress duplicates or low-quality chatter.
78
- Output: one routed message per owner with mention summary, source link, account/person context, signal category, confidence, and suggested next step. Include Slack Block Kit formatting and an email fallback.
79
- Verification: add a small dry-run input, run deepline plays check, run one sample execution against a scratch output destination, and report exactly what changed and what was not published.`,
80
- },
81
- {
82
- id: 'competitor-churn',
83
- title: 'Competitor complaint monitor',
84
- description:
85
- 'Surface competitor dissatisfaction or sunset risk while the account is likely open to change.',
86
- prompt: `Create a Deepline definePlay() workflow for a competitor complaint monitor.
87
-
88
- Input: competitor names, complaint or migration keywords, source allowlist, target-account matching rules, and owner routing.
89
- Behavior: find recent competitor dissatisfaction or sunset-risk mentions, match them to companies where possible, classify the pain, and suppress vague or stale mentions.
90
- Output: one routed message per owner with account or person, competitor, pain summary, source URL, suggested displacement angle, and confidence. Include Slack Block Kit formatting and an email fallback.
91
- Verification: add a small dry-run input, run deepline plays check, run one sample execution against a scratch output destination, and report exactly what changed and what was not published.`,
92
- },
93
- {
94
- id: 'customer-usage-renewal',
95
- title: 'Customer usage and renewal monitor',
96
- description:
97
- 'Route expansion, risk, and renewal signals from product behavior to the account team.',
98
- prompt: `Create a Deepline definePlay() workflow for a customer usage and renewal monitor.
99
-
100
- Input: product usage events, entitlement limits, renewal dates, account health fields, and owner routing.
101
- Behavior: detect expansion signals, underuse risk, overage pressure, renewal timing, or feature adoption changes; explain the evidence; and suppress noisy repeat alerts.
102
- Output: one routed message per owner with account name, signal type, usage evidence, renewal context, business impact, and suggested next step. Include Slack Block Kit formatting and an email fallback.
103
- Verification: add a small dry-run input, run deepline plays check, run one sample execution against a scratch output destination, and report exactly what changed and what was not published.`,
104
- },
105
- ];
10
+ export const MONITOR_STARTER_TEMPLATES: MonitorStarterTemplate[] =
11
+ MONITOR_QUICKSTART_PROMPTS.map(({ id, title, description, prompt }) => ({
12
+ id,
13
+ title,
14
+ description,
15
+ prompt,
16
+ }));
@@ -196,6 +196,8 @@ export const RUNTIME_SCHEDULER_CANCEL_STATEMENT_TIMEOUT_MS = 15_000;
196
196
 
197
197
  export type RuntimeSchedulerPoolOptions = {
198
198
  url: string;
199
+ /** Background indexing must not occupy the lifecycle checkout budget. */
200
+ purpose?: 'observability';
199
201
  applicationName?: string;
200
202
  maxClients?: number;
201
203
  /**
@@ -783,7 +785,7 @@ function getPoolEntry(options: RuntimeSchedulerPoolOptions): PoolEntry {
783
785
  // The connection lifetime is part of a Pool's semantics. Do not let a
784
786
  // serverless short-idle caller accidentally reuse a long-lived worker pool
785
787
  // merely because both target the same runtime database.
786
- const key = `${connectionString}::${maxClients}::${idleTimeoutMs}::${maxAcquireQueueDepth}::${maxAcquireQueueWaitMs}`;
788
+ const key = `${connectionString}::${maxClients}::${idleTimeoutMs}::${maxAcquireQueueDepth}::${maxAcquireQueueWaitMs}${options.purpose === 'observability' ? '::observability' : ''}`;
787
789
  const existing = registry.get(key);
788
790
  if (existing) {
789
791
  // Next dev can retain the process-global pool across a module hot reload.
@@ -18023,10 +18023,15 @@ async function claimPostgresSchedulerOutboxPrefix(
18023
18023
  input.searchOnly === true
18024
18024
  ? runLedgerTerminalPendingSql(alias, options)
18025
18025
  : runLedgerUrgentLifecyclePendingSql(alias, options);
18026
+ // Search is locally indexed and acknowledged; it has no remote ingress
18027
+ // receipt to freeze. Preserve causal ordering/due/lease checks, but admit
18028
+ // its full prefix instead of inheriting lifecycle's singleton transport.
18026
18029
  const ingressPrefixSql =
18027
- supportsTerminalBatchMarker && input.allowIngressPrefix === true
18028
- ? `(head.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents})`
18029
- : 'false';
18030
+ input.searchOnly === true
18031
+ ? 'true'
18032
+ : supportsTerminalBatchMarker && input.allowIngressPrefix === true
18033
+ ? `(head.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents})`
18034
+ : 'false';
18030
18035
  return await client.query<{
18031
18036
  row_kind: 'claimed' | 'delivered';
18032
18037
  event_id: string;
@@ -18145,14 +18150,16 @@ async function claimPostgresSchedulerOutboxPrefix(
18145
18150
  -- Admission is causally first and must be projected alone. A later
18146
18151
  -- start/terminal append may not leapfrog a delayed queued row.
18147
18152
  AND (
18148
- head.event_type NOT IN (${RUN_EVENT_KIND_SQL_LITERAL.runCreated}, ${RUN_EVENT_KIND_SQL_LITERAL.runStarted}, ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents})
18153
+ ${input.searchOnly === true ? 'true' : 'false'}
18154
+ OR head.event_type NOT IN (${RUN_EVENT_KIND_SQL_LITERAL.runCreated}, ${RUN_EVENT_KIND_SQL_LITERAL.runStarted}, ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents})
18149
18155
  OR outbox.event_id = head.event_id
18150
18156
  OR (head.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents}
18151
18157
  AND ${ingressPrefixSql})
18152
18158
  )
18153
18159
  -- Only a receiver with the exact-prefix verifier may combine ingress
18154
18160
  -- facts; old launch origins and schemas retain singleton receipts.
18155
- AND (outbox.event_type <> ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents}
18161
+ AND (${input.searchOnly === true ? 'true' : 'false'}
18162
+ OR outbox.event_type <> ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents}
18156
18163
  OR outbox.event_id = head.event_id
18157
18164
  OR ${ingressPrefixSql})
18158
18165
  -- Once any ingress prefix was attempted, its receipt is immutable,
@@ -19456,6 +19463,11 @@ export function postgresSchedulerCancellationOutboxLease(runId: string): {
19456
19463
  }
19457
19464
 
19458
19465
  export type RunLedgerOutboxDispatchInput = {
19466
+ /** Dedicated bounded checkout for indexing; claims/ACKs use withClient. */
19467
+ withObservabilityClient?: <T>(
19468
+ operation: (client: PostgresSchedulerQueryClient) => Promise<T>,
19469
+ ) => Promise<T>;
19470
+
19459
19471
  /** Only the existing background projector claims the independent search prefix. */
19460
19472
  searchOnly?: boolean;
19461
19473
  /** Only the existing background projector indexes deferred raw inputs. */
@@ -19995,63 +20007,98 @@ async function dispatchRunLedgerOutboxBatchWithScope(
19995
20007
  retryingEventIds: released,
19996
20008
  };
19997
20009
  }
19998
- for (const event of events) {
19999
- if (
20010
+ const deferredEvents = events.filter(
20011
+ (event) =>
20000
20012
  payloadField(
20001
20013
  event.payload,
20002
20014
  RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD,
20003
- ) !== true
20004
- )
20005
- continue;
20006
- await withClient(async (client) => {
20007
- await renewPostgresRunLedgerDelivery(
20015
+ ) === true,
20016
+ );
20017
+ const materialize = async (prefix: typeof deferredEvents) => {
20018
+ await withClient((client) =>
20019
+ renewPostgresRunLedgerDelivery(
20008
20020
  client,
20009
20021
  {
20010
- eventIds: events.map((row) => row.eventId),
20022
+ eventIds: events.map((event) => event.eventId),
20011
20023
  leaseOwner,
20012
20024
  leaseSeconds: 120,
20013
20025
  },
20014
20026
  options,
20015
- );
20016
- // Keep projection below its renewed lease. The final payload update
20017
- // fences ownership; indexing locks neither outbox nor Run rows.
20027
+ ),
20028
+ );
20029
+ await (input.withObservabilityClient ?? withClient)(async (client) => {
20018
20030
  await client.query('BEGIN');
20019
20031
  try {
20020
- // Let PostgreSQL cancel before the pool's client-side timeout,
20021
- // so rollback completes while the checkout is still usable.
20022
20032
  await client.query(
20023
20033
  "SELECT set_config('statement_timeout', $1, true)",
20024
20034
  [
20025
20035
  `${Math.min(60_000, RUNTIME_RELIABILITY_POLICY.schedulerDatabase.queryTimeoutMs - 1_000)}ms`,
20026
20036
  ],
20027
20037
  );
20028
- const result = await client.query<{ payload: unknown }>(
20029
- `SELECT ${quoteIdent(schemaName(options))}.materialize_runtime_outbox_event($1, $2) AS payload`,
20030
- [event.eventId, leaseOwner],
20038
+ // The recursive dependency evaluates this already-bounded causal
20039
+ // prefix in order, in one database round trip. Its eight-fact
20040
+ // contract is unchanged. The timeout bounds the entire statement
20041
+ // below the lease; the installed function still fences every fact.
20042
+ const result = await client.query<{
20043
+ ordinal: number;
20044
+ payload: unknown;
20045
+ }>(
20046
+ `WITH RECURSIVE projected(ordinal, payload) AS (
20047
+ SELECT 1, ${quoteIdent(schemaName(options))}.materialize_runtime_outbox_event(($1::text[])[1], $2)
20048
+ UNION ALL
20049
+ SELECT prior.ordinal + 1,
20050
+ ${quoteIdent(schemaName(options))}.materialize_runtime_outbox_event(($1::text[])[prior.ordinal + 1], $2)
20051
+ FROM projected AS prior
20052
+ WHERE prior.ordinal < cardinality($1::text[])
20053
+ )
20054
+ SELECT ordinal, payload FROM projected ORDER BY ordinal`,
20055
+ [prefix.map((event) => event.eventId), leaseOwner],
20031
20056
  );
20032
20057
  await client.query('COMMIT');
20033
- event.payload = result.rows[0]!.payload;
20058
+ for (const row of result.rows)
20059
+ prefix[row.ordinal - 1]!.payload = row.payload;
20034
20060
  } catch (error) {
20035
20061
  await client.query('ROLLBACK');
20062
+ throw error;
20063
+ }
20064
+ });
20065
+ };
20066
+ try {
20067
+ await materialize(deferredEvents);
20068
+ } catch (error) {
20069
+ if (
20070
+ !(
20071
+ error &&
20072
+ typeof error === 'object' &&
20073
+ 'code' in error &&
20074
+ error.code === 'P0001'
20075
+ )
20076
+ ) {
20077
+ throw error;
20078
+ }
20079
+ // An invalid immutable fact must quarantine only itself. The aborted
20080
+ // batch wrote nothing; replay its small prefix one fact at a time to
20081
+ // identify that fact and retain the valid indexed predecessors.
20082
+ for (const event of deferredEvents) {
20083
+ try {
20084
+ await materialize([event]);
20085
+ } catch (factError) {
20036
20086
  if (
20037
- error &&
20038
- typeof error === 'object' &&
20039
- 'code' in error &&
20040
- error.code === 'P0001'
20087
+ factError &&
20088
+ typeof factError === 'object' &&
20089
+ 'code' in factError &&
20090
+ factError.code === 'P0001'
20041
20091
  ) {
20042
- // The installed projector's explicit RAISEs reject malformed
20043
- // or conflicting immutable facts. Retain them in the existing
20044
- // quarantine instead of retrying the same invalid input forever.
20045
20092
  throw new DeferredRuntimeProjectionInputError(
20046
20093
  event.eventId,
20047
- error instanceof Error
20048
- ? error.message
20094
+ factError instanceof Error
20095
+ ? factError.message
20049
20096
  : 'Invalid runtime event projection input.',
20050
20097
  );
20051
20098
  }
20052
- throw error;
20099
+ throw factError;
20053
20100
  }
20054
- });
20101
+ }
20055
20102
  }
20056
20103
  }
20057
20104
  return null;
@@ -74,6 +74,7 @@ export const RUN_LEDGER_OUTBOX_RECOVERY_MAX_IN_FLIGHT = 32;
74
74
  // The higher exported ceiling remains available to isolated recovery callers,
75
75
  // but it is not the steady-state production setting.
76
76
  const RUN_LEDGER_OUTBOX_RECOVERY_PRODUCTION_IN_FLIGHT = 8;
77
+ const RUN_LEDGER_OUTBOX_RECOVERY_INDEX_IN_FLIGHT = 4;
77
78
  // Admissions retain three of four claim opportunities. The fourth selects
78
79
  // the oldest eligible causal head across all runs, preventing terminal work
79
80
  // from starving behind continuous new admissions. An empty class never costs
@@ -89,6 +90,9 @@ export type RunLedgerOutboxRecoveryDeps = {
89
90
  dispatchOnce: (
90
91
  headPriority: RunLedgerOutboxHeadPriority,
91
92
  ) => Promise<PostgresSchedulerOutboxProjectionResult>;
93
+ /** Independent index deliveries share this controller and notification source. */
94
+ dispatchObservabilityOnce?: () => Promise<PostgresSchedulerOutboxProjectionResult>;
95
+ observabilityParallelism?: number;
92
96
  /** Earliest pending retry time or active lease expiry in the durable outbox. */
93
97
  readNextDueAt: () => Promise<Date | null>;
94
98
  /** A direct LISTEN connection. It is a hint only; a null lease is safe. */
@@ -159,6 +163,22 @@ export function startRunLedgerOutboxRecovery(
159
163
  ),
160
164
  )
161
165
  : 1;
166
+ if (deps.dispatchObservabilityOnce && parallelism < 2) {
167
+ throw new Error(
168
+ 'Independent observability delivery requires at least two slots.',
169
+ );
170
+ }
171
+ const observabilityParallelism = deps.dispatchObservabilityOnce
172
+ ? Math.min(
173
+ Math.max(1, deps.observabilityParallelism ?? 1),
174
+ Math.max(1, parallelism - 1),
175
+ )
176
+ : 0;
177
+ const lifecycleParallelism = Math.max(
178
+ 1,
179
+ parallelism - observabilityParallelism,
180
+ );
181
+ let observabilityRequested = false;
162
182
  let closed = false;
163
183
  let requested = false;
164
184
  let failureCount = 0;
@@ -174,22 +194,24 @@ export function startRunLedgerOutboxRecovery(
174
194
  let nextDispatchId = 0;
175
195
  let lastDispatchSettledAt: number | null = null;
176
196
  let admissionTurns = 0;
177
- const dispatchOnce =
178
- async (): Promise<PostgresSchedulerOutboxProjectionResult> => {
179
- const oldestReady =
180
- admissionTurns === ADMISSION_TURNS_PER_OLDEST_READY_TURN;
181
- admissionTurns = oldestReady ? 0 : admissionTurns + 1;
182
- const id = nextDispatchId++;
183
- inFlightStartedAt.set(id, now());
184
- try {
185
- return await deps.dispatchOnce(
186
- oldestReady ? 'oldest_ready' : 'admission_first',
187
- );
188
- } finally {
189
- inFlightStartedAt.delete(id);
190
- lastDispatchSettledAt = now();
191
- }
192
- };
197
+ const dispatchOnce = async (
198
+ observability = false,
199
+ ): Promise<PostgresSchedulerOutboxProjectionResult> => {
200
+ const oldestReady =
201
+ admissionTurns === ADMISSION_TURNS_PER_OLDEST_READY_TURN;
202
+ if (!observability) admissionTurns = oldestReady ? 0 : admissionTurns + 1;
203
+ const id = nextDispatchId++;
204
+ inFlightStartedAt.set(id, now());
205
+ try {
206
+ if (observability) return await deps.dispatchObservabilityOnce!();
207
+ return await deps.dispatchOnce(
208
+ oldestReady ? 'oldest_ready' : 'admission_first',
209
+ );
210
+ } finally {
211
+ inFlightStartedAt.delete(id);
212
+ lastDispatchSettledAt = now();
213
+ }
214
+ };
193
215
 
194
216
  const clearWakeTimer = () => {
195
217
  if (!timer) return;
@@ -220,6 +242,7 @@ export function startRunLedgerOutboxRecovery(
220
242
  requestDrain = (reason: string) => {
221
243
  if (closed) return;
222
244
  requested = true;
245
+ observabilityRequested = !!deps.dispatchObservabilityOnce;
223
246
  if (cooldownUntilMs !== null && now() < cooldownUntilMs) return;
224
247
  clearWakeTimer();
225
248
  if (drainPromise) {
@@ -313,11 +336,13 @@ export function startRunLedgerOutboxRecovery(
313
336
  const drain = async (reason: string): Promise<void> => {
314
337
  type Delivery = {
315
338
  id: symbol;
339
+ observability: boolean;
316
340
  result:
317
341
  | PromiseSettledResult<PostgresSchedulerOutboxProjectionResult>
318
342
  | { status: 'skipped' };
319
343
  };
320
344
  const pending = new Set<symbol>();
345
+ const pendingObservability = new Set<symbol>();
321
346
  const deliveries: Delivery[] = [];
322
347
  let refill = false;
323
348
  let dispatchFailed = false;
@@ -345,15 +370,16 @@ export function startRunLedgerOutboxRecovery(
345
370
  completed = attempted = projected = retrying = blocked = 0;
346
371
  };
347
372
 
348
- const launch = () => {
373
+ const launch = (observability = false) => {
349
374
  const id = Symbol();
350
375
  pending.add(id);
376
+ if (observability) pendingObservability.add(id);
351
377
  const settle = (result: Delivery['result']) => {
352
378
  if (result.status === 'rejected' && !dispatchFailed) {
353
379
  dispatchFailed = true;
354
380
  scheduleFailureRetry('dispatch', result.reason);
355
381
  }
356
- deliveries.push({ id, result });
382
+ deliveries.push({ id, observability, result });
357
383
  wakeActiveDrain?.();
358
384
  };
359
385
  // Subscribe once per delivery. Repeated Promise.race calls would retain
@@ -364,26 +390,54 @@ export function startRunLedgerOutboxRecovery(
364
390
  return;
365
391
  }
366
392
  try {
367
- settle({ status: 'fulfilled', value: await dispatchOnce() });
393
+ settle({
394
+ status: 'fulfilled',
395
+ value: await dispatchOnce(observability),
396
+ });
368
397
  } catch (reason) {
369
398
  settle({ status: 'rejected', reason });
370
399
  }
371
400
  });
372
401
  };
373
402
 
403
+ const canProbeLifecycle = () =>
404
+ !closed &&
405
+ !dispatchFailed &&
406
+ requested &&
407
+ pending.size < parallelism &&
408
+ pending.size - pendingObservability.size < lifecycleParallelism;
409
+ const canProbeObservability = () =>
410
+ !closed &&
411
+ !dispatchFailed &&
412
+ observabilityRequested &&
413
+ pending.size < parallelism &&
414
+ pendingObservability.size < observabilityParallelism;
415
+
374
416
  for (;;) {
375
417
  if (!closed && !dispatchFailed) {
376
418
  // A wake grants one indexed probe, including when other slots are
377
419
  // still delivering. Keep it pending when full so an empty result
378
420
  // cannot consume a notification that arrived after its query began.
379
- if (requested && pending.size < parallelism) {
421
+ if (canProbeObservability()) {
422
+ observabilityRequested = false;
423
+ while (
424
+ pendingObservability.size < observabilityParallelism &&
425
+ pending.size < parallelism
426
+ )
427
+ launch(true);
428
+ }
429
+ if (canProbeLifecycle()) {
380
430
  requested = false;
381
431
  launch();
382
432
  }
383
433
  // Productive completion permits immediate use of every free slot.
384
434
  // Empty results grant no refill, so idle work cannot spin on scans.
385
435
  if (refill) {
386
- while (pending.size < parallelism) launch();
436
+ while (
437
+ pending.size < parallelism &&
438
+ pending.size - pendingObservability.size < lifecycleParallelism
439
+ )
440
+ launch();
387
441
  refill = false;
388
442
  }
389
443
  }
@@ -398,10 +452,8 @@ export function startRunLedgerOutboxRecovery(
398
452
  wakeActiveDrain = resolve;
399
453
  if (
400
454
  deliveries.length > 0 ||
401
- (requested &&
402
- !closed &&
403
- !dispatchFailed &&
404
- pending.size < parallelism)
455
+ canProbeLifecycle() ||
456
+ canProbeObservability()
405
457
  )
406
458
  resolve();
407
459
  });
@@ -409,6 +461,7 @@ export function startRunLedgerOutboxRecovery(
409
461
  const delivery = deliveries.shift();
410
462
  if (!delivery) continue;
411
463
  pending.delete(delivery.id);
464
+ pendingObservability.delete(delivery.id);
412
465
  if (delivery.result.status !== 'fulfilled') {
413
466
  // Stop new claims but settle every already-started delivery before
414
467
  // cooldown or close can hand ownership to another drain.
@@ -423,7 +476,9 @@ export function startRunLedgerOutboxRecovery(
423
476
  if (completed >= parallelism) reportDeliveries();
424
477
  // Retryable rows own durable retry clocks. The SQL claim keeps their
425
478
  // run ordered while successful slots keep unrelated runs moving.
426
- refill = result.attempted > 0;
479
+ if (delivery.observability) {
480
+ if (result.attempted > 0) observabilityRequested = true;
481
+ } else refill = result.attempted > 0;
427
482
  }
428
483
  };
429
484
 
@@ -598,12 +653,37 @@ export function startRunLedgerOutboxRecoveryController(input: {
598
653
  onDelivered?: (deliveries: readonly RunLedgerOutboxDeliveredRow[]) => void;
599
654
  }): RunLedgerOutboxRecoveryController {
600
655
  const schedulerOptions = input.schedulerOptions ?? {};
601
- let dispatchOrdinal = 0;
602
- let searchInFlight = 0;
603
- const maxSearchInFlight = Math.max(
604
- 1,
605
- Math.floor(RUN_LEDGER_OUTBOX_RECOVERY_PRODUCTION_IN_FLIGHT / 2),
606
- );
656
+ const observabilityPoolOptions: RuntimeSchedulerPoolOptions = {
657
+ ...input.poolOptions,
658
+ purpose: 'observability',
659
+ applicationName: 'deepline-runtime-observability',
660
+ maxClients: RUN_LEDGER_OUTBOX_RECOVERY_INDEX_IN_FLIGHT,
661
+ };
662
+ const dispatch = (
663
+ headPriority: RunLedgerOutboxHeadPriority,
664
+ searchOnly: boolean,
665
+ ) =>
666
+ dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
667
+ (operation) =>
668
+ withRuntimeSchedulerClient(
669
+ searchOnly ? observabilityPoolOptions : input.poolOptions,
670
+ operation,
671
+ ),
672
+ {
673
+ tokenAuthority: 'gateway',
674
+ materializeObservability: true,
675
+ searchOnly,
676
+ withObservabilityClient: (operation) =>
677
+ withRuntimeSchedulerClient(observabilityPoolOptions, operation),
678
+ limit: 8,
679
+ headPriority,
680
+ // The lease holder delivers a run's terminal committed during its
681
+ // append, instead of leaving it for the next wave.
682
+ continueThroughTerminal: true,
683
+ ...(input.onDelivered ? { onDelivered: input.onDelivered } : {}),
684
+ },
685
+ schedulerOptions,
686
+ );
607
687
  return startRunLedgerOutboxRecovery({
608
688
  acquireNotifyHub: () =>
609
689
  input.useDirectNotify === false
@@ -614,33 +694,9 @@ export function startRunLedgerOutboxRecoveryController(input: {
614
694
  schedulerOptions,
615
695
  }),
616
696
  ),
617
- dispatchOnce: async (headPriority) => {
618
- const searchOnly =
619
- dispatchOrdinal++ % 2 === 1 && searchInFlight < maxSearchInFlight;
620
- if (searchOnly) searchInFlight += 1;
621
- try {
622
- return await dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
623
- (operation) =>
624
- withRuntimeSchedulerClient(input.poolOptions, operation),
625
- {
626
- tokenAuthority: 'gateway',
627
- materializeObservability: true,
628
- // One durable controller, with half its capacity reserved for
629
- // lifecycle delivery even when every search operation is blocked.
630
- searchOnly,
631
- limit: 8,
632
- headPriority,
633
- // The lease holder delivers a run's terminal committed during its
634
- // append, instead of leaving it for the next wave.
635
- continueThroughTerminal: true,
636
- ...(input.onDelivered ? { onDelivered: input.onDelivered } : {}),
637
- },
638
- schedulerOptions,
639
- );
640
- } finally {
641
- if (searchOnly) searchInFlight -= 1;
642
- }
643
- },
697
+ dispatchOnce: (headPriority) => dispatch(headPriority, false),
698
+ dispatchObservabilityOnce: () => dispatch('oldest_ready', true),
699
+ observabilityParallelism: RUN_LEDGER_OUTBOX_RECOVERY_INDEX_IN_FLIGHT,
644
700
  // `dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope` releases each
645
701
  // short scheduler checkout before it calls the Vercel gateway. The two
646
702
  // replicas therefore drain up to sixteen independent runs concurrently