Skip to content

Commit f511dde

Browse files
committed
Merge remote-tracking branch 'origin/staging' into fix/chat-code-not-copied-inline
2 parents e0b6404 + cd935e8 commit f511dde

194 files changed

Lines changed: 27843 additions & 1022 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"type": "module",
1010
"main": "dist/main.cjs",
1111
"engines": {
12-
"bun": ">=1.2.13",
12+
"bun": ">=1.3.14",
1313
"node": ">=20.0.0"
1414
},
1515
"scripts": {

apps/docs/content/docs/en/cli/commands.mdx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,29 @@ sim configure [options]
113113
| `--unset <key...>` | No | Remove settings (endpoint, workspace, output). |
114114

115115
</CommandTable>
116+
117+
## Ask Sim and print the reply
118+
119+
```bash
120+
sim chat <message> [options]
121+
```
122+
123+
**Arguments**
124+
125+
<CommandTable>
126+
127+
| Argument | Required | Description |
128+
| --- | --- | --- |
129+
| `message` | Yes | What to ask Sim |
130+
131+
</CommandTable>
132+
133+
**Options**
134+
135+
<CommandTable>
136+
137+
| Option | Required | Description |
138+
| --- | --- | --- |
139+
| `-c, --conversation <id>` | No | Continue the conversation with this ID. |
140+
141+
</CommandTable>

apps/docs/content/docs/en/cli/reference.mdx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,34 @@ sim configure [options]
101101

102102
</CommandTable>
103103

104+
## sim chat
105+
106+
Ask Sim and print the reply
107+
108+
```bash
109+
sim chat <message> [options]
110+
```
111+
112+
**Arguments**
113+
114+
<CommandTable>
115+
116+
| Argument | Required | Description |
117+
| --- | --- | --- |
118+
| `message` | Yes | What to ask Sim |
119+
120+
</CommandTable>
121+
122+
**Options**
123+
124+
<CommandTable>
125+
126+
| Option | Required | Description |
127+
| --- | --- | --- |
128+
| `-c, --conversation <id>` | No | Continue the conversation with this ID. |
129+
130+
</CommandTable>
131+
104132
## sim profiles
105133

106134
Also spelled `sim profile`.

apps/realtime/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"license": "Apache-2.0",
66
"type": "module",
77
"engines": {
8-
"bun": ">=1.2.13",
8+
"bun": ">=1.3.14",
99
"node": ">=20.0.0"
1010
},
1111
"scripts": {

apps/sim/app/api/cron/cleanup-stale-executions/route.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,8 +396,9 @@ describe('stale execution cleanup deadline grace', () => {
396396
const response = await GET(createRequest())
397397

398398
expect(response.status).toBe(200)
399-
expect(dbChainMockFns.transaction).toHaveBeenCalledTimes(8)
400-
expect(dbChainMockFns.for).toHaveBeenCalledTimes(8)
399+
// Nine batched arms: the connector sync-log retention pass is the newest.
400+
expect(dbChainMockFns.transaction).toHaveBeenCalledTimes(9)
401+
expect(dbChainMockFns.for).toHaveBeenCalledTimes(9)
401402
for (const [strength, options] of dbChainMockFns.for.mock.calls) {
402403
expect(strength).toBe('update')
403404
expect(options).toEqual({ skipLocked: true })
@@ -469,7 +470,7 @@ describe('stale execution cleanup deadline grace', () => {
469470
const limits = dbChainMockFns.limit.mock.calls.map(([limit]) => limit)
470471
expect(limits.filter((limit) => limit === 100)).toHaveLength(20)
471472
expect(limits.filter((limit) => limit === 1000)).toHaveLength(30)
472-
expect(limits.filter((limit) => limit === 2000)).toHaveLength(11)
473+
expect(limits.filter((limit) => limit === 2000)).toHaveLength(12)
473474

474475
const workflowUpdates = dbChainMockFns.update.mock.calls.filter(
475476
([table]) => table === workflowExecutionLogs

apps/sim/app/api/cron/cleanup-stale-executions/route.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { db } from '@sim/db'
22
import {
33
asyncJobs,
4+
knowledgeConnectorSyncLog,
45
tableJobs,
56
workflowDeploymentOperation,
67
workflowExecutionLogs,
@@ -31,6 +32,7 @@ import {
3132
STALE_SWEEPABLE_EXECUTION_STATUSES,
3233
type StaleSweepableExecutionStatus,
3334
} from '@/lib/logs/types'
35+
import { cancelStaleDispatches } from '@/lib/table/dispatcher'
3436
import { deleteFile } from '@/lib/uploads/core/storage-service'
3537
import {
3638
carrierNotIrrecoverableSql,
@@ -52,12 +54,33 @@ const EXECUTION_DEADLINE_ERROR = getTimeoutErrorMessage(undefined)
5254
const TABLE_JOB_STALE_THRESHOLD_MINUTES = 95
5355
/** Terminal table-jobs older than this are pruned; only the latest job per table is ever read. */
5456
const TABLE_JOB_RETENTION_HOURS = 24
57+
/**
58+
* A table run dispatch whose holder has not made progress for this long is
59+
* treated as dead. Same shape and window as the table-job threshold above: the
60+
* 90-minute Trigger.dev task ceiling (`maxDuration` in `trigger.config.ts`) plus
61+
* five minutes of cleanup grace, measured from the dispatcher's own per-window
62+
* heartbeat rather than from when the run was requested.
63+
*/
64+
const TABLE_DISPATCH_STALE_THRESHOLD_MINUTES = 95
65+
/** Per-run ceiling on reaped dispatches, so one tick cannot fan out unbounded SSE. */
66+
const TABLE_DISPATCH_MAX_PER_RUN = 200
5567
/**
5668
* Terminal deployment operations older than this are pruned. Every reader of
5769
* this table is latest-generation-only, and idempotency keys only need to
5870
* survive a client retry window, so 30 days is generous.
5971
*/
6072
const DEPLOYMENT_OPERATION_RETENTION_DAYS = 30
73+
/**
74+
* Terminal connector sync logs older than this are pruned. Nothing pruned them
75+
* before, so the table grew by one row per sync run forever — a connector on a
76+
* fifteen-minute interval writes about 35,000 rows a year on its own. That cost
77+
* lands on `loadPreviousListingObservation`, which reads the newest `completed`
78+
* row per connector through an index covering `connector_id` alone, so every
79+
* retained row makes the sort behind the deletion-safety corroboration slower.
80+
*/
81+
const CONNECTOR_SYNC_LOG_RETENTION_DAYS = 30
82+
const CONNECTOR_SYNC_LOG_PRUNE_BATCH_SIZE = 2000
83+
const CONNECTOR_SYNC_LOG_MAX_ROWS_PER_RUN = 20_000
6184
const DEPLOYMENT_OPERATION_PRUNE_BATCH_SIZE = 2000
6285
const DEPLOYMENT_OPERATION_PRUNE_MAX_BATCHES = 10
6386
const WORKFLOW_EXECUTION_MUTATION_BATCH_SIZE = 100
@@ -144,6 +167,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
144167
const staleTableJobThreshold = new Date(
145168
now.getTime() - TABLE_JOB_STALE_THRESHOLD_MINUTES * 60 * 1000
146169
)
170+
const staleDispatchThreshold = new Date(
171+
now.getTime() - TABLE_DISPATCH_STALE_THRESHOLD_MINUTES * 60 * 1000
172+
)
147173

148174
let staleExecutionsFound = 0
149175
let cleaned = 0
@@ -538,6 +564,90 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
538564
})
539565
}
540566

567+
/**
568+
* Prune terminal connector sync logs past retention.
569+
*
570+
* HARD INVARIANT: the newest row per connector must survive, and so must the
571+
* newest `completed` row. `loadPreviousListingObservation` reconstructs the
572+
* previous listing from the latest `completed` log, and that reconstruction
573+
* decides whether a suspect listing is corroborated — i.e. whether
574+
* reconciliation may delete documents. Pruning the last `completed` row
575+
* would silently change deletion behaviour, so both `exists` guards below
576+
* are load-bearing rather than defensive.
577+
*
578+
* `started` rows are never eligible: they are either in flight or waiting on
579+
* the scheduler's own sweep to close them.
580+
*/
581+
let connectorSyncLogsPruned = 0
582+
try {
583+
const syncLogRetention = new Date(
584+
Date.now() - CONNECTOR_SYNC_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000
585+
)
586+
const newerSyncLog = alias(knowledgeConnectorSyncLog, 'newer_sync_log')
587+
const newerCompletedSyncLog = alias(knowledgeConnectorSyncLog, 'newer_completed_sync_log')
588+
const syncLogPredicate = and(
589+
inArray(knowledgeConnectorSyncLog.status, ['completed', 'failed']),
590+
lt(knowledgeConnectorSyncLog.startedAt, syncLogRetention),
591+
exists(
592+
db
593+
.select({ id: newerSyncLog.id })
594+
.from(newerSyncLog)
595+
.where(
596+
and(
597+
eq(newerSyncLog.connectorId, knowledgeConnectorSyncLog.connectorId),
598+
gt(newerSyncLog.startedAt, knowledgeConnectorSyncLog.startedAt)
599+
)
600+
)
601+
),
602+
or(
603+
ne(knowledgeConnectorSyncLog.status, 'completed'),
604+
exists(
605+
db
606+
.select({ id: newerCompletedSyncLog.id })
607+
.from(newerCompletedSyncLog)
608+
.where(
609+
and(
610+
eq(newerCompletedSyncLog.connectorId, knowledgeConnectorSyncLog.connectorId),
611+
eq(newerCompletedSyncLog.status, 'completed'),
612+
gt(newerCompletedSyncLog.startedAt, knowledgeConnectorSyncLog.startedAt)
613+
)
614+
)
615+
)
616+
)
617+
)
618+
const syncLogResult = await runBatchedMutation({
619+
batchSize: CONNECTOR_SYNC_LOG_PRUNE_BATCH_SIZE,
620+
maxRowsPerRun: CONNECTOR_SYNC_LOG_MAX_ROWS_PER_RUN,
621+
claim: (tx, limit) =>
622+
tx
623+
.select({ id: knowledgeConnectorSyncLog.id })
624+
.from(knowledgeConnectorSyncLog)
625+
.where(syncLogPredicate)
626+
.limit(limit)
627+
.for('update', { skipLocked: true }),
628+
mutation: (tx, candidateIds) =>
629+
tx
630+
.delete(knowledgeConnectorSyncLog)
631+
.where(inArray(knowledgeConnectorSyncLog.id, candidateIds))
632+
.returning({ id: knowledgeConnectorSyncLog.id }),
633+
})
634+
connectorSyncLogsPruned = syncLogResult.affected
635+
if (connectorSyncLogsPruned > 0) {
636+
logger.info(
637+
`Pruned ${connectorSyncLogsPruned} old connector sync logs (retention: ${CONNECTOR_SYNC_LOG_RETENTION_DAYS}d)`
638+
)
639+
}
640+
if (syncLogResult.reachedLimit) {
641+
logger.info('Deferred remaining connector sync logs after reaching the per-run cap', {
642+
maxRowsPerRun: CONNECTOR_SYNC_LOG_MAX_ROWS_PER_RUN,
643+
})
644+
}
645+
} catch (error) {
646+
logger.error('Failed to prune old connector sync logs:', {
647+
error: toError(error).message,
648+
})
649+
}
650+
541651
/**
542652
* Prune terminal deployment operations past retention. HARD INVARIANT:
543653
* the newest-generation row per workflow must always survive — the next
@@ -604,6 +714,29 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
604714
})
605715
}
606716

717+
/**
718+
* Cancel table run dispatches abandoned by a dead dispatcher. Nothing else
719+
* reclaims them — every other terminal transition is user- or flow-initiated
720+
* — so a dispatcher killed mid-loop left the row `dispatching` forever and
721+
* the client's "X running" overlay with it. Ages from the dispatcher's
722+
* per-window heartbeat, so a slow-but-live dispatch is spared.
723+
*/
724+
let staleDispatchesCancelled = 0
725+
try {
726+
staleDispatchesCancelled = (
727+
await cancelStaleDispatches(staleDispatchThreshold, TABLE_DISPATCH_MAX_PER_RUN)
728+
).length
729+
if (staleDispatchesCancelled > 0) {
730+
logger.warn(`Cancelled ${staleDispatchesCancelled} abandoned table run dispatches`, {
731+
thresholdMinutes: TABLE_DISPATCH_STALE_THRESHOLD_MINUTES,
732+
})
733+
}
734+
} catch (error) {
735+
logger.error('Failed to cancel abandoned table run dispatches:', {
736+
error: toError(error).message,
737+
})
738+
}
739+
607740
return NextResponse.json({
608741
success: true,
609742
executions: {
@@ -622,6 +755,14 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
622755
tableJobs: {
623756
staleMarkedFailed: staleTableJobsMarkedFailed,
624757
},
758+
connectorSyncLogs: {
759+
pruned: connectorSyncLogsPruned,
760+
retentionDays: CONNECTOR_SYNC_LOG_RETENTION_DAYS,
761+
},
762+
tableRunDispatches: {
763+
staleCancelled: staleDispatchesCancelled,
764+
thresholdMinutes: TABLE_DISPATCH_STALE_THRESHOLD_MINUTES,
765+
},
625766
deploymentOperations: {
626767
pruned: deploymentOperationsPruned,
627768
retentionDays: DEPLOYMENT_OPERATION_RETENTION_DAYS,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Pins what `maxAttempts` *means*, using the real AWS SDK.
3+
*
4+
* `route.test.ts` asserts the route configures `maxAttempts: 1`; on its own that
5+
* only pins a number. This file counts how many datapoints a real
6+
* `CloudWatchClient` actually hands to a peer that accepts the request and then
7+
* dies before writing a response byte -- the ambiguous transport failure the
8+
* SDK's retry layer replays, and the one that makes CloudWatch aggregate a
9+
* duplicate. It fails if a future SDK bump changes the default budget or stops
10+
* honouring the pin.
11+
*
12+
* Deliberately not mocking `@aws-sdk/client-cloudwatch` here: the SDK's retry
13+
* middleware is the subject under test, so it must be the real one.
14+
*
15+
* @vitest-environment node
16+
*/
17+
import http from 'node:http'
18+
import type { AddressInfo } from 'node:net'
19+
import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch'
20+
import { describe, expect, it } from 'vitest'
21+
22+
/** `@smithy/util-retry`'s `DEFAULT_MAX_ATTEMPTS`, which every unpinned client inherits. */
23+
const SDK_DEFAULT_MAX_ATTEMPTS = 3
24+
25+
async function countDeliveries(clientConfig: Record<string, unknown>): Promise<number> {
26+
let received = 0
27+
const server = http.createServer((req, res) => {
28+
req.on('data', () => {})
29+
req.on('end', () => {
30+
if (String(req.headers['x-amz-target'] ?? '').endsWith('PutMetricData')) {
31+
received++
32+
req.socket.destroy()
33+
return
34+
}
35+
res.writeHead(200, { 'content-type': 'application/x-amz-json-1.0' })
36+
res.end('{}')
37+
})
38+
})
39+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
40+
const { port } = server.address() as AddressInfo
41+
42+
const client = new CloudWatchClient({
43+
region: 'us-east-1',
44+
endpoint: `http://127.0.0.1:${port}`,
45+
credentials: { accessKeyId: 'AKIAEXAMPLE', secretAccessKey: 'secret' },
46+
...clientConfig,
47+
})
48+
49+
try {
50+
await client.send(
51+
new PutMetricDataCommand({
52+
Namespace: 'Sim/Test',
53+
MetricData: [{ MetricName: 'Requests', Value: 1 }],
54+
})
55+
)
56+
} catch {
57+
/* Every attempt fails by design; the delivery count is the assertion. */
58+
} finally {
59+
client.destroy()
60+
await new Promise<void>((resolve) => server.close(() => resolve()))
61+
}
62+
return received
63+
}
64+
65+
describe('aws sdk retry semantics for PutMetricData', () => {
66+
it('delivers the datapoint exactly once when maxAttempts is pinned to 1', async () => {
67+
await expect(countDeliveries({ maxAttempts: 1 })).resolves.toBe(1)
68+
})
69+
70+
it('aggregates a duplicate for every retry the default budget allows', async () => {
71+
await expect(countDeliveries({})).resolves.toBe(SDK_DEFAULT_MAX_ATTEMPTS)
72+
})
73+
})

0 commit comments

Comments
 (0)