@essentialai/cogent-bridge 3.23.9 → 3.24.1
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/CHANGELOG.md +109 -0
- package/README.md +15 -3
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/services/peer-presence.d.ts +77 -0
- package/dist/services/peer-presence.d.ts.map +1 -0
- package/dist/services/peer-presence.js +181 -0
- package/dist/services/peer-presence.js.map +1 -0
- package/dist/startup.d.ts +28 -0
- package/dist/startup.d.ts.map +1 -1
- package/dist/startup.js +77 -0
- package/dist/startup.js.map +1 -1
- package/dist/tools/deregister-peer.d.ts.map +1 -1
- package/dist/tools/deregister-peer.js +22 -0
- package/dist/tools/deregister-peer.js.map +1 -1
- package/dist/tools/health-check.d.ts.map +1 -1
- package/dist/tools/health-check.js +20 -0
- package/dist/tools/health-check.js.map +1 -1
- package/dist/tools/register-peer.d.ts.map +1 -1
- package/dist/tools/register-peer.js +20 -4
- package/dist/tools/register-peer.js.map +1 -1
- package/package.json +4 -3
- package/server.json +2 -2
- package/dist/e2e/helpers.d.ts +0 -101
- package/dist/e2e/helpers.d.ts.map +0 -1
- package/dist/e2e/helpers.js +0 -228
- package/dist/e2e/helpers.js.map +0 -1
- package/dist/services/session-wake-lock.d.ts +0 -26
- package/dist/services/session-wake-lock.d.ts.map +0 -1
- package/dist/services/session-wake-lock.js +0 -198
- package/dist/services/session-wake-lock.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
|
+
## 3.24.1 — 2026-09-15
|
|
2
|
+
|
|
3
|
+
Patch release. One product fix, one user-facing discoverability fix, and three gate fixes — every
|
|
4
|
+
one of them a case of a check reporting something that was not true.
|
|
5
|
+
|
|
6
|
+
### `cogent_register_peer` could never auto-create a channel (product)
|
|
7
|
+
|
|
8
|
+
Registering with **no channel configured** — the zero-config onboarding path — has never worked.
|
|
9
|
+
The client posted `` label: `Auto-created by ${label}` `` while the relay's own
|
|
10
|
+
`CreateSessionRequestSchema` requires `/^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/`: lowercase, digits and
|
|
11
|
+
hyphens. A capital `A` and spaces cannot match, so every auto-create returned **HTTP 400
|
|
12
|
+
INVALID_INPUT**. Measured against a real relay:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
{ secret, label: "Auto-created by Payment Orchestrator" } -> 400 invalid_format
|
|
16
|
+
{ secret } -> 201 label "slim-tap-27be"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The 400 was visible as far back as 3.5.2 and was read as "avoid falling into auto-create", so the
|
|
20
|
+
symptom was routed around while the path stayed broken. Fixed by omitting the label and letting the
|
|
21
|
+
relay name the channel — it already does so with collision-retry scoped to the org. The generated
|
|
22
|
+
label is now persisted so a human can learn which channel to join.
|
|
23
|
+
|
|
24
|
+
### Slash commands say what input they expect
|
|
25
|
+
|
|
26
|
+
`/cogent:register` parses a channel name, a password, a peer name and an optional ORGID out of free
|
|
27
|
+
text, and falls back to the project `.env` when given nothing. None of that was discoverable: the
|
|
28
|
+
picker showed only a description. All commands that parse `$ARGUMENTS` now declare `argument-hint`:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
/cogent:register [channel "<name>" password "<pw>" peer "<peerId>" [ORGID "<org-id>"]] — omit to use .env
|
|
32
|
+
/cogent:send [peerId] [message] — omit to pick from the peer list
|
|
33
|
+
/cogent:init [<project-path> <project-path> ...] [--share <shared-path>]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Gates: three false verdicts fixed
|
|
37
|
+
|
|
38
|
+
- **An unanswered `--human-inbound` window is BLOCKED, not a product break.** When nobody typed the
|
|
39
|
+
line, the harness printed `PRODUCT FAILURES ❌ … customer-visible Slack regression` and exited 1
|
|
40
|
+
— a regression claimed out of an empty chair. Now stamped with a `NO_HUMAN_INPUT` code and
|
|
41
|
+
classified first, so it exits 2 ("proves nothing"). The adjacent real failure — Slack seeing the
|
|
42
|
+
message while the adapter drops it — is asserted to stay exit 1.
|
|
43
|
+
- **Harnesses target the SANDBOX by default.** Seven of them hardcoded
|
|
44
|
+
`argVal("--endpoint", "https://cogent.tools")`, so running the tests was indistinguishable from
|
|
45
|
+
exercising production. `scripts/target-lib.mjs` is now the single source of truth, production is
|
|
46
|
+
an explicit `--target prod`, and an unreachable target BLOCKS rather than degrading — there is
|
|
47
|
+
deliberately no fallback. This immediately exposed `wake-race-test`, which was labelled
|
|
48
|
+
`runnable_offline: true` while silently POSTing to the production relay on every run; it now uses
|
|
49
|
+
the local relay rig.
|
|
50
|
+
- **Per-agent vendor liveness preflight** (`scripts/vendor-liveness-test.mjs`). Asks each vendor
|
|
51
|
+
whether it is alive **through the product's own spawn path** before a matrix burns an hour
|
|
52
|
+
proving the same thing. Exit 2 on a dead vendor, never 1: an expired quota is not a Cogent defect.
|
|
53
|
+
|
|
54
|
+
### Unchanged
|
|
55
|
+
|
|
56
|
+
No wire-format change. No relay behaviour change beyond the advertised plugin version.
|
|
57
|
+
## 3.24.0 — 2026-09-14
|
|
58
|
+
|
|
59
|
+
### An agent knows whether it is IN its channel — and puts itself back
|
|
60
|
+
|
|
61
|
+
**The defect.** Registration is an explicit act: something must call `cogent_register_peer`. A
|
|
62
|
+
bridge starting does NOT register — it only publishes the tools. So an agent session could come up
|
|
63
|
+
holding valid credentials while being ABSENT from its own channel, invisible to everyone messaging
|
|
64
|
+
it. That state was silent AND self-perpetuating, because every repair sat behind a successful
|
|
65
|
+
registration:
|
|
66
|
+
|
|
67
|
+
- `announceBridge()` — written at register time
|
|
68
|
+
- `reapStaleBridges()` — "only after a peer is genuinely registered"
|
|
69
|
+
- `triggerReRegistration()` — reads in-memory state, which is null in a fresh process
|
|
70
|
+
|
|
71
|
+
When registration was the thing that did not happen, nothing recovered and nothing cleaned up.
|
|
72
|
+
Measured on one checkout: **18 live bridges at 3.23.9**, only 2 announcements (both dead pids), last
|
|
73
|
+
successful register five days earlier, and the peer absent from a healthy Team channel whose two
|
|
74
|
+
other agents were present and waiting on it.
|
|
75
|
+
|
|
76
|
+
**Presence**, in the vocabulary this codebase already used:
|
|
77
|
+
|
|
78
|
+
| state | meaning |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `local` | no relay; presence does not apply |
|
|
81
|
+
| `deferred` | cloud-configured, never joined |
|
|
82
|
+
| `offline` | owns a peer for this cwd but has not registered it — nobody can reach it |
|
|
83
|
+
| `online` | registered; the channel lists this agent |
|
|
84
|
+
|
|
85
|
+
Evaluated once at startup, fire-and-forget after the transport connects, and **reported**:
|
|
86
|
+
`offline -> online` at INFO, a stuck `offline` at WARN. `cogent_health_check` now carries
|
|
87
|
+
`presence`, so an agent can ASK its own state instead of inferring it from transport internals.
|
|
88
|
+
|
|
89
|
+
- **`COGENT_AUTO_PRESENCE`** (default ON) — set `false` to observe the state without acting on it.
|
|
90
|
+
- **`cogent_deregister_peer` now clears the stored `peerId`.** That field is the CONSENT SIGNAL:
|
|
91
|
+
without clearing it, the next start would put an agent back into a channel it deliberately left.
|
|
92
|
+
The rest of the credential is kept — channel membership is still valid.
|
|
93
|
+
|
|
94
|
+
**Safety.** Credentials are keyed per working directory, so reading this directory's own file
|
|
95
|
+
cannot inherit another project's session. The agent-session id need not be correct here; auto-relay
|
|
96
|
+
re-resolves it on every wake. We register rather than merely announce: announcing without
|
|
97
|
+
registering would make every leftover bridge immortal — exempt from reap signal 4, yet never
|
|
98
|
+
superseding itself.
|
|
99
|
+
|
|
100
|
+
### Fixed
|
|
101
|
+
|
|
102
|
+
- **The presence restore now replays the stored `peerSecret`.** `registerPeer()` takes it as the
|
|
103
|
+
11th, OPTIONAL parameter and the restore passed ten. A peer owned by an EARLIER token — the normal
|
|
104
|
+
state after a re-join — answered `PEER_NOT_YOURS`, the restore failed, and the agent stayed
|
|
105
|
+
offline: precisely the state the feature exists to leave. Per the TypeScript vault
|
|
106
|
+
(`concepts/Optional_Parameters`), an omitted optional parameter is silently `undefined`, so
|
|
107
|
+
**deleting the argument left both `tsc --noEmit` and the unit tests at exit 0**. The positional
|
|
108
|
+
tuple is now extracted as `presenceRegisterArgs()` and every position is pinned by test.
|
|
109
|
+
|
|
1
110
|
## 3.23.9 — 2026-09-06
|
|
2
111
|
|
|
3
112
|
### Fixed — `cogent_health_check` described the RELAY, not this bridge
|
package/README.md
CHANGED
|
@@ -116,7 +116,9 @@ codex mcp add cogent \
|
|
|
116
116
|
### Gemini
|
|
117
117
|
|
|
118
118
|
Gemini joins a channel as a **standalone poll-agent**, not a plugin — it polls the relay and
|
|
119
|
-
answers on its own. Nothing to install as an MCP server, and nothing to `plugin update
|
|
119
|
+
answers on its own. Nothing to install as an MCP server, and nothing to `plugin update`: you
|
|
120
|
+
restart the process to pick up a new version. A systemd unit is provided at
|
|
121
|
+
`scripts/cogent-gemini-agent.service`.
|
|
120
122
|
|
|
121
123
|
```bash
|
|
122
124
|
export GEMINI_API_KEY=<your-key>
|
|
@@ -124,8 +126,18 @@ COGENT_GEMINI_CHANNEL=<channel> COGENT_GEMINI_SECRET=<secret> COGENT_GEMINI_PEER
|
|
|
124
126
|
node scripts/cogent-gemini-agent.mjs
|
|
125
127
|
```
|
|
126
128
|
|
|
127
|
-
It registers itself
|
|
128
|
-
|
|
129
|
+
It registers itself, answers directed messages and human broadcasts, and shows up in
|
|
130
|
+
`/cogent peers` like any other peer. Three differences are worth knowing before you wire it in:
|
|
131
|
+
|
|
132
|
+
1. **It replies on a poll, not a wake.** Claude Code and Codex are resumed the instant a message
|
|
133
|
+
arrives. Gemini checks the relay on an interval (`COGENT_GEMINI_POLL_MS`, default 4000 ms), so
|
|
134
|
+
its answers typically land a few seconds later than a plugin agent's.
|
|
135
|
+
2. **It answers; it does not start conversations.** A poll-agent has no `cogent_send_message`
|
|
136
|
+
tool, so it cannot post unprompted or open a thread — it only responds to what it is sent.
|
|
137
|
+
3. **An API key is required.** Google no longer supports subscription auth for the Gemini CLI, so
|
|
138
|
+
`GEMINI_API_KEY` is the only rail. If the key is missing, disabled or out of quota the agent
|
|
139
|
+
still registers and joins the channel — it then replies with the vendor's error instead of
|
|
140
|
+
going silent, so check its replies, not just the peer list.
|
|
129
141
|
|
|
130
142
|
<!-- COGENT:COMMANDS:START -->
|
|
131
143
|
<!-- Generated by scripts/sync-docs.mjs from server/src/install/commands.ts.
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { SERVER_NAME, SERVER_VERSION } from "./constants.js";
|
|
4
4
|
import { startUpdateCheckLoop } from "./backend/npm-update-check.js";
|
|
5
|
-
import { runStartup, runPreflights, resolvePendingLabel, isCloudMode, setInboxNotifier, cloudWsClient, cloudInbox, autoRelay } from "./startup.js";
|
|
5
|
+
import { runStartup, runPreflights, resolvePendingLabel, isCloudMode, setInboxNotifier, cloudWsClient, cloudInbox, autoRelay, restorePeerPresence } from "./startup.js";
|
|
6
6
|
import { logger } from "./logger.js";
|
|
7
7
|
import { registerRegisterPeerTool } from "./tools/register-peer.js";
|
|
8
8
|
import { registerSendMessageTool } from "./tools/send-message.js";
|
|
@@ -145,6 +145,18 @@ async function main() {
|
|
|
145
145
|
void runPreflights().catch((err) => {
|
|
146
146
|
logger.warn(`Preflight error (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
147
147
|
});
|
|
148
|
+
// PRESENCE. Work out whether this agent is actually IN its channel, and if it owns a
|
|
149
|
+
// peer there but has not registered it, put it back. An agent whose bridge is running
|
|
150
|
+
// while its peer is absent is invisible to everyone messaging it, and every repair in
|
|
151
|
+
// this client used to sit behind a successful register — so when registration was the
|
|
152
|
+
// thing that did not happen, nothing recovered and nothing cleaned up. Measured: 18
|
|
153
|
+
// live bridges on one checkout, peer absent from a healthy channel for five days.
|
|
154
|
+
// States and the full explanation: services/peer-presence.ts.
|
|
155
|
+
// Fire-and-forget for the same reason as the two above: it does network I/O and must
|
|
156
|
+
// never precede `initialize`. Never throws; guarded anyway.
|
|
157
|
+
void restorePeerPresence().catch((err) => {
|
|
158
|
+
logger.warn(`Presence restore error (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
159
|
+
});
|
|
148
160
|
// Best-effort proactive update nudge. Fire-and-forget: never blocks the server
|
|
149
161
|
// becoming ready, never throws, and writes only to STDERR (stdout is the MCP
|
|
150
162
|
// protocol). Stays silent unless a newer cogent-bridge is published on npm.
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,mBAAmB,EAAE,WAAW,EAAE,gBAAgB,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** The four states an agent can be in with respect to its channel. */
|
|
2
|
+
export type PresenceState = "local" | "deferred" | "offline" | "online";
|
|
3
|
+
/** What a presence evaluation concluded, and what it did about it. */
|
|
4
|
+
export type PresenceOutcome = {
|
|
5
|
+
state: "local" | "deferred" | "online";
|
|
6
|
+
restored: false;
|
|
7
|
+
detail: string;
|
|
8
|
+
} | {
|
|
9
|
+
state: "online";
|
|
10
|
+
restored: true;
|
|
11
|
+
peerId: string;
|
|
12
|
+
detail: string;
|
|
13
|
+
} | {
|
|
14
|
+
state: "offline";
|
|
15
|
+
restored: false;
|
|
16
|
+
detail: string;
|
|
17
|
+
};
|
|
18
|
+
export interface PresenceDeps {
|
|
19
|
+
/** True only when the ACTIVE backend really is the cloud one — not a configuration flag. */
|
|
20
|
+
isCloudConfigured: () => boolean;
|
|
21
|
+
/** True when something already registered in THIS process; its state is authoritative. */
|
|
22
|
+
alreadyRegisteredInProcess: () => boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Is a LIVE bridge already announcing this peer for this working directory?
|
|
25
|
+
*
|
|
26
|
+
* 🔴 THE SHARED-CHECKOUT CASE. There is ONE credential file per cwd, so every bridge in a
|
|
27
|
+
* checkout reads the SAME peerId. Two agents legitimately share a checkout under DIFFERENT
|
|
28
|
+
* peer identities (marvin-coder + marvin-mobile — which is why the ownership registry is keyed
|
|
29
|
+
* on (cwd, peerId) and not cwd alone). Without this check, a second bridge restores the first
|
|
30
|
+
* agent's peerId, supersession stands one of them down, and an agent silently loses its own
|
|
31
|
+
* identity. Proven by controlled A/B: with the restore unguarded, wake-dedup-matrix-test
|
|
32
|
+
* --topology two-peer could no longer detect its own break (STAYED_GREEN, 0 overlapping
|
|
33
|
+
* resumes); with presence off it went RED with 1. The gate was telling the truth.
|
|
34
|
+
*/
|
|
35
|
+
peerAlreadyServed: (peerId: string) => boolean;
|
|
36
|
+
loadCredentials: () => Promise<{
|
|
37
|
+
endpoint?: string;
|
|
38
|
+
sessionId?: string;
|
|
39
|
+
token?: string;
|
|
40
|
+
peerId?: string;
|
|
41
|
+
/** AUD-003 ownership proof, issued once when this peer was created. */
|
|
42
|
+
peerSecret?: string;
|
|
43
|
+
} | null>;
|
|
44
|
+
/** Go cloud-live and register, restoring the channel entry. Throws on failure. */
|
|
45
|
+
goOnline: (peer: {
|
|
46
|
+
peerId: string;
|
|
47
|
+
endpoint: string;
|
|
48
|
+
sessionId: string;
|
|
49
|
+
token: string;
|
|
50
|
+
cwd: string;
|
|
51
|
+
label: string;
|
|
52
|
+
/** Replayed on register so a peer owned by an EARLIER token can be reclaimed. */
|
|
53
|
+
peerSecret?: string;
|
|
54
|
+
}) => Promise<void>;
|
|
55
|
+
cwd: () => string;
|
|
56
|
+
env?: NodeJS.ProcessEnv;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Automatic presence restore. ON by default — a default-off self-heal heals nobody.
|
|
60
|
+
* Opt out with COGENT_AUTO_PRESENCE=false to observe the state without acting on it.
|
|
61
|
+
*/
|
|
62
|
+
export declare function autoPresenceEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Determine this agent's presence and, when it is `offline` with a peer it owns,
|
|
65
|
+
* bring it back `online`.
|
|
66
|
+
*
|
|
67
|
+
* NEVER THROWS. Runs fire-and-forget after the MCP transport connects — the
|
|
68
|
+
* `initialize` handshake must never wait on I/O (scripts/startup-nonblocking-test.mjs).
|
|
69
|
+
*/
|
|
70
|
+
export declare function restorePresence(deps: PresenceDeps): Promise<PresenceOutcome>;
|
|
71
|
+
/** Record the evaluated presence. Called by startup after each evaluation. */
|
|
72
|
+
export declare function setPresence(outcome: PresenceOutcome): void;
|
|
73
|
+
/** The agent's own view of its channel presence. Null until first evaluated. */
|
|
74
|
+
export declare function getPresence(): PresenceOutcome | null;
|
|
75
|
+
/** Report the outcome once, at a level that matches how much it matters. */
|
|
76
|
+
export declare function logPresence(outcome: PresenceOutcome): void;
|
|
77
|
+
//# sourceMappingURL=peer-presence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peer-presence.d.ts","sourceRoot":"","sources":["../../src/services/peer-presence.ts"],"names":[],"mappings":"AAuDA,sEAAsE;AACtE,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAExE,sEAAsE;AACtE,MAAM,MAAM,eAAe,GACvB;IAAE,KAAK,EAAE,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3E;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACnE;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1D,MAAM,WAAW,YAAY;IAC3B,4FAA4F;IAC5F,iBAAiB,EAAE,MAAM,OAAO,CAAC;IACjC,0FAA0F;IAC1F,0BAA0B,EAAE,MAAM,OAAO,CAAC;IAC1C;;;;;;;;;;;OAWG;IACH,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAC/C,eAAe,EAAE,MAAM,OAAO,CAAC;QAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,uEAAuE;QACvE,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,IAAI,CAAC,CAAC;IACV,kFAAkF;IAClF,QAAQ,EAAE,CAAC,IAAI,EAAE;QACf,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,iFAAiF;QACjF,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAIjF;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,CAkFlF;AAYD,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAE1D;AAED,gFAAgF;AAChF,wBAAgB,WAAW,IAAI,eAAe,GAAG,IAAI,CAEpD;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAW1D"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// PEER PRESENCE — what state this agent is in on its channel, why, and getting
|
|
3
|
+
// it back to `online` without anyone being told to restart something.
|
|
4
|
+
//
|
|
5
|
+
// ── THE VOCABULARY, taken from what this codebase already says ──────────────
|
|
6
|
+
// "local" COGENT_LOCAL / no relay. Presence is not a concept here.
|
|
7
|
+
// "deferred" EXISTING TERM (backend-provider.ts): cloud-CONFIGURED but never joined —
|
|
8
|
+
// there are no stored credentials for this working directory. Nothing to
|
|
9
|
+
// restore; the agent must join a channel first.
|
|
10
|
+
// "offline" Stored credentials name a peer for this working directory, but this
|
|
11
|
+
// process has not registered it. The bridge is running; the AGENT IS NOT
|
|
12
|
+
// IN THE CHANNEL. Other peers see it as absent and cannot address it.
|
|
13
|
+
// "online" Registered. The channel lists this agent and messages reach it.
|
|
14
|
+
//
|
|
15
|
+
// ── HOW AN AGENT ENDS UP `offline` — the mechanism, stated plainly ──────────
|
|
16
|
+
// Registration is an explicit act: something must call `cogent_register_peer`. The
|
|
17
|
+
// bridge starting does NOT register — it only publishes the tools. So every time an
|
|
18
|
+
// agent session starts, the bridge comes up holding valid credentials while the agent
|
|
19
|
+
// is absent from its own channel, and stays that way until the agent calls the tool.
|
|
20
|
+
// If the agent never calls it — it forgot, it crashed first, the session was resumed
|
|
21
|
+
// from a transcript that had already registered — the peer is never (re-)created, and
|
|
22
|
+
// the relay's stale sweep eventually deletes the entry left over from last time.
|
|
23
|
+
//
|
|
24
|
+
// That state used to be both SILENT and SELF-PERPETUATING, because every repair in
|
|
25
|
+
// this client sat behind a successful registration:
|
|
26
|
+
// - announceBridge() (auto-relay) — written at register time
|
|
27
|
+
// - reapStaleBridges() (auto-relay) — "only after a peer is genuinely registered"
|
|
28
|
+
// - triggerReRegistration()(startup) — reads in-memory state, null in a fresh process
|
|
29
|
+
// So when registration was the thing that did not happen, nothing recovered and nothing
|
|
30
|
+
// cleaned up. Measured 2026-09-14 on one checkout: 18 live bridges at 3.23.9, last
|
|
31
|
+
// successful register five days earlier, the peer absent from a healthy Team channel
|
|
32
|
+
// whose two other agents were present and waiting on it.
|
|
33
|
+
//
|
|
34
|
+
// ── WHY RESTORING IT IS SAFE ────────────────────────────────────────────────
|
|
35
|
+
// - Credentials are keyed PER WORKING DIRECTORY
|
|
36
|
+
// (~/.cogent/credentials/<sha256(cwd)[..16]>.json, commit f758d8ee). Reading this
|
|
37
|
+
// directory's own file cannot inherit another project's session — that was the
|
|
38
|
+
// LEGACY GLOBAL file, which is only ever `fs.access`'d for a warning and never read.
|
|
39
|
+
// - The agent-session id need not be correct here: auto-relay re-resolves it on every
|
|
40
|
+
// wake (auto-relay.ts, "Only adopt a resolved id") and register-peer itself falls
|
|
41
|
+
// back to the channel session id on an unresolved resolution.
|
|
42
|
+
// - `peerId` in the credential is the CONSENT SIGNAL. create/join write credentials
|
|
43
|
+
// without one, and deregister clears it, so a deliberate departure is never undone.
|
|
44
|
+
//
|
|
45
|
+
// ── WHY WE DO NOT JUST ANNOUNCE ─────────────────────────────────────────────
|
|
46
|
+
// Reap signal 4 skips any ANNOUNCED pid, reasoning that an announced bridge is 3.23.4+
|
|
47
|
+
// and stands down by itself. Announcing WITHOUT registering would make every leftover
|
|
48
|
+
// immortal: exempt from reaping, yet never superseding itself (supersession is only
|
|
49
|
+
// consulted on a wake, which a bridge with no peer never gets). The invariant is "a live
|
|
50
|
+
// bridge either REGISTERS — and so announces and can stand down — or it stays reapable."
|
|
51
|
+
// Restoring presence registers, so the announce it performs is correct.
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
import { logger } from "../logger.js";
|
|
54
|
+
/**
|
|
55
|
+
* Automatic presence restore. ON by default — a default-off self-heal heals nobody.
|
|
56
|
+
* Opt out with COGENT_AUTO_PRESENCE=false to observe the state without acting on it.
|
|
57
|
+
*/
|
|
58
|
+
export function autoPresenceEnabled(env = process.env) {
|
|
59
|
+
const v = env.COGENT_AUTO_PRESENCE;
|
|
60
|
+
if (v === undefined || v === "")
|
|
61
|
+
return true;
|
|
62
|
+
return !/^(0|false|no|off)$/i.test(v.trim());
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Determine this agent's presence and, when it is `offline` with a peer it owns,
|
|
66
|
+
* bring it back `online`.
|
|
67
|
+
*
|
|
68
|
+
* NEVER THROWS. Runs fire-and-forget after the MCP transport connects — the
|
|
69
|
+
* `initialize` handshake must never wait on I/O (scripts/startup-nonblocking-test.mjs).
|
|
70
|
+
*/
|
|
71
|
+
export async function restorePresence(deps) {
|
|
72
|
+
try {
|
|
73
|
+
if (!deps.isCloudConfigured()) {
|
|
74
|
+
return { state: "local", restored: false, detail: "local mode — presence does not apply" };
|
|
75
|
+
}
|
|
76
|
+
if (deps.alreadyRegisteredInProcess()) {
|
|
77
|
+
return { state: "online", restored: false, detail: "registered by this process" };
|
|
78
|
+
}
|
|
79
|
+
const creds = await deps.loadCredentials();
|
|
80
|
+
if (!creds || !creds.endpoint || !creds.sessionId || !creds.token) {
|
|
81
|
+
return {
|
|
82
|
+
state: "deferred",
|
|
83
|
+
restored: false,
|
|
84
|
+
detail: "no stored channel credentials for this working directory — join a channel first",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (!creds.peerId) {
|
|
88
|
+
// Credentials WITHOUT a peerId: created by join/create, or cleared by a deliberate
|
|
89
|
+
// deregister. Either way this agent is not meant to be in the channel right now.
|
|
90
|
+
return {
|
|
91
|
+
state: "deferred",
|
|
92
|
+
restored: false,
|
|
93
|
+
detail: "channel credentials exist but name no peer — nothing was registered here",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const peerId = creds.peerId;
|
|
97
|
+
// Someone live is already this peer. Registering again would take the identity from them.
|
|
98
|
+
if (deps.peerAlreadyServed(peerId)) {
|
|
99
|
+
return {
|
|
100
|
+
state: "online",
|
|
101
|
+
restored: false,
|
|
102
|
+
detail: `peer '${peerId}' is already served by a live bridge for this working directory`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (!autoPresenceEnabled(deps.env ?? process.env)) {
|
|
106
|
+
// Report the state, take no action. This is the "observe, do not act" mode.
|
|
107
|
+
return {
|
|
108
|
+
state: "offline",
|
|
109
|
+
restored: false,
|
|
110
|
+
detail: `peer '${peerId}' is not registered and COGENT_AUTO_PRESENCE is off — this agent is absent from its channel`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
await deps.goOnline({
|
|
114
|
+
peerId,
|
|
115
|
+
endpoint: creds.endpoint,
|
|
116
|
+
sessionId: creds.sessionId,
|
|
117
|
+
token: creds.token,
|
|
118
|
+
cwd: deps.cwd(),
|
|
119
|
+
// 🔴 THE PEER'S DISPLAY LABEL IS NOT PERSISTED ANYWHERE — only the CHANNEL's is,
|
|
120
|
+
// in credentials.label (create-session.ts writes `label: data.label`; join-session
|
|
121
|
+
// writes the resolved channel label). Using that would label the agent after the
|
|
122
|
+
// channel: a restore once put "paymentorchestrator" beside a sibling called "POFE".
|
|
123
|
+
// peerId is the one identifier we hold that genuinely names this agent. The
|
|
124
|
+
// operator's chosen label returns on the next cogent_register_peer, which carries it.
|
|
125
|
+
label: peerId,
|
|
126
|
+
// 🔴 WITHOUT THIS THE RESTORE CANNOT RECLAIM ITS OWN PEER. A re-join issues a NEW
|
|
127
|
+
// token, and AUD-003 ownership is proven by the token that CREATED the peer or by
|
|
128
|
+
// the secret issued with it. Omitting it means an entry owned by an earlier token
|
|
129
|
+
// answers PEER_NOT_YOURS and the agent stays offline — precisely the state this
|
|
130
|
+
// module exists to leave. The secret is in the credential file; send it.
|
|
131
|
+
peerSecret: creds.peerSecret,
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
state: "online",
|
|
135
|
+
restored: true,
|
|
136
|
+
peerId,
|
|
137
|
+
// Says what THIS process did. Whether the relay now lists the peer is the relay's
|
|
138
|
+
// state, and asserting it from here would be a success nobody measured.
|
|
139
|
+
detail: `re-registered peer '${peerId}' from stored credentials`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
144
|
+
return {
|
|
145
|
+
state: "offline",
|
|
146
|
+
restored: false,
|
|
147
|
+
detail: `presence restore failed: ${reason}`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Last evaluated presence, so cogent_health_check can report the agent's own state
|
|
153
|
+
* without re-probing the relay.
|
|
154
|
+
*
|
|
155
|
+
* It lives HERE, in the leaf module, and not in startup.ts: a tool importing startup.ts
|
|
156
|
+
* creates an import cycle (tools -> startup -> tools) and the imported binding reads back
|
|
157
|
+
* `undefined` at call time. That cost 26 health-check tests once; do not move it.
|
|
158
|
+
*/
|
|
159
|
+
let lastPresence = null;
|
|
160
|
+
/** Record the evaluated presence. Called by startup after each evaluation. */
|
|
161
|
+
export function setPresence(outcome) {
|
|
162
|
+
lastPresence = outcome;
|
|
163
|
+
}
|
|
164
|
+
/** The agent's own view of its channel presence. Null until first evaluated. */
|
|
165
|
+
export function getPresence() {
|
|
166
|
+
return lastPresence;
|
|
167
|
+
}
|
|
168
|
+
/** Report the outcome once, at a level that matches how much it matters. */
|
|
169
|
+
export function logPresence(outcome) {
|
|
170
|
+
if (outcome.state === "online" && outcome.restored) {
|
|
171
|
+
logger.info(`Presence: offline -> online — ${outcome.detail}. This agent is addressable on its ` +
|
|
172
|
+
`channel again, and leftover bridges for this checkout are now reapable.`);
|
|
173
|
+
}
|
|
174
|
+
else if (outcome.state === "offline") {
|
|
175
|
+
logger.warn(`Presence: OFFLINE — ${outcome.detail}.`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
logger.debug(`Presence: ${outcome.state} — ${outcome.detail}.`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=peer-presence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peer-presence.js","sourceRoot":"","sources":["../../src/services/peer-presence.ts"],"names":[],"mappings":"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"}
|
package/dist/startup.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CloudWsClient, MessageInbox } from "./cloud/index.js";
|
|
2
2
|
import { HttpClient } from "./cloud/http-client.js";
|
|
3
|
+
import { type PresenceOutcome } from "./services/peer-presence.js";
|
|
3
4
|
export { autoRelay } from "./services/auto-relay.js";
|
|
4
5
|
export declare let cloudWsClient: CloudWsClient | null;
|
|
5
6
|
export declare let cloudInbox: MessageInbox | null;
|
|
@@ -18,6 +19,33 @@ export declare function setInboxNotifier(fn: () => void): void;
|
|
|
18
19
|
* Debounced: only one re-registration can run at a time.
|
|
19
20
|
*/
|
|
20
21
|
export declare function triggerReRegistration(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Evaluate this agent's PRESENCE on its channel and, when it is `offline` with a peer it
|
|
24
|
+
* owns, bring it back `online`. See services/peer-presence.ts for the state vocabulary,
|
|
25
|
+
* how an agent ends up offline, and why restoring it is safe.
|
|
26
|
+
*
|
|
27
|
+
* Fire-and-forget from index.ts AFTER the transport connects — it performs network I/O
|
|
28
|
+
* and must never precede the `initialize` handshake (scripts/startup-nonblocking-test.mjs).
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The exact positional argument tuple for a presence restore's registerPeer call.
|
|
32
|
+
*
|
|
33
|
+
* 🔴 EXTRACTED SO IT CAN BE TESTED. registerPeer's trailing parameters are OPTIONAL, and
|
|
34
|
+
* per the TypeScript vault (concepts/Optional_Parameters -> sources/.../0281) omitting one
|
|
35
|
+
* "will provide the function with an `undefined` value parameter" — silently. Measured:
|
|
36
|
+
* deleting `peerSecret` from the call site left BOTH `tsc --noEmit` AND the presence unit
|
|
37
|
+
* tests at exit 0, because those tests cover the module, not this wiring. A dropped
|
|
38
|
+
* ownership proof means a peer owned by an earlier token can never be reclaimed, and the
|
|
39
|
+
* agent stays offline. The type system cannot see it; this tuple + its test can.
|
|
40
|
+
*/
|
|
41
|
+
export declare function presenceRegisterArgs(p: {
|
|
42
|
+
peerId: string;
|
|
43
|
+
sessionId: string;
|
|
44
|
+
cwd: string;
|
|
45
|
+
label: string;
|
|
46
|
+
peerSecret?: string;
|
|
47
|
+
}): [string, string, string, string, string, "agent", undefined, undefined, string, string, string | undefined];
|
|
48
|
+
export declare function restorePeerPresence(): Promise<PresenceOutcome>;
|
|
21
49
|
/** Test seam: is a label still waiting to be resolved post-connect? */
|
|
22
50
|
export declare function hasPendingLabelResolve(): boolean;
|
|
23
51
|
/**
|
package/dist/startup.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEpD,OAAO,EAA6C,KAAK,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAa9G,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAiBrD,eAAO,IAAI,aAAa,EAAE,aAAa,GAAG,IAAW,CAAC;AACtD,eAAO,IAAI,UAAU,EAAE,YAAY,GAAG,IAAW,CAAC;AAClD,eAAO,IAAI,eAAe,EAAE,UAAU,GAAG,IAAW,CAAC;AAKrD;;;;GAIG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAErC;AASD,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAGrD;AAiHD;;;;GAIG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CA8C3D;AAED;;;;;;;GAOG;AACH;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAc9G;AAED,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,eAAe,CAAC,CAsCpE;AAsFD,uEAAuE;AACvE,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAsCzD;AAED,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAwCf;AAMD,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAqMhD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAkBnD"}
|
package/dist/startup.js
CHANGED
|
@@ -14,6 +14,8 @@ import { loadCredentials, hasJoinedInProcess, legacyGlobalCredentialsExist, } fr
|
|
|
14
14
|
import { CloudWsClient, MessageInbox } from "./cloud/index.js";
|
|
15
15
|
import { HttpClient } from "./cloud/http-client.js";
|
|
16
16
|
import { autoRelay } from "./services/auto-relay.js";
|
|
17
|
+
import { restorePresence, logPresence, setPresence } from "./services/peer-presence.js";
|
|
18
|
+
import { listLiveBridges } from "./services/wake-ownership.js";
|
|
17
19
|
import { heartbeat } from "./services/heartbeat.js";
|
|
18
20
|
import { preflightCodex, preflightCodexSandbox, preflightCodexServerCollision, } from "./services/codex-preflight.js";
|
|
19
21
|
import { setSandboxOverride } from "./services/codex-cli.js";
|
|
@@ -184,6 +186,81 @@ export async function triggerReRegistration() {
|
|
|
184
186
|
reRegistrationInProgress = false;
|
|
185
187
|
}
|
|
186
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Evaluate this agent's PRESENCE on its channel and, when it is `offline` with a peer it
|
|
191
|
+
* owns, bring it back `online`. See services/peer-presence.ts for the state vocabulary,
|
|
192
|
+
* how an agent ends up offline, and why restoring it is safe.
|
|
193
|
+
*
|
|
194
|
+
* Fire-and-forget from index.ts AFTER the transport connects — it performs network I/O
|
|
195
|
+
* and must never precede the `initialize` handshake (scripts/startup-nonblocking-test.mjs).
|
|
196
|
+
*/
|
|
197
|
+
/**
|
|
198
|
+
* The exact positional argument tuple for a presence restore's registerPeer call.
|
|
199
|
+
*
|
|
200
|
+
* 🔴 EXTRACTED SO IT CAN BE TESTED. registerPeer's trailing parameters are OPTIONAL, and
|
|
201
|
+
* per the TypeScript vault (concepts/Optional_Parameters -> sources/.../0281) omitting one
|
|
202
|
+
* "will provide the function with an `undefined` value parameter" — silently. Measured:
|
|
203
|
+
* deleting `peerSecret` from the call site left BOTH `tsc --noEmit` AND the presence unit
|
|
204
|
+
* tests at exit 0, because those tests cover the module, not this wiring. A dropped
|
|
205
|
+
* ownership proof means a peer owned by an earlier token can never be reclaimed, and the
|
|
206
|
+
* agent stays offline. The type system cannot see it; this tuple + its test can.
|
|
207
|
+
*/
|
|
208
|
+
export function presenceRegisterArgs(p) {
|
|
209
|
+
return [
|
|
210
|
+
p.peerId,
|
|
211
|
+
p.sessionId,
|
|
212
|
+
p.cwd,
|
|
213
|
+
p.label,
|
|
214
|
+
CLIENT_VERSION,
|
|
215
|
+
"agent",
|
|
216
|
+
undefined, // channelSessionId — not tracked on a presence restore
|
|
217
|
+
undefined, // capabilities — likewise
|
|
218
|
+
deriveWorkspaceId(p.cwd),
|
|
219
|
+
p.sessionId, // threadId
|
|
220
|
+
p.peerSecret, // AUD-003 ownership proof — reclaims a peer owned by an earlier token
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
export async function restorePeerPresence() {
|
|
224
|
+
const outcome = await restorePresence({
|
|
225
|
+
// The endpoint being a relay URL is what makes presence meaningful. We deliberately do
|
|
226
|
+
// NOT gate on the active backend being the cloud one: at startup it is still the
|
|
227
|
+
// deferred FileBackend placeholder, and gating on it made this whole path inert
|
|
228
|
+
// (measured: "skipped: not-cloud-mode" with valid credentials on disk).
|
|
229
|
+
isCloudConfigured: () => {
|
|
230
|
+
const ep = getConfig().COGENT_ENDPOINT;
|
|
231
|
+
return !!ep && isCloudEndpoint(ep);
|
|
232
|
+
},
|
|
233
|
+
alreadyRegisteredInProcess: () => autoRelay.getDiagnostics().registered,
|
|
234
|
+
peerAlreadyServed: (peerId) => {
|
|
235
|
+
try {
|
|
236
|
+
return listLiveBridges(process.cwd(), peerId).some((b) => b.pid !== process.pid);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return false; // unreadable registry ⇒ no proof of competition ⇒ proceed
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
loadCredentials: () => loadCredentials(),
|
|
243
|
+
cwd: () => process.cwd(),
|
|
244
|
+
goOnline: async ({ peerId, endpoint, sessionId, token, cwd, label, peerSecret }) => {
|
|
245
|
+
// Swap the deferred placeholder for a real cloud backend the supported way — the
|
|
246
|
+
// same call join-session uses. Building a second backend here instead would leave
|
|
247
|
+
// the tools on the placeholder and the presence path on another: a split brain.
|
|
248
|
+
await reinitCloudBackend(endpoint, sessionId, token);
|
|
249
|
+
const backend = getBackend();
|
|
250
|
+
await backend.registerPeer(...presenceRegisterArgs({ peerId, sessionId, cwd, label, peerSecret }));
|
|
251
|
+
// Wiring auto-relay is the POINT, not a side effect: setLocalPeer() is what calls
|
|
252
|
+
// announceBridge() and reapStaleBridges(), so restoring presence is also what makes
|
|
253
|
+
// leftover bridges for this checkout reapable again.
|
|
254
|
+
autoRelay.setLocalPeer(peerId, sessionId, cwd, "agent", label, sessionId);
|
|
255
|
+
if (cloudWsClient)
|
|
256
|
+
cloudWsClient.setPeerId(peerId);
|
|
257
|
+
heartbeat.restart();
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
setPresence(outcome);
|
|
261
|
+
logPresence(outcome);
|
|
262
|
+
return outcome;
|
|
263
|
+
}
|
|
187
264
|
// ---------------------------------------------------------------------------
|
|
188
265
|
// Helper to create a CloudWsClient with standard callbacks
|
|
189
266
|
// ---------------------------------------------------------------------------
|