@volter/supercode-orchestrator 0.5.204 → 0.5.206
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/README.md +138 -4
- package/activation.mjs +93 -2
- package/adapters/rh2.mjs +7 -2
- package/agents.mjs +24 -2
- package/apply/doors.mjs +25 -2
- package/bin/agent-channel.mjs +41 -22
- package/bin/connection-runner.mjs +141 -0
- package/bin/orchestrator.mjs +11 -2
- package/bin/runtime.mjs +16 -9
- package/board/args.mjs +3 -2
- package/board/attachment-read.mjs +77 -0
- package/board/call.mjs +4 -2
- package/board/capacity.mjs +46 -9
- package/board/ci.mjs +157 -53
- package/board/cli.mjs +225 -87
- package/board/client.mjs +18 -2
- package/board/custody.mjs +109 -0
- package/board/dispatch.mjs +297 -75
- package/board/doors.mjs +47 -17
- package/board/engine.mjs +78 -24
- package/board/fleet-listeners.mjs +268 -0
- package/board/fleet-observer.mjs +79 -0
- package/board/fleet-targets.mjs +82 -0
- package/board/incidents.mjs +7 -0
- package/board/listing.mjs +5 -0
- package/board/machine-fleet.mjs +7 -0
- package/board/notice-receipts.mjs +67 -0
- package/board/owner-door-worker.mjs +61 -0
- package/board/owner-door.mjs +46 -0
- package/board/owner-health.mjs +8 -2
- package/board/published.mjs +2 -2
- package/board/record-view.mjs +2 -0
- package/board/store.mjs +55 -3
- package/board/tools.mjs +11 -4
- package/board/workflow.mjs +91 -28
- package/board/ztrack.mjs +87 -21
- package/board-input.d.ts +2 -0
- package/board-input.mjs +16 -0
- package/connection.mjs +153 -0
- package/index.d.ts +7 -5
- package/index.mjs +1 -1
- package/installed-apps.mjs +31 -20
- package/loop.mjs +91 -36
- package/mcp/bridge.mjs +12 -0
- package/mcp/hermes-kanban-tools.json +22 -14
- package/mcp/mount.mjs +10 -9
- package/ontology.d.ts +37 -0
- package/operator.mjs +72 -4
- package/orchestration.mjs +7 -2
- package/package.json +9 -5
- package/reducer.mjs +74 -5
package/README.md
CHANGED
|
@@ -4,6 +4,21 @@ This is currently a private workspace package. Examples assume the checkout's
|
|
|
4
4
|
workspace dependencies and a built Volter Harness CLI; the package name is not a claim
|
|
5
5
|
that an installable npm release exists.
|
|
6
6
|
|
|
7
|
+
Verified worker binding replaces superseded worker notice subscriptions across
|
|
8
|
+
an arc and its subtasks. Native notices wait with their event cursor intact when
|
|
9
|
+
liveness is unread; agent and board mailboxes keep their independent routes.
|
|
10
|
+
Read the [worker-notice contract](../../docs/reference/board-worker-notices.md)
|
|
11
|
+
and [retained subscription evidence](../../docs/evidence/board-worker-notices-2026-10-09.md)
|
|
12
|
+
for source behavior and the remaining native/publication qualification.
|
|
13
|
+
|
|
14
|
+
`supercode workflow claim <card>` opens a manual run and commits the declared
|
|
15
|
+
`running` workflow stage with its claim. Readiness is checked against the
|
|
16
|
+
workflow stage; the home must declare `running`. Creating with
|
|
17
|
+
`--initial-status running` uses the same path. Subsequent `block` follows that
|
|
18
|
+
stage's own transition, which ends the run in the default workflow. See the
|
|
19
|
+
[private manual reading](../../docs/evidence/board-manual-claim-2026-10-09.md)
|
|
20
|
+
for scope and limits; existing inconsistent claims are not repaired automatically.
|
|
21
|
+
|
|
7
22
|
The orchestrator's runtime over the ontology: one typed operational model
|
|
8
23
|
(`OrchestratorState` = an orchestration value plus a runtime half) whose folder is its serialization.
|
|
9
24
|
`docs/ORCHESTRATOR-IR.md` is normative. The orchestration value and every codec are the ontology's, in
|
|
@@ -42,6 +57,48 @@ Rules the codec keeps (enforced in Rust, observed here):
|
|
|
42
57
|
since import (UNI-22); it copies the store byte for byte when they did not.
|
|
43
58
|
- Files the IR does not model are never touched by `save` and are carried by path on a migration.
|
|
44
59
|
|
|
60
|
+
## Board machine-call deadlines
|
|
61
|
+
|
|
62
|
+
The board owner's machine calls share one timeout budget across channel admission,
|
|
63
|
+
node or harness admission, and the method request. A call whose admission expires
|
|
64
|
+
never sends the method after its caller has stopped waiting. It leaves the owner's
|
|
65
|
+
shared channel and clients available to other callers. Once a method request has
|
|
66
|
+
started, a timeout reports that whether it ran is unknown.
|
|
67
|
+
|
|
68
|
+
Timing steps name each admission phase and the request. The SDK request receives
|
|
69
|
+
the remaining budget and cancellation signal; node requests receive the remaining
|
|
70
|
+
budget, with their existing request/reply cleanup. The workflow caller also records
|
|
71
|
+
session lookup, request preparation and the wait for the owner's answer, including
|
|
72
|
+
an owner call that ends by throwing. These steps contain durations, not request bodies.
|
|
73
|
+
These bounds do not establish
|
|
74
|
+
subsecond board verbs or identify the cause of a historical slow call. The retained
|
|
75
|
+
readings and limits are in
|
|
76
|
+
[board call admission evidence](../../docs/evidence/board-call-admission-2026-10-10.md).
|
|
77
|
+
|
|
78
|
+
## Read a card's attachment
|
|
79
|
+
|
|
80
|
+
List a card's recorded uploads, then download one onto the calling machine:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
supercode workflow attachments <card> --at sc:<host>:board:<name> --json
|
|
84
|
+
supercode workflow attachment-get <card> <attachment-id> --at sc:<host>:board:<name> --out ./artifact.png
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The npm front door serves `attachment-get`. Without `--at`, use `--root <home>`
|
|
88
|
+
and optionally `--board <slug>`. `--out` names a local file; its parent directory
|
|
89
|
+
must exist. A completed download replaces that file. An interrupted download
|
|
90
|
+
retains a separate partial file and reports its path; there is no automatic resume.
|
|
91
|
+
The board host needs an orchestrator that serves `attachment-read` and its normal
|
|
92
|
+
running board owner. Remote reads keep the board door's caller and grant checks.
|
|
93
|
+
|
|
94
|
+
`workflow attachment-read <card> <attachment-id>` returns JSON with at most 1 MiB
|
|
95
|
+
of `content_base64`, metadata, offsets, `eof` and a filesystem metadata `version`.
|
|
96
|
+
Subsequent reads pass `--offset`, `--length` and the first response's `--version`.
|
|
97
|
+
It accepts a recorded attachment id rather than an arbitrary host path. Version
|
|
98
|
+
checking detects metadata changes; it is not a content digest or writer attestation.
|
|
99
|
+
The worker's existing `kanban_attachments` MCP tool lists uploads as before;
|
|
100
|
+
adding `attachment_id`, and optionally `offset`, `length` and `version`, reads a
|
|
101
|
+
range under the same worker scope. The maximum `length` is 1,048,576 bytes.
|
|
45
102
|
## Board GitHub REST receipts
|
|
46
103
|
|
|
47
104
|
`supercode workflow github-usage --since 1h --json` reads this user's local
|
|
@@ -140,7 +197,7 @@ Tiers on the way out:
|
|
|
140
197
|
too, rather than written as its own reference.
|
|
141
198
|
|
|
142
199
|
Secrets never enter the model: any string under a credential-shaped key — openclaw's own rule
|
|
143
|
-
(`
|
|
200
|
+
(`orchestration/codec/openclaw.rs::is_credential_key`: the lower-cased key ENDS WITH `token|key|secret|password|
|
|
144
201
|
credential`), minus the names it spells that way for non-secrets (`sessionKey`, `store_key`, …) —
|
|
145
202
|
moves to the vault as a `{dotenv}` ref and is re-inlined on export where openclaw reads it.
|
|
146
203
|
|
|
@@ -169,6 +226,12 @@ await orch.dispatch(events.tick(now)); // the clock is an event too
|
|
|
169
226
|
adapters (`deliver`). A worker runs with the profile folder as cwd and the profile's harness home
|
|
170
227
|
inside it (`activationEnv`), so its AGENTS.md is in context by the ordinary instruction-file walk.
|
|
171
228
|
|
|
229
|
+
The board's held profile view follows root/profile `AGENTS.md` changes, including
|
|
230
|
+
linked targets, and reloads on its next call. The current call retains its basis.
|
|
231
|
+
This file refresh does not replace instructions in a running native session;
|
|
232
|
+
[c60's source record](../../docs/evidence/persona-file-invalidation-2026-10-10.md)
|
|
233
|
+
keeps running-main delivery and readback open.
|
|
234
|
+
|
|
172
235
|
## Cron and webhooks (ORC-5)
|
|
173
236
|
|
|
174
237
|
```js
|
|
@@ -308,6 +371,29 @@ Tools, one per verb, names and argument shapes transcribed from Hermes's `tools/
|
|
|
308
371
|
`supercode mcp serve` (the coding tools) is a different server for a different purpose and is
|
|
309
372
|
untouched.
|
|
310
373
|
|
|
374
|
+
## Board model selection and native refusals
|
|
375
|
+
|
|
376
|
+
Board pane lanes take the card's model override, then `worker.model`, then the
|
|
377
|
+
profile's `model.default` when the harness accepts a native model flag. With no
|
|
378
|
+
declared model the harness retains its own default. For a fresh Claude launch,
|
|
379
|
+
the dispatcher reads the native first assistant outcome through the session door:
|
|
380
|
+
an API error fails the run even if the terminal is still alive. The workflow's
|
|
381
|
+
existing incident policy decides the card state and recovery; a monitor board
|
|
382
|
+
retains the failed run and holds dispatch without closing its pane. This requires
|
|
383
|
+
the matching TypeScript session reader and is not resumed-turn or general
|
|
384
|
+
completion monitoring. See the [owned Mac reading](../../docs/evidence/board-claude-model-first-response-2026-10-09.md).
|
|
385
|
+
|
|
386
|
+
## Board reviewer selection
|
|
387
|
+
|
|
388
|
+
The workflow can select a reviewer from the latest coding run's harness, while
|
|
389
|
+
retaining a card's or request's explicit profile. Review-machine choices stay in
|
|
390
|
+
workflow parameters. Default same-harness review retains the coding model; an
|
|
391
|
+
explicit reviewer uses its profile's model. The review request records the
|
|
392
|
+
implementer's machine and model/provider overrides for a return with changes.
|
|
393
|
+
See [the configuration contract](../../docs/reference/board-reviewers.md) and
|
|
394
|
+
[source qualification](../../docs/evidence/board-reviewer-selection-2026-10-09.md).
|
|
395
|
+
A custom home needs its own workflow update; this source does not rewrite it.
|
|
396
|
+
|
|
311
397
|
## Acceptance (ORC-8)
|
|
312
398
|
|
|
313
399
|
The bar of `docs/ORCHESTRATOR-IR.md` §8 — one scripted week of realistic use replayed against
|
|
@@ -358,11 +444,12 @@ it as unsupported. Native Hermes channel configuration remains the worker's conf
|
|
|
358
444
|
|
|
359
445
|
A mail agent the RH2 organization declares is one participant there, as its own agent principal: an admin issues that
|
|
360
446
|
agent a token at RH2's agent-token door, and the home keeps it for the agent's channel, which then joins its Room's
|
|
361
|
-
conversation answering every line:
|
|
447
|
+
conversation answering every line. The record names the one agent it is (`--agent`, its Teams id): it is mounted in
|
|
448
|
+
that agent's own sessions as the `rh2-agent` MCP server, and never in another agent's that shares its name or profile:
|
|
362
449
|
|
|
363
450
|
```sh
|
|
364
451
|
supercode orchestrator apps install rh2-agent --origin https://pilot.runhuman.com --organization volter \
|
|
365
|
-
--principal prn_… --token-file ./account-manager.token --profile account-manager
|
|
452
|
+
--principal prn_… --agent agent_… --token-file ./account-manager.token --profile account-manager
|
|
366
453
|
supercode orchestrator apps remove rh2-agent.account-manager
|
|
367
454
|
```
|
|
368
455
|
|
|
@@ -490,6 +577,30 @@ mark without the notice — nobody is reading them.
|
|
|
490
577
|
The repo's Hermes fixture home (`crates/harness/fixtures/hermes_home`) is a ready home to drive it
|
|
491
578
|
against by hand; that needs `target/debug/supercode` built.
|
|
492
579
|
|
|
580
|
+
## Board session cleanup
|
|
581
|
+
|
|
582
|
+
A manager or creator changing a card's assignee can release its ended worker
|
|
583
|
+
from a `needs_input` dialog. The board requires its retained reassignment and
|
|
584
|
+
release, a fresh reading of the exact harness/session, and an unexpired close
|
|
585
|
+
claim. It closes the pane through the owning board's door and retains the
|
|
586
|
+
reading; it sends no dialog key or approval response. Working turns, adopted sessions and panes held by an active successor keep
|
|
587
|
+
their existing protection. Unknown activity retains main’s native-record quiet
|
|
588
|
+
watch; this exception requires a positive needs_input reading for the exact
|
|
589
|
+
launched worker. The candidate's [source and package record](../../docs/evidence/board-reassignment-input-dialog-2026-10-10.md)
|
|
590
|
+
leaves native closing and replacement-launch qualification open.
|
|
591
|
+
|
|
592
|
+
## Remote card text
|
|
593
|
+
|
|
594
|
+
The npm front door's `workflow … --at sc:<machine>:board:<name>` uses
|
|
595
|
+
`prepareBoardText(argv)` from `@volter/supercode-orchestrator/board-input` before calling
|
|
596
|
+
the machine's board door. It returns `{ argv, stdin }`: the board parser selects a
|
|
597
|
+
comment's `--file` or a create/specify verb's `--body-file`, reads that file on the
|
|
598
|
+
caller (`-` reads standard input), and appends the selected file flag with `-`.
|
|
599
|
+
The last flag value wins, preserving the other arguments. Other verbs and help
|
|
600
|
+
do not read a text file. The helper opens no transport and makes no card write;
|
|
601
|
+
the daemon carries the input to the board owner under the caller's existing grants
|
|
602
|
+
and card permissions. See the [Teams guide](../../docs/guides/teams.md#call-a-board-on-another-machine).
|
|
603
|
+
|
|
493
604
|
## The operator door (ORC-13)
|
|
494
605
|
|
|
495
606
|
The orchestrator's WRITE door — what `supercode jobs|profiles|sessions <verb> --harness orchestrator`
|
|
@@ -510,7 +621,9 @@ out {"ok":true,"result":{"ran":"created cron job job_… …","job_id":"…","j
|
|
|
510
621
|
```
|
|
511
622
|
|
|
512
623
|
`op` is one of `jobs.create|update|pause|resume|run|delete`, `profiles.create|delete`,
|
|
513
|
-
`sessions.new|reset`, `
|
|
624
|
+
`sessions.new|reset`, `sessions.launch|launch_status|launch_close` (live only: a concurrent session of the profile's
|
|
625
|
+
agent started on request, under a launch key; `supercode agent launch` drives them; a launch's refusal is answered
|
|
626
|
+
`{ok:false, error, code}` with `code` `conflict`, `budget` or `invalid_request`), `access.pair_approve`. The verb dispatches its `events.operator(...)` on the
|
|
514
627
|
**loop's own queue**, so an operator write is ordered against inbound messages and ticks like
|
|
515
628
|
anything else and the running loop's state and the folder can never disagree. `result` re-reads the
|
|
516
629
|
affected record out of state after the reducer ran.
|
|
@@ -579,3 +692,24 @@ root/executor is refused; the lease is retained until shutdown completes. The
|
|
|
579
692
|
helper does not clear a live lease or escalate to SIGKILL. Embedders can stop their
|
|
580
693
|
owned clock before disabling the integration and start it again on the next boot.
|
|
581
694
|
Hosted HTTP refusal messages preserve the executor's bounded JSON error reason.
|
|
695
|
+
|
|
696
|
+
Review source metadata admits four active GitHub reads and 64 waiting owners,
|
|
697
|
+
using four ordered source workers and a shared stored-token lookup. A source
|
|
698
|
+
verification reuses its one PR observation, retaining head and authority
|
|
699
|
+
checks; failed fetches retain their cause and available native code.
|
|
700
|
+
[The source record](../../docs/evidence/board-review-metadata-admission-2026-10-12.md)
|
|
701
|
+
states the current handoff and adoption limits.
|
|
702
|
+
|
|
703
|
+
## Board session custody
|
|
704
|
+
|
|
705
|
+
The candidate Node board CLI adds `supercode workflow detach <card> --to person:<id>`.
|
|
706
|
+
It retains the native identity, records the person's custody and removes the
|
|
707
|
+
dispatcher run, subscriptions and capacity counts without closing the session.
|
|
708
|
+
Queued effects and later continuation cannot take it back; the existing `adopt`
|
|
709
|
+
door must verify a live native session before the board owns it again.
|
|
710
|
+
|
|
711
|
+
The [command contract](../../docs/reference/board-session-detach.md) describes
|
|
712
|
+
authority, in-progress-operation refusals and already-issued-request boundaries.
|
|
713
|
+
[Source observations](../../docs/evidence/board-session-detach-2026-10-09.md)
|
|
714
|
+
cover actual help, compilation and read-only retained records. Live handback,
|
|
715
|
+
return through adoption and released availability remain unqualified.
|
package/activation.mjs
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// The profile folder is a complete home; each harness's config home is a
|
|
3
3
|
// subfolder named by its id, pointed at through the harness's own env var.
|
|
4
4
|
|
|
5
|
-
import { join, relative, resolve } from 'node:path';
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
|
-
import { mkdirSync, readFileSync, readdirSync, lstatSync, rmSync, writeFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { mkdirSync, readFileSync, readdirSync, lstatSync, rmSync, writeFileSync, existsSync, statSync, realpathSync } from 'node:fs';
|
|
8
8
|
import { CLAUDE_BRIDGE, linkTo, skillSources } from './hermes-home.mjs';
|
|
9
9
|
import { ownLogin } from './worker-route.mjs';
|
|
10
10
|
import { execFile } from 'node:child_process';
|
|
@@ -73,10 +73,101 @@ export function declareWorkerHome({ harness, env, cwd, log = () => {} }) {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* The folder a worker runs in, made one of this machine's Teams sync roots when none covers it, through the connector's
|
|
78
|
+
* own door: `supercode teams connect --context <context> --cwd <folder> --add-root --exact-folder`, the restricted rule a
|
|
79
|
+
* person's `--add-root` makes, on a root that covers that one folder and nothing under it (`exact_only`: the connector
|
|
80
|
+
* captures a session only when its recorded folder is that folder, after realpath). A worker's session is recorded in the
|
|
81
|
+
* folder it was started in, so the exact root captures it, and no subfolder is published by it. Whether a root covers
|
|
82
|
+
* the folder is the CLI's own rule (`teams roots --cwd`: rootCovers in roots.mjs, as `agent launch --status` reads it).
|
|
83
|
+
*
|
|
84
|
+
* A folder a root already covers (one a person connected by hand, wherever it is) is captured, and nothing more is done.
|
|
85
|
+
* Otherwise one folder is still every session recorded in it, so only a folder the orchestrator owns is connected: one
|
|
86
|
+
* strictly inside its home (`root`), after realpath, that is no profile's own folder. The home itself is refused
|
|
87
|
+
* (`default` with no `worker.cwd` runs there, and a person's own session started at the home's top level would be
|
|
88
|
+
* published with it), every folder outside the home (an absolute or `..` `worker.cwd`: a checkout or a parent a person
|
|
89
|
+
* works in), and every profile's folder (`profileDirs`: a profile with no `worker.cwd` runs there, where people edit
|
|
90
|
+
* the profile in sessions of their own). A refusal says which `worker.cwd` to set; a person may connect such a folder by
|
|
91
|
+
* hand.
|
|
92
|
+
*
|
|
93
|
+
* The context is the orchestrator's (`SUPERCODE_CONTEXT`), whose connector the root goes to. A launch into a folder
|
|
94
|
+
* outside every root ran, was never captured, and its launch read `starting` until the Workplace integration gave it up
|
|
95
|
+
* (volter-org/home/workspace, 2026-10-15). Once per folder per process, off the launch's path; a refusal (not the
|
|
96
|
+
* orchestrator's folder, no context, no installed connector) is said once and held REFUSAL_HOLD_MS before it is asked
|
|
97
|
+
* again.
|
|
98
|
+
*/
|
|
99
|
+
const coveredFolders = new Map();
|
|
100
|
+
const REFUSAL_HOLD_MS = 5 * 60_000;
|
|
101
|
+
export function declareWorkerFolder({ cwd, root, profile = null, profileDirs = [], context = process.env.SUPERCODE_CONTEXT, scope = null, log = () => {} }) {
|
|
102
|
+
let folder, home;
|
|
103
|
+
try { folder = realpathSync(cwd); home = realpathSync(root); } catch { return Promise.resolve(false); }
|
|
104
|
+
const held = coveredFolders.get(folder);
|
|
105
|
+
if (held && (held.until === undefined || Date.now() < held.until)) return held.answer;
|
|
106
|
+
const program = process.env.SUPERCODE_BIN || 'supercode';
|
|
107
|
+
const run = (args) => new Promise((done) => execFile(program, args, { timeout: 30_000 }, (error, stdout, stderr) => done({ error, stdout, stderr })));
|
|
108
|
+
const said = (answer) => String(answer.stderr ?? '').trim().split('\n')
|
|
109
|
+
.filter((line) => line && !/^\(node:\d+\) ExperimentalWarning: SQLite|^\(Use `node --trace-warnings/.test(line)).at(-1)
|
|
110
|
+
?? (answer.error?.code != null ? `exited ${answer.error.code}` : 'failed');
|
|
111
|
+
const refused = (why) => { log('warn', `worker folder ${folder}: its sessions are not captured by Teams: ${why}`); return false; };
|
|
112
|
+
const entry = {};
|
|
113
|
+
entry.answer = (async () => {
|
|
114
|
+
if (!context) return refused('no Teams context names where to connect it (SUPERCODE_CONTEXT)');
|
|
115
|
+
if (scope?.offline) return refused(`context ${context} is not a Teams context`);
|
|
116
|
+
// a root that covers it already (one a person connected by hand, wherever the folder is) captures it: nothing to do
|
|
117
|
+
const roots = await run(['teams', 'roots', '--json', '--context', context, '--cwd', folder]);
|
|
118
|
+
let covering = null;
|
|
119
|
+
try { covering = JSON.parse(roots.stdout).data.covering; } catch { /* unread: the connect below says what holds it */ }
|
|
120
|
+
if (Array.isArray(covering) && covering.length) return true;
|
|
121
|
+
// connected on a person's behalf only when the folder is the orchestrator's own: strictly inside its home, and not
|
|
122
|
+
// a profile's own folder (where people edit the profile, its AGENTS.md, in sessions of their own)
|
|
123
|
+
const within = relative(home, folder);
|
|
124
|
+
const set = `set worker.cwd for ${profile ? `profile ${profile}` : 'its profile'} to a folder of its own (for example \`worker.cwd: workspace\`), or connect this folder yourself (\`supercode teams connect --cwd ${folder} --add-root --exact-folder\`)`;
|
|
125
|
+
if (within === '') return refused(`it is the orchestrator home itself, where people start their own sessions, and an exact root there would publish theirs too: ${set}`);
|
|
126
|
+
if (within === '..' || within.startsWith(`..${sep}`) || isAbsolute(within)) return refused(`it is outside the orchestrator home ${home}, a folder people may work in, and is never connected on their behalf: ${set}`);
|
|
127
|
+
const real = (dir) => { try { return realpathSync(dir); } catch { return null; } };
|
|
128
|
+
if (profileDirs.some((dir) => dir && real(dir) === folder)) return refused(`it is a profile's own folder, where people edit the profile in sessions of their own, and an exact root there would publish theirs too: ${set}`);
|
|
129
|
+
const connected = await run(['teams', 'connect', '--context', context, '--cwd', folder, '--add-root', '--exact-folder']);
|
|
130
|
+
if (connected.error) return refused(`it could not be connected: ${said(connected)}`);
|
|
131
|
+
log('info', `worker folder ${folder} connected to Teams as an exact-folder sync root (its sessions are captured from the connector's next heartbeat)`);
|
|
132
|
+
return true;
|
|
133
|
+
})();
|
|
134
|
+
coveredFolders.set(folder, entry);
|
|
135
|
+
// a refusal is held a few minutes, then asked again by the next start; a covered folder stays covered for the process
|
|
136
|
+
void entry.answer.then((covered) => { if (!covered) entry.until = Date.now() + REFUSAL_HOLD_MS; });
|
|
137
|
+
return entry.answer;
|
|
138
|
+
}
|
|
139
|
+
|
|
76
140
|
export function activationCwd(profile, workdir) {
|
|
77
141
|
return resolve(profile.dir, workdir ?? profile.worker?.cwd ?? '.');
|
|
78
142
|
}
|
|
79
143
|
|
|
144
|
+
/**
|
|
145
|
+
* The folder a worker starts in, ready: `activationCwd`. A profile is a folder its worker runs in, or a subfolder of
|
|
146
|
+
* it (docs/architecture/orchestrator.md §0.3, `cwd: RelPath`, relative to the profile dir): a subfolder the profile names
|
|
147
|
+
* and lacks is the profile's own, and is made, empty. A folder outside the profile's (an absolute path, or one reached
|
|
148
|
+
* through `..`) is someone else's, and is never made here: a start refuses, naming it. Before this a missing cwd failed
|
|
149
|
+
* the start inside harness serve, and nothing kept why.
|
|
150
|
+
*/
|
|
151
|
+
export function workerCwd(profile, workdir) {
|
|
152
|
+
const cwd = activationCwd(profile, workdir);
|
|
153
|
+
if (existsSync(cwd)) {
|
|
154
|
+
if (!statSync(cwd).isDirectory()) throw new Error(`the worker's cwd ${cwd} (profile ${profile.name}) is not a folder`);
|
|
155
|
+
return cwd;
|
|
156
|
+
}
|
|
157
|
+
// Inside is judged where the folder would really be made: its deepest existing ancestor, symbolic links resolved,
|
|
158
|
+
// with the missing rest under it, against the profile's own folder resolved the same way. A link in the profile's
|
|
159
|
+
// folder to another (profile/repo -> a checkout) puts what is made under it in that other folder, never the profile's.
|
|
160
|
+
let ancestor = cwd;
|
|
161
|
+
while (!existsSync(ancestor) && dirname(ancestor) !== ancestor) ancestor = dirname(ancestor);
|
|
162
|
+
const target = join(realpathSync(ancestor), relative(ancestor, cwd));
|
|
163
|
+
const inside = relative(realpathSync(profile.dir), target);
|
|
164
|
+
if (!inside || inside === '..' || inside.startsWith(`..${sep}`) || isAbsolute(inside)) {
|
|
165
|
+
throw new Error(`the worker's cwd ${cwd} (worker.cwd ${JSON.stringify(workdir ?? profile.worker?.cwd ?? '.')} in profile ${profile.name}) does not exist; it is outside the profile's folder${target === cwd ? '' : ` (it would be made at ${target})`}, so it is not made here: create it, or name another`);
|
|
166
|
+
}
|
|
167
|
+
mkdirSync(target, { recursive: true });
|
|
168
|
+
return cwd;
|
|
169
|
+
}
|
|
170
|
+
|
|
80
171
|
/**
|
|
81
172
|
* Where each harness reads a persona from any cwd, and in what form: its config home's own instruction
|
|
82
173
|
* file. A worker often runs in a project checkout outside the home, where the project walk never reaches
|
package/adapters/rh2.mjs
CHANGED
|
@@ -49,7 +49,8 @@ export class Rh2Adapter {
|
|
|
49
49
|
this.running = false;
|
|
50
50
|
/** conversation id -> { roomId, after } */
|
|
51
51
|
this.cursors = new Map();
|
|
52
|
-
/** room id -> { name, names: Map<principalId, displayName>, conversations: Map<id, name
|
|
52
|
+
/** room id -> { name, names: Map<principalId, displayName>, conversations: Map<id, name>, seated: whether the Workplace
|
|
53
|
+
* delivers this Room's lines to this principal's own session } */
|
|
53
54
|
this.rooms = new Map();
|
|
54
55
|
/** message id -> the root of the thread it is in (its own id for a root). */
|
|
55
56
|
this.threadOf = new Map();
|
|
@@ -158,6 +159,9 @@ export class Rh2Adapter {
|
|
|
158
159
|
const room = view.room ?? {};
|
|
159
160
|
if (room.state && room.state !== 'active') continue;
|
|
160
161
|
const names = new Map((view.members ?? []).filter((m) => m.principalId).map((m) => [String(m.principalId), String(m.displayName ?? m.principalId)]));
|
|
162
|
+
// Where this principal's own seat carries a session, the Workplace delivers the Room's lines to that session itself
|
|
163
|
+
// (runhuman-2 ADR 0046 decision 4); this adapter then hands none of them on, so no line reaches it twice.
|
|
164
|
+
const seated = (view.members ?? []).some((m) => String(m.principalId) === this.principalId() && m.sessionBinding);
|
|
161
165
|
const conversations = new Map();
|
|
162
166
|
for (const conversation of view.conversations ?? []) {
|
|
163
167
|
if (!conversation.conversationId || !(conversation.permissions ?? []).includes('conversation.read')) continue;
|
|
@@ -174,7 +178,7 @@ export class Rh2Adapter {
|
|
|
174
178
|
: { roomId, after: 0, since: previous - CLOCK_SKEW_MS });
|
|
175
179
|
}
|
|
176
180
|
}
|
|
177
|
-
this.rooms.set(roomId, { name: String(room.name ?? roomId), names, conversations });
|
|
181
|
+
this.rooms.set(roomId, { name: String(room.name ?? roomId), names, conversations, seated });
|
|
178
182
|
}
|
|
179
183
|
for (const id of [...this.cursors.keys()]) if (!live.has(id)) this.cursors.delete(id);
|
|
180
184
|
}
|
|
@@ -242,6 +246,7 @@ export class Rh2Adapter {
|
|
|
242
246
|
if (this.threadOf.size > THREAD_MEMORY) this.threadOf.delete(this.threadOf.keys().next().value);
|
|
243
247
|
}
|
|
244
248
|
if (!isMe && conversation?.answers !== 'all' && !mentioned) return;
|
|
249
|
+
if (!isMe && room?.seated) return;
|
|
245
250
|
const chatId = `${roomId}/${conversationId}`;
|
|
246
251
|
// A person's line opens a turn: show typing here until the reply lands (the loop has no
|
|
247
252
|
// typing effect by design — the adapter, which knows both ends of the turn, carries it).
|
package/agents.mjs
CHANGED
|
@@ -26,6 +26,9 @@ function store(root){
|
|
|
26
26
|
CREATE TABLE IF NOT EXISTS agent_runtime_sessions(org TEXT NOT NULL,runtime_ref TEXT NOT NULL,session_ref TEXT NOT NULL,PRIMARY KEY(org,runtime_ref));
|
|
27
27
|
CREATE TABLE IF NOT EXISTS agent_runs(org TEXT NOT NULL,agent_id TEXT NOT NULL,session_ref TEXT NOT NULL,machine TEXT,run_id TEXT,profile_json TEXT,ended_at INTEGER,end_reason TEXT,main INTEGER NOT NULL DEFAULT 1,starter_session TEXT,usage_json TEXT,PRIMARY KEY(org,session_ref));
|
|
28
28
|
CREATE TABLE IF NOT EXISTS agent_usage_unread(org TEXT NOT NULL,session_ref TEXT NOT NULL,stamp TEXT NOT NULL,PRIMARY KEY(org,session_ref));`);
|
|
29
|
+
const columns=new Set(db.prepare('PRAGMA table_info(agent_runs)').all().map(row=>row.name));
|
|
30
|
+
if(!columns.has('conversation'))db.exec('ALTER TABLE agent_runs ADD COLUMN conversation INTEGER NOT NULL DEFAULT 0');
|
|
31
|
+
if(!columns.has('started_at'))db.exec('ALTER TABLE agent_runs ADD COLUMN started_at INTEGER NOT NULL DEFAULT 0');
|
|
29
32
|
db.prepare('DELETE FROM agent_card_assignment_changes WHERE recorded=1').run();
|
|
30
33
|
return db;
|
|
31
34
|
}
|
|
@@ -222,6 +225,19 @@ export function localAgentRecord(root,org,id){
|
|
|
222
225
|
sponsor:declaration.sponsor??null,profile:intent.profile_ref,state:'active',synced:!!intent.declared,main_session:null,revision:0},
|
|
223
226
|
intended:intent.profile,assignments:intent.assignments,org,synced:!!intent.declared};
|
|
224
227
|
}
|
|
228
|
+
/** The id a home's mail agent runs as, read and never minted: its own declared identity (`agent/<name>`) where the home
|
|
229
|
+
* holds one, else its profile's (`profile/<profile>`), as localProfileAgent keys them, each at its current generation.
|
|
230
|
+
* Null when the home holds neither, or has no agent store. A channel names its agent by this id (agent-channel). */
|
|
231
|
+
export function homeAgentId(root,{org,server,name,profile=name}){
|
|
232
|
+
if(!existsSync(join(root,'agent-state.sqlite')))return null;
|
|
233
|
+
const db=store(root);
|
|
234
|
+
try{
|
|
235
|
+
const at=(key)=>{const stable=`${server}/${org}/${key}`;
|
|
236
|
+
return db.prepare('SELECT agent_id FROM agent_declaration_heads WHERE org=? AND key=?').get(org,stable)?.agent_id
|
|
237
|
+
??db.prepare('SELECT id FROM agent_intents WHERE org=? AND key=?').get(org,stable)?.id??null;};
|
|
238
|
+
return (name!==profile?at(`agent/${name}`):null)??at(`profile/${profile}`);
|
|
239
|
+
}finally{db.close();}
|
|
240
|
+
}
|
|
225
241
|
/** Earlier sessions this home ran for an agent, for a relaunch's pointers when Teams cannot list them. */
|
|
226
242
|
export function localAgentSessions(root,org,id){
|
|
227
243
|
const db=store(root);try{return db.prepare('SELECT session_ref AS session,machine,ended_at,end_reason FROM agent_runs WHERE org=? AND agent_id=? ORDER BY rowid DESC LIMIT 200').all(org,id);}finally{db.close();}
|
|
@@ -377,7 +393,7 @@ export function recordNativeAgentSession(root,runtimeRef,session) {
|
|
|
377
393
|
const owner=agentRunForSession(root,[runtimeRef]);
|
|
378
394
|
if(!owner||owner.session_ref===session)return owner;
|
|
379
395
|
recordAgentRun(root,owner.org,owner.agent_id,{session,machine:owner.machine,run:owner.run_id,
|
|
380
|
-
profile:owner.profile_json?JSON.parse(owner.profile_json):null,main:owner.main===1,starterSession:owner.starter_session});
|
|
396
|
+
profile:owner.profile_json?JSON.parse(owner.profile_json):null,main:owner.main===1,starterSession:owner.starter_session,conversation:owner.conversation===1});
|
|
381
397
|
const db=store(root);try{
|
|
382
398
|
db.exec('BEGIN IMMEDIATE');
|
|
383
399
|
db.prepare('INSERT INTO agent_runtime_sessions(org,runtime_ref,session_ref) VALUES (?,?,?) ON CONFLICT(org,runtime_ref) DO UPDATE SET session_ref=excluded.session_ref').run(owner.org,runtimeRef,session);
|
|
@@ -387,12 +403,13 @@ export function recordNativeAgentSession(root,runtimeRef,session) {
|
|
|
387
403
|
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
|
|
388
404
|
return agentRunForSession(root,[runtimeRef]);
|
|
389
405
|
}
|
|
390
|
-
export function recordAgentRun(root,org,id,{session,machine=null,run,profile=null,main=true,starterSession=null}){
|
|
406
|
+
export function recordAgentRun(root,org,id,{session,machine=null,run,profile=null,main=true,starterSession=null,conversation=false}){
|
|
391
407
|
const db=store(root);try{
|
|
392
408
|
db.exec('BEGIN IMMEDIATE');
|
|
393
409
|
const old=db.prepare('SELECT agent_id FROM agent_runs WHERE org=? AND session_ref=?').get(org,session);
|
|
394
410
|
if(old&&old.agent_id!==id)throw new Error('local session ownership cannot change agent identity');
|
|
395
411
|
db.prepare('INSERT INTO agent_runs(org,agent_id,session_ref,machine,run_id,profile_json,main,starter_session) VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(org,session_ref) DO UPDATE SET machine=excluded.machine,run_id=excluded.run_id,profile_json=COALESCE(excluded.profile_json,agent_runs.profile_json),main=excluded.main,starter_session=COALESCE(agent_runs.starter_session,excluded.starter_session) WHERE agent_runs.machine IS NOT excluded.machine OR agent_runs.run_id IS NOT excluded.run_id OR (excluded.profile_json IS NOT NULL AND agent_runs.profile_json IS NOT excluded.profile_json) OR agent_runs.main IS NOT excluded.main OR (agent_runs.starter_session IS NULL AND excluded.starter_session IS NOT NULL)').run(org,id,session,machine,String(run),profile?JSON.stringify(profile):null,main?1:0,starterSession);
|
|
412
|
+
db.prepare('UPDATE agent_runs SET conversation=?,started_at=CASE WHEN started_at=0 THEN ? ELSE started_at END WHERE org=? AND session_ref=?').run(conversation?1:0,Date.now(),org,session);
|
|
396
413
|
db.exec('COMMIT');
|
|
397
414
|
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
|
|
398
415
|
}
|
|
@@ -470,3 +487,8 @@ export function agentEnvironment(agent,{root,context,session=null,main=true}={})
|
|
|
470
487
|
SUPERCODE_AGENT_MAIN:main?'1':'0',SUPERCODE_AGENT_SYNCED:a.synced===false?'0':'1',SUPERCODE_AGENT_ASSIGNMENTS:JSON.stringify(agent.assignments??[]),
|
|
471
488
|
...(session?{SUPERCODE_AGENT_SESSION:session}:{})};
|
|
472
489
|
}
|
|
490
|
+
|
|
491
|
+
/** A standing profile's conversation runtime; scheduled/card runs never select its conversation. */
|
|
492
|
+
export function localAgentConversation(root,org,id){
|
|
493
|
+
const db=store(root);try{return db.prepare('SELECT session_ref session,started_at,ended_at FROM agent_runs WHERE org=? AND agent_id=? AND conversation=1 ORDER BY (ended_at IS NULL) DESC,started_at DESC,rowid DESC LIMIT 1').get(org,id)??null;}finally{db.close();}
|
|
494
|
+
}
|
package/apply/doors.mjs
CHANGED
|
@@ -18,11 +18,14 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
18
18
|
import { createConnection } from 'node:net';
|
|
19
19
|
import { dirname, join, resolve } from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { redactText } from '@volter/teams/redact';
|
|
21
22
|
|
|
22
23
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
const MARK = '@@supercode-apply@@';
|
|
24
25
|
/** A door call's ceiling: Hermes's own functions answer in well under this. */
|
|
25
26
|
const DOOR_TIMEOUT_MS = 60_000;
|
|
27
|
+
// Redact the complete text before truncation: a length bound must not split a token before its detector sees it.
|
|
28
|
+
const diagnostic = text => redactText(String(text ?? '')).slice(0, 2000);
|
|
26
29
|
|
|
27
30
|
/** Hermes's schedule string for a declared schedule. */
|
|
28
31
|
export function hermesSchedule(schedule) {
|
|
@@ -223,8 +226,28 @@ export async function orchestratorDoor({ root, profile = 'default', load, socket
|
|
|
223
226
|
const op = async (name, args) => {
|
|
224
227
|
const live = await viaSocket(socket ?? join(root, 'orchestrator.sock'), { op: name, args, profile });
|
|
225
228
|
const answer = live ?? (() => {
|
|
226
|
-
|
|
227
|
-
|
|
229
|
+
// The applier may be imported by Bun (the install's keeper). The operator CLI needs Node's runtime,
|
|
230
|
+
// including node:sqlite; a caller's executable is not necessarily the CLI's interpreter.
|
|
231
|
+
const node = process.env.SUPERCODE_NODE_BIN?.trim()
|
|
232
|
+
|| (process.versions.bun || process.versions.deno ? 'node' : process.execPath);
|
|
233
|
+
const run = spawnSync(node, [bin, name, '--root', root, '--profile', profile, '--json', JSON.stringify(args)], { encoding: 'utf8', timeout: DOOR_TIMEOUT_MS });
|
|
234
|
+
let parsed;
|
|
235
|
+
try { parsed = JSON.parse((run.stdout ?? '').trim().split('\n').pop()); } catch { /* retained below */ }
|
|
236
|
+
if (run.error || run.signal || run.status !== 0 || typeof parsed?.ok !== 'boolean' || !parsed.ok) {
|
|
237
|
+
const details = [
|
|
238
|
+
run.error?.message && `spawn: ${diagnostic(run.error.message)}`,
|
|
239
|
+
run.signal && `signal ${run.signal}`,
|
|
240
|
+
parsed?.error && `refusal: ${diagnostic(typeof parsed.error === 'string' ? parsed.error : JSON.stringify(parsed.error))}`,
|
|
241
|
+
typeof parsed?.ok !== 'boolean' && 'no readable operator response',
|
|
242
|
+
typeof parsed?.ok !== 'boolean' && run.stdout?.trim() && `stdout: ${diagnostic(run.stdout)}`,
|
|
243
|
+
run.stderr?.trim() && `stderr: ${diagnostic(run.stderr)}`,
|
|
244
|
+
].filter(Boolean).join('; ');
|
|
245
|
+
throw Object.assign(new Error(`${name} via cold Node ${diagnostic(node)} failed (exit ${run.status ?? 'unknown'}): ${details}`), {
|
|
246
|
+
code: run.error?.code ?? (run.status || (parsed?.ok === false ? 'OPERATOR_REFUSED' : 'BAD_OPERATOR_RESPONSE')),
|
|
247
|
+
exitCode: run.status, signal: run.signal,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return parsed;
|
|
228
251
|
})();
|
|
229
252
|
if (!answer.ok) throw new Error(answer.error);
|
|
230
253
|
return answer.result;
|
package/bin/agent-channel.mjs
CHANGED
|
@@ -17,17 +17,20 @@
|
|
|
17
17
|
// instead and opens nothing.
|
|
18
18
|
//
|
|
19
19
|
// RH2 is reached as the agent's own principal where the organization declares the agent (docs/adr/0017: `supercode
|
|
20
|
-
// orchestrator apps install rh2-agent … --
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// the Room
|
|
20
|
+
// orchestrator apps install rh2-agent … --agent <its Teams id>` in the home, `--root` or HERMES_HOME), the record bound to
|
|
21
|
+
// the agent this channel names with `--agent-id`, else to the id the home holds for that agent (homeAgentId: its declared
|
|
22
|
+
// identity, else its profile's by the home's agent layer `agents.json`), so its threads and its call are one participant
|
|
23
|
+
// and no other agent's record is taken; otherwise through the Room-chat app's installation for this agent (`apps install
|
|
24
|
+
// room-chat … --profile <agent>`). The record's origin, organization, principal and token are the agent's identity in the
|
|
25
|
+
// Room. The bridge's thread anchors are kept in `--state`.
|
|
24
26
|
//
|
|
25
|
-
// The agent's
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
27
|
+
// The agent's seat in its Room carries its main session (RFC 0020 decision 26): where this machine is in a Teams team,
|
|
28
|
+
// the seat is bound to the main session (`supercode sessions bind`, the session owner's view binding for the seat), and
|
|
29
|
+
// rebound to each new main session, as the Agents → Room agents app binds an arc's agent. Until the main session is in
|
|
30
|
+
// the team's catalog the binding is retried.
|
|
29
31
|
import { parseArgs } from 'node:util';
|
|
30
32
|
import { execFileSync } from 'node:child_process';
|
|
33
|
+
import { readFileSync } from 'node:fs';
|
|
31
34
|
import { homedir } from 'node:os';
|
|
32
35
|
import { join } from 'node:path';
|
|
33
36
|
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
@@ -35,14 +38,15 @@ import { Rh2Adapter } from '../adapters/rh2.mjs';
|
|
|
35
38
|
import { AgentMailboxBridge } from '../adapters/agent-mailbox.mjs';
|
|
36
39
|
import { JiraCommentsAdapter } from '../adapters/jira-comments.mjs';
|
|
37
40
|
import { ChatSdkAdapter } from '../adapters/chat-sdk.mjs';
|
|
38
|
-
import { installedApps } from '../installed-apps.mjs';
|
|
41
|
+
import { installedAgentRecord, installedApps } from '../installed-apps.mjs';
|
|
42
|
+
import { agentScope, homeAgentId } from '../agents.mjs';
|
|
39
43
|
|
|
40
44
|
const { values } = parseArgs({
|
|
41
45
|
args: process.argv.slice(3),
|
|
42
|
-
options: { agent: { type: 'string' }, root: { type: 'string' }, room: { type: 'string' }, 'room-key': { type: 'string' }, 'room-name': { type: 'string' }, principal: { type: 'string' }, preset: { type: 'string' }, role: { type: 'string' }, state: { type: 'string' }, platform: { type: 'string' }, site: { type: 'string' }, jql: { type: 'string' }, 'slack-channel': { type: 'string' }, help: { type: 'boolean' } },
|
|
46
|
+
options: { agent: { type: 'string' }, 'agent-id': { type: 'string' }, root: { type: 'string' }, room: { type: 'string' }, 'room-key': { type: 'string' }, 'room-name': { type: 'string' }, principal: { type: 'string' }, preset: { type: 'string' }, role: { type: 'string' }, state: { type: 'string' }, platform: { type: 'string' }, site: { type: 'string' }, jql: { type: 'string' }, 'slack-channel': { type: 'string' }, help: { type: 'boolean' } },
|
|
43
47
|
});
|
|
44
48
|
if (values.help || !values.agent) {
|
|
45
|
-
console.log('supercode-orchestrator agent-channel --agent <name> [--root <home>] [--room <id> | --room-key <key> [--principal <rh2 principal id> --preset <preset id> --role <role key> [--room-name <name>]]] [--state <file>]\n supercode-orchestrator agent-channel --agent <name> --platform slack --slack-channel <id>[,<id>] [--state <file>]\n supercode-orchestrator agent-channel --agent <name> --platform jira --site <url> --jql <jql> [--state <file>]');
|
|
49
|
+
console.log('supercode-orchestrator agent-channel --agent <name> [--agent-id <its Teams id>] [--root <home>] [--room <id> | --room-key <key> [--principal <rh2 principal id> --preset <preset id> --role <role key> [--room-name <name>]]] [--state <file>]\n supercode-orchestrator agent-channel --agent <name> --platform slack --slack-channel <id>[,<id>] [--state <file>]\n supercode-orchestrator agent-channel --agent <name> --platform jira --site <url> --jql <jql> [--state <file>]');
|
|
46
50
|
process.exit(values.help ? 0 : 2);
|
|
47
51
|
}
|
|
48
52
|
const log = (level, line) => (level === 'debug' ? undefined : console.error(`${new Date().toISOString()} ${level} ${line}`));
|
|
@@ -78,10 +82,21 @@ if (values.platform && values.platform !== 'rh2') {
|
|
|
78
82
|
process.on('SIGINT', stop);
|
|
79
83
|
} else {
|
|
80
84
|
const root = values.root ?? process.env.HERMES_HOME;
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
// The agent's own record only when it is this agent's (bound to its Teams id, as an agent's sessions are mounted with it):
|
|
86
|
+
// the id named, else the one this home holds for the agent, read and never minted.
|
|
87
|
+
const agentId = values['agent-id'] ?? (root ? (() => {
|
|
88
|
+
try {
|
|
89
|
+
// a named profile implies its own agent: a home with no agent layer runs the agent on the profile of its name
|
|
90
|
+
let layer = {};
|
|
91
|
+
try { layer = JSON.parse(readFileSync(join(root, 'agents.json'), 'utf8')); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
92
|
+
const { org, server } = agentScope(process.env.SUPERCODE_CONTEXT);
|
|
93
|
+
return homeAgentId(root, { org, server, name: values.agent, profile: layer.agents?.[values.agent]?.profile ?? values.agent });
|
|
94
|
+
} catch (error) { log('warn', `${values.agent}: its id in ${root} was not read (${error.message})`); return null; }
|
|
95
|
+
})() : null);
|
|
96
|
+
const own = root && agentId ? installedAgentRecord(root, agentId) : null;
|
|
97
|
+
const installation = own ?? (root ? installedApps(root).find((app) => app.app === 'room-chat' && app.profile === values.agent) : undefined);
|
|
83
98
|
if (!installation) {
|
|
84
|
-
console.error(`agent-channel: ${values.agent} has no RH2 identity in ${root ?? '(no home: --root or HERMES_HOME)'}; install its agent (supercode orchestrator apps install rh2-agent … --
|
|
99
|
+
console.error(`agent-channel: ${values.agent} has no RH2 identity in ${root ?? '(no home: --root or HERMES_HOME)'}; install its agent (supercode orchestrator apps install rh2-agent … --agent ${agentId ?? '<its Teams id>, and run this with --agent-id <its Teams id>'}) or Room chat for it (apps install room-chat … --profile ${values.agent})`);
|
|
85
100
|
process.exit(1);
|
|
86
101
|
}
|
|
87
102
|
const identity = { api_url: installation.origin, organization: installation.organizationId, principal_id: installation.principalId, token: installation.token };
|
|
@@ -157,23 +172,27 @@ async function seatSession(session) {
|
|
|
157
172
|
* is read again on every tick, so a re-declared main is bound at the next one. A bind that fails is said once per main
|
|
158
173
|
* and reason (a main that is not in the team's catalog, or one no session ever ran as), never only at debug level,
|
|
159
174
|
* which this process does not print. */
|
|
160
|
-
async function
|
|
175
|
+
async function bindMainSession() {
|
|
161
176
|
let main;
|
|
162
177
|
try { main = [sc(['agent', 'show', values.agent, '--json'])].flat().find((a) => a.name === values.agent)?.main_session; }
|
|
163
|
-
catch (error) { warnOnce(`agent show: ${lastLine(error)}`, `${values.agent}: its agent record was not read (${lastLine(error)}); its
|
|
178
|
+
catch (error) { warnOnce(`agent show: ${lastLine(error)}`, `${values.agent}: its agent record was not read (${lastLine(error)}); its seat stays bound to ${boundTo ?? 'nothing'}`); return; }
|
|
164
179
|
const session = typeof main === 'string' ? main.split(':').pop() : null;
|
|
165
|
-
if (!session) { warnOnce('no main', `${values.agent}: its agent record names no main session; its
|
|
180
|
+
if (!session) { warnOnce('no main', `${values.agent}: its agent record names no main session; its seat stays bound to ${boundTo ?? 'nothing'}`); return; }
|
|
166
181
|
if (session === boundTo) return;
|
|
167
182
|
let bound;
|
|
168
183
|
try { bound = sc(['sessions', 'bind', session, '--room', roomId, '--agent', adapter.principalId(), '--rh2', adapter.apiUrl(), '--json']).data; }
|
|
169
|
-
catch (error) { warnOnce(`${session}: ${lastLine(error)}`, `${values.agent}: its
|
|
184
|
+
catch (error) { warnOnce(`${session}: ${lastLine(error)}`, `${values.agent}: its seat is not bound to its main session ${main} (exit ${error.status ?? '?'}: ${lastLine(error)}); retried every 15 s with the record read again`); return; }
|
|
185
|
+
// The session by its reference (RH2 ADR 0050 decision 4); RH2 takes the registration that provides this agent's sessions.
|
|
186
|
+
await seatSession({ harness: bound.session.harness, reference: bound.session.reference });
|
|
170
187
|
failedAs = null;
|
|
171
|
-
await seatSession({ harness: bound.harness, sessionId: bound.sessionId, team: bound.team, teamsUrl: bound.teamsUrl });
|
|
172
188
|
boundTo = session;
|
|
173
|
-
log('info', `${values.agent}: its
|
|
189
|
+
log('info', `${values.agent}: its seat in the account Room carries its main session ${session}`);
|
|
174
190
|
}
|
|
175
|
-
|
|
176
|
-
|
|
191
|
+
// A refused seat is said once per reason and retried on the next tick, the first one too: it never ends the channel (a
|
|
192
|
+
// refusal at start would otherwise restart the service in a loop, with the mailbox bridge down each time).
|
|
193
|
+
const rebind = () => bindMainSession().catch((error) => warnOnce(`seat: ${error.message}`, `${values.agent}: its seat was refused its main session (${error.message}); retried every 15 s`));
|
|
194
|
+
await rebind();
|
|
195
|
+
const rebinding = setInterval(() => { void rebind(); }, 15_000);
|
|
177
196
|
const stop = async () => { clearInterval(rebinding); await bridge.stop(); await client.close?.(); process.exit(0); };
|
|
178
197
|
process.on('SIGTERM', stop);
|
|
179
198
|
process.on('SIGINT', stop);
|