@evident-ai/runner-cdk 3.5.2-dev.507f0a3 → 3.5.2-dev.7130f36
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/evident-scale-to-zero-construct.js +7 -0
- package/dist/microvm/construct.js +9 -7
- package/dist/microvm-image-context/hooks/common.sh +59 -5
- package/dist/microvm-image-context/hooks/session-db-maintenance.mjs +69 -0
- package/dist/microvm-image-context/hooks/terminate +2 -0
- package/package.json +1 -1
|
@@ -157,6 +157,13 @@ class EvidentScaleToZeroConstruct extends constructs_1.Construct {
|
|
|
157
157
|
// Object Get/Put/Delete scoped to this agent's prefix. DeleteObject is
|
|
158
158
|
// required: Litestream's retention/compaction prunes old LTX files.
|
|
159
159
|
replicaBucket.grantReadWrite(taskRole, `${this.replicaPrefix}/*`);
|
|
160
|
+
// Vault credentials are read and conditionally written, so this shared prefix
|
|
161
|
+
// needs no delete or list permission; conditional writes use request headers.
|
|
162
|
+
taskRole.addToPrincipalPolicy(new iam.PolicyStatement({
|
|
163
|
+
sid: 'ReadWriteCredentialVaultObjects',
|
|
164
|
+
actions: ['s3:GetObject', 's3:PutObject'],
|
|
165
|
+
resources: [replicaBucket.arnForObjects('credential-vaults/*')],
|
|
166
|
+
}));
|
|
160
167
|
this.service = new ecs.FargateService(this, 'Service', {
|
|
161
168
|
cluster,
|
|
162
169
|
taskDefinition,
|
|
@@ -37,6 +37,7 @@ exports.EvidentMicrovmConstruct = void 0;
|
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
const cdk = __importStar(require("aws-cdk-lib"));
|
|
39
39
|
const constructs_1 = require("constructs");
|
|
40
|
+
const iam = __importStar(require("aws-cdk-lib/aws-iam"));
|
|
40
41
|
const lambda = __importStar(require("aws-cdk-lib/aws-lambda"));
|
|
41
42
|
const s3 = __importStar(require("aws-cdk-lib/aws-s3"));
|
|
42
43
|
const lambda_microvm_cdk_1 = require("@evident-ai/lambda-microvm-cdk");
|
|
@@ -189,13 +190,14 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
|
189
190
|
hooks: imageHooks,
|
|
190
191
|
}));
|
|
191
192
|
}
|
|
192
|
-
//
|
|
193
|
-
// the
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
193
|
+
// The execution role serves every image shape, so per-session state is scoped
|
|
194
|
+
// to the `runners/` root; vault credentials need only GetObject and PutObject.
|
|
195
|
+
durableState.grantReadWrite(defaultImage.executionRole, 'runners/*');
|
|
196
|
+
defaultImage.executionRole.addToPrincipalPolicy(new iam.PolicyStatement({
|
|
197
|
+
sid: 'ReadWriteCredentialVaultObjects',
|
|
198
|
+
actions: ['s3:GetObject', 's3:PutObject'],
|
|
199
|
+
resources: [durableState.arnForObjects('credential-vaults/*')],
|
|
200
|
+
}));
|
|
199
201
|
// The image receives only an ARN; `/run` reads the secret value at runtime.
|
|
200
202
|
props.runnerSecret?.grantRead(defaultImage.executionRole);
|
|
201
203
|
// The shape catalogue the controller resolves a doorbell's `shape` field
|
|
@@ -445,6 +445,26 @@ stop_litestream() {
|
|
|
445
445
|
log "litestream stopped ${outcome}"
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
+
SESSION_DB_LITESTREAM_QUIESCED=0
|
|
449
|
+
|
|
450
|
+
quiesce_litestream_for_session_db() {
|
|
451
|
+
if [ "${SESSION_DB_LITESTREAM_QUIESCED}" -eq 1 ]; then
|
|
452
|
+
return 0
|
|
453
|
+
fi
|
|
454
|
+
|
|
455
|
+
if [ -z "${PERSISTENCE_BUCKET:-}" ] || [ -e "${SESSION_DB_NO_REPLICATE_MARKER}" ] || [ ! -s "${LITESTREAM_CONFIG_FILE}" ]; then
|
|
456
|
+
return 0
|
|
457
|
+
fi
|
|
458
|
+
|
|
459
|
+
if litestream_is_running; then
|
|
460
|
+
stop_litestream
|
|
461
|
+
else
|
|
462
|
+
warn "SESSION-DB-REPLICATOR-DIED: no live litestream at ${LITESTREAM_PID_FILE}; flushing one-shot anyway"
|
|
463
|
+
fi
|
|
464
|
+
|
|
465
|
+
SESSION_DB_LITESTREAM_QUIESCED=1
|
|
466
|
+
}
|
|
467
|
+
|
|
448
468
|
# The cheap stop: one SIGTERM, one rm, one log line, NO poll loop — `/run`'s
|
|
449
469
|
# own cleanup calls this, never stop_litestream, because that path only runs
|
|
450
470
|
# after /run has already FAILED: no session happened yet, so a graceful final
|
|
@@ -519,11 +539,7 @@ flush_session_db() {
|
|
|
519
539
|
return 0
|
|
520
540
|
fi
|
|
521
541
|
|
|
522
|
-
|
|
523
|
-
stop_litestream
|
|
524
|
-
else
|
|
525
|
-
warn "SESSION-DB-REPLICATOR-DIED: no live litestream at ${LITESTREAM_PID_FILE}; flushing one-shot anyway"
|
|
526
|
-
fi
|
|
542
|
+
quiesce_litestream_for_session_db
|
|
527
543
|
|
|
528
544
|
# Only reached with the daemon confirmed stopped, or never running — never
|
|
529
545
|
# concurrently with it (see above).
|
|
@@ -539,6 +555,44 @@ flush_session_db() {
|
|
|
539
555
|
}
|
|
540
556
|
# --- flush_session_db (end) --------------------------------------------------
|
|
541
557
|
|
|
558
|
+
SESSION_DB_MAINTENANCE_DEADLINE_SECONDS=4
|
|
559
|
+
SESSION_DB_MAINTENANCE_KILL_GRACE_SECONDS=1
|
|
560
|
+
SESSION_DB_MAINTENANCE_MAX_BYTES=268435456
|
|
561
|
+
|
|
562
|
+
maintain_session_db() {
|
|
563
|
+
if [ -z "${PERSISTENCE_BUCKET:-}" ]; then
|
|
564
|
+
warn "SESSION-DB-MAINTENANCE-SKIP: persistence is disabled"
|
|
565
|
+
return 0
|
|
566
|
+
fi
|
|
567
|
+
|
|
568
|
+
if [ -e "${SESSION_DB_NO_REPLICATE_MARKER}" ]; then
|
|
569
|
+
warn "SESSION-DB-MAINTENANCE-SKIP: this boot's session DB was not proven safe to replicate"
|
|
570
|
+
return 0
|
|
571
|
+
fi
|
|
572
|
+
|
|
573
|
+
if [ ! -s "${LITESTREAM_CONFIG_FILE}" ]; then
|
|
574
|
+
error "SESSION-DB-MAINTENANCE-SKIP: no usable ${LITESTREAM_CONFIG_FILE}"
|
|
575
|
+
return 0
|
|
576
|
+
fi
|
|
577
|
+
|
|
578
|
+
if litestream_is_running; then
|
|
579
|
+
warn "SESSION-DB-MAINTENANCE-SKIP: litestream is still running"
|
|
580
|
+
return 0
|
|
581
|
+
fi
|
|
582
|
+
|
|
583
|
+
local maintenance_rc=0
|
|
584
|
+
timeout -k "${SESSION_DB_MAINTENANCE_KILL_GRACE_SECONDS}" "${SESSION_DB_MAINTENANCE_DEADLINE_SECONDS}" \
|
|
585
|
+
node "$(dirname "${BASH_SOURCE[0]}")/session-db-maintenance.mjs" \
|
|
586
|
+
"${HOME:-/home/runner}/.local/share/opencode/opencode.db" \
|
|
587
|
+
"${SESSION_DB_MAINTENANCE_MAX_BYTES}" || maintenance_rc=$?
|
|
588
|
+
|
|
589
|
+
if [ "${maintenance_rc}" -ne 0 ]; then
|
|
590
|
+
warn "SESSION-DB-MAINTENANCE: helper exited ${maintenance_rc}; continuing teardown"
|
|
591
|
+
fi
|
|
592
|
+
|
|
593
|
+
return 0
|
|
594
|
+
}
|
|
595
|
+
|
|
542
596
|
# How long the guest CLI runs with no activity before it exits itself
|
|
543
597
|
# (`evident run --idle-timeout`), which is what
|
|
544
598
|
# turns a truly-abandoned VM into the clean-offline POST that lets Evident
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { statSync } from 'node:fs';
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
import { performance } from 'node:perf_hooks';
|
|
6
|
+
|
|
7
|
+
const [dbPath, maxBytesText] = process.argv.slice(2);
|
|
8
|
+
const maxBytes = Number(maxBytesText);
|
|
9
|
+
const startedAt = performance.now();
|
|
10
|
+
|
|
11
|
+
function fileBytes(path) {
|
|
12
|
+
try {
|
|
13
|
+
return statSync(path).size;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return 0;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function walFrames(path, pageSize) {
|
|
21
|
+
const bytes = fileBytes(path);
|
|
22
|
+
return bytes < 32 ? 0 : Math.floor((bytes - 32) / (pageSize + 24));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function elapsedMs() {
|
|
26
|
+
return (performance.now() - startedAt).toFixed(3);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let database;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
if (!dbPath || !Number.isInteger(maxBytes) || maxBytes <= 0) {
|
|
33
|
+
throw new Error('expected a database path and a positive byte limit');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const dbBytes = statSync(dbPath).size;
|
|
37
|
+
database = new DatabaseSync(dbPath);
|
|
38
|
+
const pageSize = Number(database.prepare('PRAGMA page_size').get().page_size);
|
|
39
|
+
const walPath = `${dbPath}-wal`;
|
|
40
|
+
const walBytes = fileBytes(walPath);
|
|
41
|
+
const totalBytes = dbBytes + walBytes;
|
|
42
|
+
const walFramesBefore = walFrames(walPath, pageSize);
|
|
43
|
+
|
|
44
|
+
if (totalBytes > maxBytes) {
|
|
45
|
+
console.log(
|
|
46
|
+
`SESSION-DB-MAINTENANCE-SKIP: reason=size-gate db_bytes=${dbBytes} ` +
|
|
47
|
+
`wal_bytes=${walBytes} total_bytes=${totalBytes} max_bytes=${maxBytes} ` +
|
|
48
|
+
`wal_frames_before=${walFramesBefore} wal_frames_after=${walFramesBefore} ` +
|
|
49
|
+
`elapsed_ms=${elapsedMs()}`,
|
|
50
|
+
);
|
|
51
|
+
} else {
|
|
52
|
+
const checkpoint = database.prepare('PRAGMA wal_checkpoint(TRUNCATE)').get();
|
|
53
|
+
const walFramesAfter = walFrames(walPath, pageSize);
|
|
54
|
+
console.log(
|
|
55
|
+
`SESSION-DB-MAINTENANCE: operation=wal_checkpoint_truncate ` +
|
|
56
|
+
`db_bytes=${dbBytes} wal_bytes=${walBytes} wal_frames_before=${walFramesBefore} ` +
|
|
57
|
+
`wal_frames_after=${walFramesAfter} busy=${checkpoint.busy} log=${checkpoint.log} ` +
|
|
58
|
+
`checkpointed=${checkpoint.checkpointed} elapsed_ms=${elapsedMs()}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(
|
|
63
|
+
`SESSION-DB-MAINTENANCE-ERROR: ${error instanceof Error ? error.message : String(error)} ` +
|
|
64
|
+
`elapsed_ms=${elapsedMs()}`,
|
|
65
|
+
);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
} finally {
|
|
68
|
+
if (database) database.close();
|
|
69
|
+
}
|
|
@@ -20,6 +20,8 @@ stop_runner_and_flush_credentials
|
|
|
20
20
|
# stop_opencode — /terminate can afford the real drain, the VM is being torn
|
|
21
21
|
# down either way.
|
|
22
22
|
stop_opencode_and_wait
|
|
23
|
+
quiesce_litestream_for_session_db
|
|
24
|
+
maintain_session_db
|
|
23
25
|
flush_session_db
|
|
24
26
|
|
|
25
27
|
# The key must not outlive the VM's last useful moment. The no-replicate
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evident-ai/runner-cdk",
|
|
3
|
-
"version": "3.5.2-dev.
|
|
3
|
+
"version": "3.5.2-dev.7130f36",
|
|
4
4
|
"description": "Reusable CDK constructs for an Evident agent runner: a single scale-to-zero Fargate runner (task + service + per-agent self-stop role + waker Lambda), or a per-session AWS Lambda MicroVM that boots on demand and suspends between messages. Instantiate once per agent from your own stack.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|