@bongos/core 1.20.60 → 1.20.62
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/.bongos-core.json +95 -40
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +6 -2
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0357-the-update-rule-set-on-deploy-is-the-one-the-sweep-follows.md +2 -0
- package/docs/api/openapi.json +112 -3
- package/docs/api-reference.md +5 -4
- package/docs/copy-inventory.md +377 -328
- package/docs/copy-registry.json +894 -421
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +546 -483
- package/docs/recipes/upgrading-the-core.md +30 -1
- package/modules/autonomy/db.js +13 -6
- package/modules/autonomy/migrations/autonomy_004_runner_identity.sql +30 -0
- package/modules/autonomy/routes/autonomy.js +16 -0
- package/modules/autonomy/runner-health.js +66 -5
- package/modules/hall-ui/public/gate.js +24 -8
- package/modules/provisioning/demo.js +176 -0
- package/modules/provisioning/migrations/provisioning_035_demo.sql +26 -0
- package/modules/provisioning/module.json +5 -3
- package/modules/provisioning/pollers/demo-archive.js +61 -0
- package/modules/provisioning/provisioning.js +2 -2
- package/modules/provisioning/routes/demo.js +54 -0
- package/modules/provisioning/routes/provisioning.js +7 -4
- package/modules/public-landing/public/projects-demo.states.json +53 -0
- package/modules/public-landing/public/projects.html +300 -23
- package/modules/public-landing/public/projects.probes.json +3 -3
- package/modules/public-landing/public/projects.states.json +2 -1
- package/modules/ui-design/kit/fixtures/provisioning-instances-demo.json +114 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/scripts/gds/autobongos-run.js +43 -2
- package/scripts/gds/update-channel.js +11 -2
- package/scripts/gds/update-sweep.js +717 -0
- package/src/module-api.js +1 -1
- package/tests/autonomy_runner_identity.mjs +215 -0
- package/tests/update_channel_db.mjs +13 -6
- package/tests/update_subscription_engine.mjs +5 -5
- package/tests/update_sweep_home.mjs +100 -0
- package/tests/wizard_demo.mjs +264 -0
- package/tests/wizard_front_door.mjs +16 -20
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"in-new": { "auth": true, "url": "/projects?view=new", "expect": { "visible": ["#panel-terms", "#termsAck"], "hidden": ["#qWrap", "#view-new .steps", "#panel-name"] } },
|
|
40
40
|
"in-new-terms-refused": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#panel-terms button[type=submit]"],["wait",300]], "expect": {"visible":["#panel-terms","#h-terms"],"hidden":["#panel-fork"],"text":[["#h-terms","Tick the box to go on. It’s how your project records that you acknowledged the terms."]]} },
|
|
41
41
|
"in-new-fork": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-fork",".doorPill","#wizDemo",".forkStage",".forkRoutes"],"hidden":["#panel-terms","#view-new .steps","#demoSoon",".bongoCell"]} },
|
|
42
|
-
"in-new-fork-demo": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",
|
|
42
|
+
"in-new-fork-demo": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",300]], "expect": {"visible":["#panel-fork","#demoSetup","#demoFitVerdict","#demoRing"],"hidden":["#forkIntro","#demoReady"],"attr":[["#wizDemo","aria-expanded","true"]]} },
|
|
43
|
+
"in-new-demo-ready": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",300],["click","[data-demo-step=\"hours\"][data-by=\"1\"]"],["click","#demoNext1"],["wait",300]], "expect": {"visible":["#demoReady",".demoLedger","#demoInvite1","#demoInvite2"],"hidden":["#demoSetup","#forkIntro"],"text":[["#demoLHours","5 hours each"]]} },
|
|
43
44
|
"in-new-name": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-name","#newName","#view-new .steps"],"hidden":["#panel-fork","#panel-terms"]} },
|
|
44
45
|
"out-new": { "auth": false, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
|
|
45
46
|
"in-new-type": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-type"],"hidden":["#panel-name","#qWrap"]} },
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The demo route (task 1004420): provisioning-instances.json with project 101 marked as a demo whose time box has run out, so its page asks keep-your-planet. Only projects-demo.states.json points at this.",
|
|
3
|
+
"instances": [
|
|
4
|
+
{
|
|
5
|
+
"id": 101,
|
|
6
|
+
"slug": "demo",
|
|
7
|
+
"status": "active",
|
|
8
|
+
"domain": "demo.cloudbongos.com",
|
|
9
|
+
"target_ref": "builder/demo",
|
|
10
|
+
"hosting_shape": "co-tenant",
|
|
11
|
+
"onboard_mode": "scaffold",
|
|
12
|
+
"created_at": "2026-08-01T10:00:00.000Z",
|
|
13
|
+
"provisioned_at": "2026-08-01T10:20:00.000Z",
|
|
14
|
+
"updated_at": "$now-3m",
|
|
15
|
+
"last_verified_at": "$now-3m",
|
|
16
|
+
"last_verified_up": true,
|
|
17
|
+
"error_note": null,
|
|
18
|
+
"dns_note": null,
|
|
19
|
+
"torn_down_at": null,
|
|
20
|
+
"open_intent": null,
|
|
21
|
+
"demo": {
|
|
22
|
+
"people": 3,
|
|
23
|
+
"hours_each": 4,
|
|
24
|
+
"builder_hours": 12,
|
|
25
|
+
"started_at": "2026-09-28T09:00:00Z",
|
|
26
|
+
"ends_at": "2026-09-28T13:00:00Z",
|
|
27
|
+
"remaining_seconds": 0,
|
|
28
|
+
"time_up": true,
|
|
29
|
+
"outcome": null,
|
|
30
|
+
"offline_on": "2026-10-28T09:00:00.000Z",
|
|
31
|
+
"offline_soon": false
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": 102,
|
|
36
|
+
"slug": "harbor",
|
|
37
|
+
"status": "provisioning",
|
|
38
|
+
"planet_template": "blob-frost-tint",
|
|
39
|
+
"planet_accent": "sun-lit",
|
|
40
|
+
"domain": null,
|
|
41
|
+
"target_ref": "builder/harbor",
|
|
42
|
+
"hosting_shape": "cloud-host",
|
|
43
|
+
"owner_monthly_usd": null,
|
|
44
|
+
"onboard_mode": "scaffold",
|
|
45
|
+
"created_at": "$now-6m",
|
|
46
|
+
"provisioned_at": null,
|
|
47
|
+
"updated_at": "$now-2m",
|
|
48
|
+
"last_verified_at": null,
|
|
49
|
+
"last_verified_up": null,
|
|
50
|
+
"error_note": null,
|
|
51
|
+
"dns_note": null,
|
|
52
|
+
"torn_down_at": null,
|
|
53
|
+
"open_intent": {
|
|
54
|
+
"action": "standup",
|
|
55
|
+
"state": "running",
|
|
56
|
+
"created_at": "$now-6m"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": 103,
|
|
61
|
+
"slug": "tidepool",
|
|
62
|
+
"status": "error",
|
|
63
|
+
"domain": "tidepool.cloudbongos.com",
|
|
64
|
+
"target_ref": "builder/tidepool",
|
|
65
|
+
"hosting_shape": "co-tenant",
|
|
66
|
+
"onboard_mode": "adopt",
|
|
67
|
+
"created_at": "2026-08-20T09:00:00.000Z",
|
|
68
|
+
"provisioned_at": null,
|
|
69
|
+
"updated_at": "$now-1d",
|
|
70
|
+
"last_verified_at": null,
|
|
71
|
+
"last_verified_up": null,
|
|
72
|
+
"error_note": "The deploy could not read the repository: it is private, and the deploy key only reads public ones.",
|
|
73
|
+
"dns_note": null,
|
|
74
|
+
"torn_down_at": null,
|
|
75
|
+
"open_intent": null
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": 104,
|
|
79
|
+
"slug": "old-lantern",
|
|
80
|
+
"status": "torn_down",
|
|
81
|
+
"domain": "old-lantern.cloudbongos.com",
|
|
82
|
+
"target_ref": "builder/old-lantern",
|
|
83
|
+
"hosting_shape": "co-tenant",
|
|
84
|
+
"onboard_mode": "scaffold",
|
|
85
|
+
"created_at": "2026-07-02T09:00:00.000Z",
|
|
86
|
+
"provisioned_at": "2026-07-02T09:30:00.000Z",
|
|
87
|
+
"updated_at": "2026-08-15T12:00:00.000Z",
|
|
88
|
+
"last_verified_at": "2026-08-15T11:50:00.000Z",
|
|
89
|
+
"last_verified_up": true,
|
|
90
|
+
"error_note": null,
|
|
91
|
+
"dns_note": null,
|
|
92
|
+
"torn_down_at": "2026-08-15T12:00:00.000Z",
|
|
93
|
+
"open_intent": null
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": 105,
|
|
97
|
+
"slug": "mooring",
|
|
98
|
+
"status": "active",
|
|
99
|
+
"domain": "mooring.cloudbongos.com",
|
|
100
|
+
"target_ref": "builder/mooring",
|
|
101
|
+
"hosting_shape": "co-tenant",
|
|
102
|
+
"onboard_mode": "scaffold",
|
|
103
|
+
"created_at": "2026-08-10T09:00:00.000Z",
|
|
104
|
+
"provisioned_at": "2026-08-10T09:20:00.000Z",
|
|
105
|
+
"updated_at": "$now-20m",
|
|
106
|
+
"last_verified_at": "$now-20m",
|
|
107
|
+
"last_verified_up": false,
|
|
108
|
+
"error_note": null,
|
|
109
|
+
"dns_note": null,
|
|
110
|
+
"torn_down_at": null,
|
|
111
|
+
"open_intent": null
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.62",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.62",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.62",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8477,5 +8477,25 @@
|
|
|
8477
8477
|
"id": "1004421",
|
|
8478
8478
|
"text": "Every project can now have its own look, so builders who work in several projects always know which hall they are in. Creating a project has a new Look step: four ready-made looks or your own colour, a switch to tint dark mode"
|
|
8479
8479
|
}
|
|
8480
|
+
],
|
|
8481
|
+
"1.20.61": [
|
|
8482
|
+
{
|
|
8483
|
+
"id": "1004407",
|
|
8484
|
+
"text": "The Gate page now shows, for each Autobongos runner, which PC it is on, which Claude account it is spending, which code version it runs (and whether that is behind), and says \"Restarting\" while it relaunches for new code inste"
|
|
8485
|
+
}
|
|
8486
|
+
],
|
|
8487
|
+
"1.20.62": [
|
|
8488
|
+
{
|
|
8489
|
+
"id": "1004047",
|
|
8490
|
+
"text": "Every live project now runs a version whose update banner works, so each owner sees when an update is waiting."
|
|
8491
|
+
},
|
|
8492
|
+
{
|
|
8493
|
+
"id": "1004420",
|
|
8494
|
+
"text": "\"Try the demo\" now works. A small team picks how many people and how many hours each, writes an idea, and gets a first read of what fits. The demo picks the settings for them (the founder decides, no rewards, all managed, priv"
|
|
8495
|
+
},
|
|
8496
|
+
{
|
|
8497
|
+
"id": "1004307",
|
|
8498
|
+
"text": "The automatic update on cloudbongos.com will now always run the newest version of itself, instead of a copy stuck from August."
|
|
8499
|
+
}
|
|
8480
8500
|
]
|
|
8481
8501
|
}
|
|
@@ -368,7 +368,7 @@ const MAX_CLAIM_ATTEMPTS = 5;
|
|
|
368
368
|
const LIMIT_UNKNOWN_RESET_MS = 30 * 60 * 1000;
|
|
369
369
|
|
|
370
370
|
function newRunnerState() {
|
|
371
|
-
return { setAside: new Map(), queueGate: null, limitUntil: null, diskHead: null };
|
|
371
|
+
return { setAside: new Map(), queueGate: null, limitUntil: null, diskHead: null, mainHead: null };
|
|
372
372
|
}
|
|
373
373
|
const RUNNER_STATE = newRunnerState();
|
|
374
374
|
|
|
@@ -703,6 +703,43 @@ function writeHeartbeat(now = Date.now(), runnerState = RUNNER_STATE) {
|
|
|
703
703
|
// is the signal.
|
|
704
704
|
const RUNNER_STARTED_AT = new Date().toISOString();
|
|
705
705
|
|
|
706
|
+
// claudeAccountLabel — WHICH Claude account this machine's workers spend
|
|
707
|
+
// (task 1004407), as a label and never a secret. Workers inherit
|
|
708
|
+
// ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN from this process
|
|
709
|
+
// (WORKER_ENV_ALLOW), and either one outranks the stored login, so those are
|
|
710
|
+
// named first, by the variable that is set and never by its value. Otherwise it
|
|
711
|
+
// is the login Claude Code itself records in ~/.claude.json (oauthAccount): the
|
|
712
|
+
// email, plus the organisation when it has one. Read on every beat, so signing
|
|
713
|
+
// a different account in shows up without a restart; null when nothing can be
|
|
714
|
+
// read, which the card leaves blank rather than guessing.
|
|
715
|
+
function claudeAccountLabel({ env = process.env, readFile = (p) => fs.readFileSync(p, 'utf8') } = {}) {
|
|
716
|
+
if (env.ANTHROPIC_API_KEY) return 'an API key (ANTHROPIC_API_KEY)';
|
|
717
|
+
if (env.CLAUDE_CODE_OAUTH_TOKEN) return 'a Claude token (CLAUDE_CODE_OAUTH_TOKEN)';
|
|
718
|
+
try {
|
|
719
|
+
const dir = env.CLAUDE_CONFIG_DIR || os.homedir();
|
|
720
|
+
const acct = JSON.parse(readFile(path.join(dir, '.claude.json'))).oauthAccount || {};
|
|
721
|
+
const email = typeof acct.emailAddress === 'string' ? acct.emailAddress.trim() : '';
|
|
722
|
+
if (!email) return null;
|
|
723
|
+
const org = typeof acct.organizationName === 'string' ? acct.organizationName.trim() : '';
|
|
724
|
+
return (org ? `${email} · ${org}` : email).slice(0, 200);
|
|
725
|
+
} catch (_) { return null; }
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// heartbeatIdentity — the fields that say which code and which account (task
|
|
729
|
+
// 1004407). Undefined fields are dropped, so a runner that cannot read its own
|
|
730
|
+
// commit sends nothing for it rather than an empty string the route would refuse.
|
|
731
|
+
function heartbeatIdentity(deps = {}) {
|
|
732
|
+
const st = deps.state || RUNNER_STATE;
|
|
733
|
+
const out = {
|
|
734
|
+
head: LOADED_HEAD || undefined,
|
|
735
|
+
disk_head: st.diskHead && st.diskHead !== LOADED_HEAD ? st.diskHead : undefined,
|
|
736
|
+
main_head: st.mainHead || undefined,
|
|
737
|
+
claude_account: (deps.claudeAccountLabel || claudeAccountLabel)() || undefined,
|
|
738
|
+
};
|
|
739
|
+
for (const k of Object.keys(out)) if (out[k] === undefined) delete out[k];
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
|
|
706
743
|
async function postHeartbeat(fields = {}, deps = {}) {
|
|
707
744
|
try {
|
|
708
745
|
const api = deps.api || await (require('./cli-lib').cliClient)();
|
|
@@ -711,6 +748,7 @@ async function postHeartbeat(fields = {}, deps = {}) {
|
|
|
711
748
|
host: os.hostname(),
|
|
712
749
|
pid: process.pid,
|
|
713
750
|
started_at: RUNNER_STARTED_AT,
|
|
751
|
+
...heartbeatIdentity(deps),
|
|
714
752
|
...fields,
|
|
715
753
|
},
|
|
716
754
|
});
|
|
@@ -870,6 +908,9 @@ async function codeDrifted(deps = {}) {
|
|
|
870
908
|
let tip = null;
|
|
871
909
|
try { tip = await runFetchHead(deps); } catch (_) { return null; }
|
|
872
910
|
if (!tip) return null;
|
|
911
|
+
// Kept for the heartbeat (task 1004407), so the hall can say "behind main"
|
|
912
|
+
// without the server ever reaching GitHub itself.
|
|
913
|
+
(deps.state || RUNNER_STATE).mainHead = tip;
|
|
873
914
|
return tip !== base ? { from: base.slice(0, 8), to: String(tip).slice(0, 8) } : false;
|
|
874
915
|
}
|
|
875
916
|
|
|
@@ -1147,6 +1188,6 @@ module.exports = {
|
|
|
1147
1188
|
readArgv, pickTask, iteration, record, logPath, spawnWorker, workerEnv, WORKER_ENV_ALLOW, resolveWorkerBin,
|
|
1148
1189
|
worktreeName, readFence, reconcileLeftoverClaims, pollSignals, waitOrJump, forever, CLAIM_PREFIX, codeDrifted, loadedHead, UPGRADE_EXIT_CODE, MIN_UPTIME_BEFORE_UPGRADE_MS,
|
|
1149
1190
|
heartbeatPath, writeHeartbeat, readHeartbeat, pidAlive, anotherRunnerIsAlive, HEARTBEAT_STALE_MS,
|
|
1150
|
-
isRunnerTree, RUNNER_TREE_RE, postHeartbeat, RUNNER_STARTED_AT, failureReason,
|
|
1191
|
+
isRunnerTree, RUNNER_TREE_RE, postHeartbeat, claudeAccountLabel, RUNNER_STARTED_AT, failureReason,
|
|
1151
1192
|
newRunnerState, loadedFiles, supervisorTouched, refreshCheckout, LIMIT_UNKNOWN_RESET_MS, QUEUE_GATED_EXIT, CLAIM_SET_ASIDE_MS, MAX_CLAIM_ATTEMPTS, owedTaskIds,
|
|
1152
1193
|
};
|
|
@@ -243,6 +243,14 @@ function loadSubscriptions({ instanceDir = process.cwd(), fsImpl = fs, file = SU
|
|
|
243
243
|
//
|
|
244
244
|
// A failure's reason is a fixed class, never psql's own text: stderr can name a DB user or host,
|
|
245
245
|
// and the reason is printed into the sweep's log.
|
|
246
|
+
//
|
|
247
|
+
// WHICH DATABASE (task 1004307). UPDATE_CHANNEL_DB names it first. That is the one a control-plane
|
|
248
|
+
// unit should set, NOT PGDATABASE: the sweep hands its own env to every upgrade it spawns
|
|
249
|
+
// ({ ...process.env, ...inst.env }), so a unit-level PGDATABASE would reach any tenant whose roster
|
|
250
|
+
// entry names no database of its own, and that tenant's migrate would run against the control
|
|
251
|
+
// plane's database. UPDATE_CHANNEL_DB means nothing to the upgrade, so it cannot leak that way.
|
|
252
|
+
// It takes a bare db name or a connection string, as psql -d does.
|
|
253
|
+
const CHANNEL_DB_ENV = 'UPDATE_CHANNEL_DB';
|
|
246
254
|
const CHANNEL_SOURCE_DEPLOY = 'deploy';
|
|
247
255
|
const CHANNEL_SOURCE_ROSTER = 'roster';
|
|
248
256
|
const DB_CHANNELS_SQL = 'SELECT slug, update_channel FROM provisioning_instances';
|
|
@@ -268,8 +276,8 @@ function channelLine(inst) {
|
|
|
268
276
|
// { ok:false, reason } when there is no database to ask, psql is missing, or the table is absent
|
|
269
277
|
// (an instance with the provisioning module off). The caller falls back to the roster and says why.
|
|
270
278
|
function readDbChannels({ env = process.env, run = spawnSync } = {}) {
|
|
271
|
-
const target = env.DATABASE_URL || env.PGDATABASE;
|
|
272
|
-
if (!target) return { ok: false, reason:
|
|
279
|
+
const target = env[CHANNEL_DB_ENV] || env.DATABASE_URL || env.PGDATABASE;
|
|
280
|
+
if (!target) return { ok: false, reason: `no ${CHANNEL_DB_ENV} (or DATABASE_URL / PGDATABASE) set for the sweep` };
|
|
273
281
|
const r = run('psql', ['-d', target, '-At', '-F', '\t', '-v', 'ON_ERROR_STOP=1', '--no-psqlrc', '-c', DB_CHANNELS_SQL],
|
|
274
282
|
{ encoding: 'utf8', timeout: DB_CHANNELS_TIMEOUT_MS });
|
|
275
283
|
if (!r || r.error) {
|
|
@@ -399,6 +407,7 @@ module.exports = {
|
|
|
399
407
|
parseNpmViewVersions,
|
|
400
408
|
loadSubscriptions,
|
|
401
409
|
readDbChannels,
|
|
410
|
+
CHANNEL_DB_ENV,
|
|
402
411
|
applyDbChannels,
|
|
403
412
|
channelLine,
|
|
404
413
|
psqlFailureClass,
|