@parall/agent-core 1.32.1 → 1.34.0

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.
@@ -106,6 +106,8 @@ export declare class ParallAgentGateway {
106
106
  private handleScheduleFire;
107
107
  private fetchAndHandleApprovalDecided;
108
108
  private catchUpFromDispatch;
109
+ private buildOfflineSummary;
110
+ private formatAge;
109
111
  private handleHello;
110
112
  private waitForDrain;
111
113
  private shutdown;
@@ -1 +1 @@
1
- {"version":3,"file":"gateway-base.d.ts","sourceRoot":"","sources":["../src/gateway-base.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gateway-base.d.ts","sourceRoot":"","sources":["../src/gateway-base.ts"],"names":[],"mappings":"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"}
@@ -1610,8 +1610,29 @@ export class ParallAgentGateway {
1610
1610
  return dispatched;
1611
1611
  }
1612
1612
  async catchUpFromDispatch() {
1613
+ const OVERFLOW_THRESHOLD = 500;
1614
+ const EXPIRE_AGE_HOURS = 24;
1615
+ const CATCHUP_MAX = 10;
1616
+ let expireSummary = null;
1617
+ let overflowMode = false;
1618
+ try {
1619
+ const { count } = await this.opts.client.getDispatchPendingCount(this.opts.config.org_id);
1620
+ if (count > OVERFLOW_THRESHOLD) {
1621
+ overflowMode = true;
1622
+ const result = await this.opts.client.expireDispatch(this.opts.config.org_id, EXPIRE_AGE_HOURS);
1623
+ if (result.total > 0) {
1624
+ this.opts.log?.warn(`dispatch catch-up overflow: ${count} pending, expired ${result.total} events older than ${EXPIRE_AGE_HOURS}h`);
1625
+ expireSummary = this.buildOfflineSummary(result.groups);
1626
+ }
1627
+ }
1628
+ }
1629
+ catch (err) {
1630
+ this.opts.log?.warn(`dispatch catch-up overflow check failed: ${String(err)}`);
1631
+ }
1613
1632
  let cursor;
1614
1633
  let processed = 0;
1634
+ let skipped = 0;
1635
+ const skippedByType = new Map();
1615
1636
  do {
1616
1637
  const page = await this.opts.client.getDispatch(this.opts.config.org_id, {
1617
1638
  limit: 50,
@@ -1622,6 +1643,18 @@ export class ParallAgentGateway {
1622
1643
  // will only reject. Items remain unacked for the replacement pod.
1623
1644
  if (this.shuttingDown)
1624
1645
  break;
1646
+ if (overflowMode && processed >= CATCHUP_MAX) {
1647
+ try {
1648
+ await this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id);
1649
+ skipped++;
1650
+ const key = item.event_type;
1651
+ skippedByType.set(key, (skippedByType.get(key) ?? 0) + 1);
1652
+ }
1653
+ catch {
1654
+ this.opts.log?.warn(`catch-up skip ack failed for ${item.id}, leaving pending`);
1655
+ }
1656
+ continue;
1657
+ }
1625
1658
  processed++;
1626
1659
  try {
1627
1660
  let dispatched = false;
@@ -1706,10 +1739,54 @@ export class ParallAgentGateway {
1706
1739
  // and the next page would be wasted API work the replacement pod redoes.
1707
1740
  cursor = !this.shuttingDown && page.has_more ? page.next_cursor : undefined;
1708
1741
  } while (cursor);
1709
- if (processed > 0) {
1710
- this.opts.log?.info(`dispatch catch-up: processed ${processed}`);
1742
+ if (expireSummary || skipped > 0) {
1743
+ const parts = [];
1744
+ if (expireSummary)
1745
+ parts.push(expireSummary);
1746
+ if (skipped > 0) {
1747
+ const recentLines = [...skippedByType.entries()]
1748
+ .map(([type, count]) => `- ${count} recent ${type.replace('.', ' ').replace('_', ' ')} events`)
1749
+ .join('\n');
1750
+ parts.push(expireSummary
1751
+ ? `Additionally, ${skipped} recent events exceeded the catch-up limit and were skipped:\n${recentLines}`
1752
+ : `[Offline Summary] You were offline. ${skipped} pending events exceeded the catch-up limit and were skipped:\n${recentLines}\n\nUse your tools (read messages, check tasks) to review anything still relevant.`);
1753
+ }
1754
+ const fullSummary = parts.join('\n\n');
1755
+ this.pendingRestartNotification = this.pendingRestartNotification
1756
+ ? `${this.pendingRestartNotification}\n\n${fullSummary}`
1757
+ : fullSummary;
1758
+ }
1759
+ if (processed > 0 || skipped > 0) {
1760
+ this.opts.log?.info(`dispatch catch-up: processed ${processed}, skipped ${skipped}`);
1711
1761
  }
1712
1762
  }
1763
+ buildOfflineSummary(groups) {
1764
+ const lines = [
1765
+ '[Offline Summary] You were offline and had a large backlog of pending events. The following old events were skipped:',
1766
+ ];
1767
+ for (const g of groups) {
1768
+ const age = this.formatAge(g.oldest);
1769
+ if (g.chat_id && g.event_type === 'message') {
1770
+ const name = g.chat_name ?? g.chat_id;
1771
+ lines.push(`- ${g.count} messages in "${name}" (oldest: ${age})`);
1772
+ }
1773
+ else {
1774
+ const label = g.event_type.replace('.', ' ').replace('_', ' ');
1775
+ lines.push(`- ${g.count} ${label} events (oldest: ${age})`);
1776
+ }
1777
+ }
1778
+ lines.push('');
1779
+ lines.push('These events were too old to process individually. Use your tools (read messages, check tasks) to review anything still relevant.');
1780
+ return lines.join('\n');
1781
+ }
1782
+ formatAge(iso) {
1783
+ const ms = Date.now() - new Date(iso).getTime();
1784
+ const hours = Math.floor(ms / 3_600_000);
1785
+ if (hours < 24)
1786
+ return `${hours}h ago`;
1787
+ const days = Math.floor(hours / 24);
1788
+ return `${days}d ago`;
1789
+ }
1713
1790
  async handleHello(data) {
1714
1791
  const { client, config, log } = this.opts;
1715
1792
  this.sessionId = data.session_id ?? '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/agent-core",
3
- "version": "1.32.1",
3
+ "version": "1.34.0",
4
4
  "description": "Shared agent runtime orchestration helpers for Parall",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "@opentelemetry/sdk-logs": "^0.57.0",
36
36
  "@opentelemetry/sdk-metrics": "^1.30.0",
37
37
  "@opentelemetry/sdk-trace-node": "^1.30.0",
38
- "@parall/sdk": "1.32.1"
38
+ "@parall/sdk": "1.34.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^22.0.0",
@@ -1979,8 +1979,34 @@ export class ParallAgentGateway {
1979
1979
  }
1980
1980
 
1981
1981
  private async catchUpFromDispatch() {
1982
+ const OVERFLOW_THRESHOLD = 500;
1983
+ const EXPIRE_AGE_HOURS = 24;
1984
+ const CATCHUP_MAX = 10;
1985
+ let expireSummary: string | null = null;
1986
+ let overflowMode = false;
1987
+
1988
+ try {
1989
+ const { count } = await this.opts.client.getDispatchPendingCount(this.opts.config.org_id);
1990
+ if (count > OVERFLOW_THRESHOLD) {
1991
+ overflowMode = true;
1992
+ const result = await this.opts.client.expireDispatch(
1993
+ this.opts.config.org_id,
1994
+ EXPIRE_AGE_HOURS,
1995
+ );
1996
+ if (result.total > 0) {
1997
+ this.opts.log?.warn(
1998
+ `dispatch catch-up overflow: ${count} pending, expired ${result.total} events older than ${EXPIRE_AGE_HOURS}h`,
1999
+ );
2000
+ expireSummary = this.buildOfflineSummary(result.groups);
2001
+ }
2002
+ }
2003
+ } catch (err) {
2004
+ this.opts.log?.warn(`dispatch catch-up overflow check failed: ${String(err)}`);
2005
+ }
1982
2006
  let cursor: string | undefined;
1983
2007
  let processed = 0;
2008
+ let skipped = 0;
2009
+ const skippedByType = new Map<string, number>();
1984
2010
 
1985
2011
  do {
1986
2012
  const page = await this.opts.client.getDispatch(this.opts.config.org_id, {
@@ -1993,6 +2019,18 @@ export class ParallAgentGateway {
1993
2019
  // will only reject. Items remain unacked for the replacement pod.
1994
2020
  if (this.shuttingDown) break;
1995
2021
 
2022
+ if (overflowMode && processed >= CATCHUP_MAX) {
2023
+ try {
2024
+ await this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id);
2025
+ skipped++;
2026
+ const key = item.event_type;
2027
+ skippedByType.set(key, (skippedByType.get(key) ?? 0) + 1);
2028
+ } catch {
2029
+ this.opts.log?.warn(`catch-up skip ack failed for ${item.id}, leaving pending`);
2030
+ }
2031
+ continue;
2032
+ }
2033
+
1996
2034
  processed++;
1997
2035
  try {
1998
2036
  let dispatched = false;
@@ -2096,9 +2134,69 @@ export class ParallAgentGateway {
2096
2134
  cursor = !this.shuttingDown && page.has_more ? page.next_cursor : undefined;
2097
2135
  } while (cursor);
2098
2136
 
2099
- if (processed > 0) {
2100
- this.opts.log?.info(`dispatch catch-up: processed ${processed}`);
2137
+ if (expireSummary || skipped > 0) {
2138
+ const parts: string[] = [];
2139
+ if (expireSummary) parts.push(expireSummary);
2140
+ if (skipped > 0) {
2141
+ const recentLines = [...skippedByType.entries()]
2142
+ .map(
2143
+ ([type, count]) =>
2144
+ `- ${count} recent ${type.replace('.', ' ').replace('_', ' ')} events`,
2145
+ )
2146
+ .join('\n');
2147
+ parts.push(
2148
+ expireSummary
2149
+ ? `Additionally, ${skipped} recent events exceeded the catch-up limit and were skipped:\n${recentLines}`
2150
+ : `[Offline Summary] You were offline. ${skipped} pending events exceeded the catch-up limit and were skipped:\n${recentLines}\n\nUse your tools (read messages, check tasks) to review anything still relevant.`,
2151
+ );
2152
+ }
2153
+ const fullSummary = parts.join('\n\n');
2154
+ this.pendingRestartNotification = this.pendingRestartNotification
2155
+ ? `${this.pendingRestartNotification}\n\n${fullSummary}`
2156
+ : fullSummary;
2101
2157
  }
2158
+
2159
+ if (processed > 0 || skipped > 0) {
2160
+ this.opts.log?.info(`dispatch catch-up: processed ${processed}, skipped ${skipped}`);
2161
+ }
2162
+ }
2163
+
2164
+ private buildOfflineSummary(
2165
+ groups: Array<{
2166
+ chat_id: string | null;
2167
+ chat_name?: string | null;
2168
+ event_type: string;
2169
+ count: number;
2170
+ oldest: string;
2171
+ newest: string;
2172
+ }>,
2173
+ ): string {
2174
+ const lines: string[] = [
2175
+ '[Offline Summary] You were offline and had a large backlog of pending events. The following old events were skipped:',
2176
+ ];
2177
+ for (const g of groups) {
2178
+ const age = this.formatAge(g.oldest);
2179
+ if (g.chat_id && g.event_type === 'message') {
2180
+ const name = g.chat_name ?? g.chat_id;
2181
+ lines.push(`- ${g.count} messages in "${name}" (oldest: ${age})`);
2182
+ } else {
2183
+ const label = g.event_type.replace('.', ' ').replace('_', ' ');
2184
+ lines.push(`- ${g.count} ${label} events (oldest: ${age})`);
2185
+ }
2186
+ }
2187
+ lines.push('');
2188
+ lines.push(
2189
+ 'These events were too old to process individually. Use your tools (read messages, check tasks) to review anything still relevant.',
2190
+ );
2191
+ return lines.join('\n');
2192
+ }
2193
+
2194
+ private formatAge(iso: string): string {
2195
+ const ms = Date.now() - new Date(iso).getTime();
2196
+ const hours = Math.floor(ms / 3_600_000);
2197
+ if (hours < 24) return `${hours}h ago`;
2198
+ const days = Math.floor(hours / 24);
2199
+ return `${days}d ago`;
2102
2200
  }
2103
2201
 
2104
2202
  private async handleHello(data: HelloData) {