@twinsearth/w2m-dsh-plugin 0.9.5 → 0.9.7

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.
@@ -35,7 +35,7 @@
35
35
  * Everything in this file is callable by a model, against a fleet whose machines run commands under
36
36
  * their own allow-lists, and it holds an operator token that authorises dispatch fleet-wide. That
37
37
  * makes the *tool surface* a boundary, and this section is the boundary written down as it actually
38
- * is — not as it would sound best. `test/tool-cards.test.mjs` asserts the parts of it that can be
38
+ * is - not as it would sound best. `test/tool-cards.test.mjs` asserts the parts of it that can be
39
39
  * asserted mechanically, and a change that crosses it is meant to turn that suite red.
40
40
  *
41
41
  * **No tool parameter names a credential, an allow-list, a key path, or a state directory.** There is
@@ -49,7 +49,7 @@
49
49
  * and is applied in one direction only: {@link checkAllowedCommand} returns a refusal or `null`, and
50
50
  * there is no path in this file that adds an entry to the list. Each target machine's own agent
51
51
  * re-decides the same gate against its own list, so a dispatcher that lost this check would still not
52
- * widen anything — the direction the shared matcher exists to keep honest (`src/agent/allowed-commands.mjs`).
52
+ * widen anything - the direction the shared matcher exists to keep honest (`src/agent/allowed-commands.mjs`).
53
53
  *
54
54
  * **No tool can change a machine's `p2pMode`.** The mode is resolved once, in `readConfig` at
55
55
  * `apply()` time, from the config object or `W2M_P2P_MODE`, and every dispatch publishes it as a fact
@@ -58,19 +58,19 @@
58
58
  * weaker path, which is the one place a dispatcher could otherwise have quietly downgraded a peer.
59
59
  *
60
60
  * **No tool writes another machine's identity or keys.** `device.json` is read (`readDevice`) and
61
- * `device.key` is loaded (`DeviceKey.load`) — neither is ever written here. What a dispatch publishes
61
+ * `device.key` is loaded (`DeviceKey.load`) - neither is ever written here. What a dispatch publishes
62
62
  * is this machine's own **public** key inside `p2p.static_keys`, so the executor can authenticate a
63
63
  * dial; the private half never leaves `src/agent/crypto.mjs`.
64
64
  *
65
65
  * **No tool result carries a token or a key.** `w2m_status` reports `paired`,
66
66
  * `operatorToken_configured` and `pairingCode_configured` as booleans and never the values. The
67
- * operator token travels only in the `Authorization` header of `POST /v1/task` — never in the request
68
- * body and never in an answer — and `direct_pushes[].static_key` is the literal marker `'published'`,
67
+ * operator token travels only in the `Authorization` header of `POST /v1/task` - never in the request
68
+ * body and never in an answer - and `direct_pushes[].static_key` is the literal marker `'published'`,
69
69
  * not the key it describes.
70
70
  *
71
71
  * What an agent-facing tool *may* do across machines is bounded and stated: ask the machines the relay
72
72
  * leased a task to run one argv inside their own allow-lists, in their own copy of the project,
73
- * read-only unless `write: true` names a `write_scope`, and report what they said — refusals
73
+ * read-only unless `write: true` names a `write_scope`, and report what they said - refusals
74
74
  * included, as values.
75
75
  *
76
76
  * # `@deepseek-ai/dsh-tools` and the offline fallback
@@ -93,6 +93,11 @@ import { createHash } from 'node:crypto';
93
93
  import { promises as fs } from 'node:fs';
94
94
  import os from 'node:os';
95
95
  import path from 'node:path';
96
+ // The individual memory layer (v0.9.6). Read through the agent's own module rather than re-parsing the
97
+ // file here: the reader carries the rules that matter -- newest-first by file position, a malformed line
98
+ // skipped rather than fatal, and the rotated generation reachable -- and a second implementation would
99
+ // drift from the writer's shape without anything failing.
100
+ import { appendMemory, readMemorySync } from '../agent/memory.mjs';
96
101
 
97
102
  // The base-address rules live in one place (§2), shared with the agent side. Importing the - rather
98
103
  // than re-deriving them her - is what stops the plugin and the agent from disagreeing about what
@@ -180,7 +185,7 @@ const MAX_STAGES = 16;
180
185
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
181
186
  * overrides it at runtime for tests and packagers.
182
187
  */
183
- const PLUGIN_VERSION = '0.9.5';
188
+ const PLUGIN_VERSION = '0.9.7';
184
189
 
185
190
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
186
191
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -227,8 +232,8 @@ const RETRYABLE_STATUS = new Set([429, 502, 503, 504]);
227
232
  * `w2m_status` as message prefixes.
228
233
  *
229
234
  * A code is added only when it names a *distinct remedy*. `W2M_CONFIG` still covers a malformed
230
- * setting at large; the credential gate below has its own code because its remedy — set
231
- * `operatorToken` — is unlike any other configuration fault's.
235
+ * setting at large; the credential gate below has its own code because its remedy - set
236
+ * `operatorToken` - is unlike any other configuration fault's.
232
237
  */
233
238
  export const REFUSAL_CODE = Object.freeze({
234
239
  /** The dispatch pre-flight found the command outside this plugin's `allowedCommands`. */
@@ -267,7 +272,7 @@ export const REFUSAL_CODE = Object.freeze({
267
272
  /** Storing a result frame failed, so the result exists only on the sender. */
268
273
  P2P_INBOX_WRITE_FAILED: 'P2P_INBOX_WRITE_FAILED',
269
274
  /**
270
- * The composer's selected target routed this dispatch. Not a refusal — a named *reason*, so the
275
+ * The composer's selected target routed this dispatch. Not a refusal - a named *reason*, so the
271
276
  * routing decision is greppable in the answer instead of being a bare `applied: true`.
272
277
  */
273
278
  SELECTION_APPLIED: 'SELECTION_APPLIED',
@@ -577,7 +582,7 @@ function readConfig(config = {}) {
577
582
  /**
578
583
  * §7: where the relay address came from.
579
584
  *
580
- * The order is `rabbitUrl` → `W2M_RABBIT_URL` → the shared server, and the *source* is carried out
585
+ * The order is `rabbitUrl` 鈫?`W2M_RABBIT_URL` 鈫?the shared server, and the *source* is carried out
581
586
  * of here rather than re-derived at report time, because "which of the three won" is exactly the
582
587
  * question a cross-network deployment gets wrong: a machine that silently talked to the shared
583
588
  * server instead of the operator's own relay looks identical to a working one until it dispatches
@@ -709,7 +714,7 @@ function readConfig(config = {}) {
709
714
  }
710
715
 
711
716
  /**
712
- * Resolve `p2pMode` from the config object, then `W2M_P2P_MODE`, then the default — loudly.
717
+ * Resolve `p2pMode` from the config object, then `W2M_P2P_MODE`, then the default - loudly.
713
718
  *
714
719
  * A value that is present and wrong is a `W2M_CONFIG` error naming the setting: falling back to
715
720
  * `auto` would dispatch work over a path the operator explicitly asked not to use, and falling back
@@ -717,7 +722,7 @@ function readConfig(config = {}) {
717
722
  * decisions the operator has to make.
718
723
  *
719
724
  * "Present" is decided by the *key*, not by truthiness. A profile patch that writes `p2pMode:` with
720
- * nothing after it hands this function `null`, and that is a value somebody typed — collapsing it
725
+ * nothing after it hands this function `null`, and that is a value somebody typed - collapsing it
721
726
  * into "the key is absent" is exactly the silent default this function exists to refuse. An unset key
722
727
  * and a blank `W2M_P2P_MODE` are the only two shapes that mean "nobody chose", and only the second is
723
728
  * treated as absent because an unset environment variable is indistinguishable from an empty one.
@@ -753,12 +758,12 @@ function resolveP2PMode(config) {
753
758
  * Every entry is validated here, before the node is built, because the alternative is a tick several
754
759
  * seconds later inside the node's announce loop: an unusable STUN entry is a configuration mistake,
755
760
  * and this project reports those at the call site rather than as a degraded status field. The rule
756
- * itself is `parseServer` from `src/agent/stun.mjs` — the same function the query path will use, so
761
+ * itself is `parseServer` from `src/agent/stun.mjs` - the same function the query path will use, so
757
762
  * "valid here" and "usable there" cannot drift.
758
763
  *
759
764
  * An **empty list is the default**, not an error. `stunServers: []` is what the bundled
760
765
  * `cordis.patch.yml` carries (a key that documents itself and names no server), and a profile that
761
- * starts from that file must not fail to load. `null` entries are dropped for the same reason — a
766
+ * starts from that file must not fail to load. `null` entries are dropped for the same reason - a
762
767
  * YAML list with a blank item is the same statement as a blank item in a profile patch. A list whose
763
768
  * entries are *wrong* is still refused: there is a difference between "nothing named" and "the wrong
764
769
  * thing named", and only the second is a mistake.
@@ -985,7 +990,7 @@ function resolveProfileDir(config) {
985
990
  /**
986
991
  * Require the Rabbit URL, naming the setting when it is absent.
987
992
  *
988
- * With §7's shared-server default this can no longer be reached by "the operator set nothing" — an
993
+ * With §7's shared-server default this can no longer be reached by "the operator set nothing" - an
989
994
  * unset `rabbitUrl` resolves to {@link SHARED_SERVER}. It stays as the last-resort guard so that a
990
995
  * future edit which clears the field fails with a sentence naming it instead of an `Invalid URL`
991
996
  * from somewhere deep in `fetch`.
@@ -1042,7 +1047,7 @@ function devicePathFor(cfg) {
1042
1047
  *
1043
1048
  * {@link readDevice} answers from a `stateDir` and is the tool-facing reader; this one adds the
1044
1049
  * `DSH_HOME` fallback (an unset `stateDir` is the documented default layout, not an error) and turns
1045
- * every failure into a `machine_id` of null plus an `error` — a machine whose identity cannot be read
1050
+ * every failure into a `machine_id` of null plus an `error` - a machine whose identity cannot be read
1046
1051
  * must still be able to dispatch, because the relay path does not need an identity at all.
1047
1052
  *
1048
1053
  * @param {ReturnType<typeof readConfig>} cfg
@@ -1082,8 +1087,8 @@ function safeFilePart(value) {
1082
1087
  *
1083
1088
  * Deliberately *not* the relay's own `jcs` (that would make this file depend on relay internals for
1084
1089
  * a two-line need). The digest here is only an integrity check of what this process stored, so the
1085
- * rule is local by definition; the value that has to match another process byte for byte — the
1086
- * dedupe key — is the one imported from the relay instead.
1090
+ * rule is local by definition; the value that has to match another process byte for byte - the
1091
+ * dedupe key - is the one imported from the relay instead.
1087
1092
  *
1088
1093
  * @param {unknown} value
1089
1094
  * @returns {string}
@@ -1099,9 +1104,9 @@ function canonicalJson(value) {
1099
1104
  /**
1100
1105
  * Does `frame` name both halves of the inbox key?
1101
1106
  *
1102
- * A frame without a `task_id` or without a `machine_id` cannot be stored or acknowledged — the
1107
+ * A frame without a `task_id` or without a `machine_id` cannot be stored or acknowledged - the
1103
1108
  * acknowledgement is keyed by `task_id`, and two machines answering the same task must not overwrite
1104
- * each other — so it is refused by name instead of being written somewhere surprising.
1109
+ * each other - so it is refused by name instead of being written somewhere surprising.
1105
1110
  *
1106
1111
  * Both halves of the refusal are returned: `reason` is the `CODE: sentence` string this has always
1107
1112
  * produced (it is what `w2m_status` shows), `reason_code` is the same code as a field, so a reader
@@ -1159,7 +1164,7 @@ async function writeFileAtomic(dir, file, text) {
1159
1164
  * Count the stored result frames.
1160
1165
  *
1161
1166
  * Depth is read from the directory rather than tracked in memory, so it answers the question that
1162
- * actually matters — "is there anything here I have not consumed" — across a plugin reload. A
1167
+ * actually matters - "is there anything here I have not consumed" - across a plugin reload. A
1163
1168
  * missing directory is depth 0, not an error: the inbox exists only once something has arrived.
1164
1169
  *
1165
1170
  * @param {string|null} dir
@@ -1233,7 +1238,7 @@ function createP2PTransport(cfg) {
1233
1238
  *
1234
1239
  * A dispatcher behind a symmetric NAT (CGNAT, in practice) can send all it likes and still be heard
1235
1240
  * by nobody: its outbound HELLO opens its own mapping, but the peer's filter stays shut until
1236
- * *something of the peer's* has gone the other way. So the executor has to dial first — and to dial
1241
+ * *something of the peer's* has gone the other way. So the executor has to dial first - and to dial
1237
1242
  * it needs two things the offer itself can carry: where the dispatcher says it is, and which punch
1238
1243
  * to join. Without them the executor has only the relay lookup, which is a second opinion about the
1239
1244
  * same machine that answers 404 for a dispatcher whose announcement has just expired.
@@ -1241,14 +1246,14 @@ function createP2PTransport(cfg) {
1241
1246
  * WHY A PUNCH ID RATHER THAN A SESSION ID
1242
1247
  *
1243
1248
  * The session id cannot be known when this request is written: one dispatch leases one task to
1244
- * several machines, and the relay decides which — in the same request that mints the task id. A
1249
+ * several machines, and the relay decides which - in the same request that mints the task id. A
1245
1250
  * single session shared by those machines would be a defect, not a simplification: on the
1246
1251
  * dispatcher's own node the second machine's HELLO would look like a re-acknowledgement of the
1247
1252
  * first machine's live channel, so one of them would have its result delivered to the other's
1248
1253
  * address. The block therefore publishes a per-dispatch *punch id*, and both ends derive the same
1249
1254
  * per-machine session from it (`punchSession` in `src/agent/p2p.mjs`): the dispatcher from the
1250
- * lease, the executor from the offer plus its own machine id. The pushed frame — which is addressed
1251
- * to one machine, so the value *is* known — carries the derived `session` as well.
1255
+ * lease, the executor from the offer plus its own machine id. The pushed frame - which is addressed
1256
+ * to one machine, so the value *is* known - carries the derived `session` as well.
1252
1257
  *
1253
1258
  * COMPATIBILITY
1254
1259
  *
@@ -1264,11 +1269,11 @@ function createP2PTransport(cfg) {
1264
1269
  * WHY A MAP RATHER THAN ONE KEY
1265
1270
  *
1266
1271
  * The executor needs the *dispatcher's* key before it can dial back, and the dispatcher needs the
1267
- * *executor's* key before it can push — and at dispatch time the relay has not yet said which
1272
+ * *executor's* key before it can push - and at dispatch time the relay has not yet said which
1268
1273
  * machines the task went to (`leases` come back in the same response that creates it). So the block
1269
1274
  * carries this machine's own key, which is known before the request is written, and
1270
1275
  * {@link offerP2PFor} adds the target's once the lease names it. Reading the field is therefore
1271
- * "look up the machine you are about to talk to", never "assume the only entry is the peer" — a
1276
+ * "look up the machine you are about to talk to", never "assume the only entry is the peer" - a
1272
1277
  * machine that receives an offer for a fleet of three has three entries and uses one.
1273
1278
  *
1274
1279
  * What that buys, in the document's own terms: no `[Peer]` block for anyone to fill in. A machine
@@ -1295,7 +1300,7 @@ function buildP2POfferBlock(cfg, runtime) {
1295
1300
  block.candidates = candidates.map((candidate) => ({ address: candidate.address, port: candidate.port }));
1296
1301
  }
1297
1302
  // This machine's own static public key. Absent (not null) when there is none, so the wire stays
1298
- // byte-identical to v0.5.x for a machine that has no key — an additive change has to be absent to
1303
+ // byte-identical to v0.5.x for a machine that has no key - an additive change has to be absent to
1299
1304
  // be additive.
1300
1305
  const own = runtime?.encryption?.public_key;
1301
1306
  const machineId = runtime?.machineId;
@@ -1311,7 +1316,7 @@ function buildP2POfferBlock(cfg, runtime) {
1311
1316
  * The added `session` is what makes the push and a reverse dial one punch rather than two: an
1312
1317
  * executor that received the offer over a channel may still have to dial back (the dispatcher closes
1313
1318
  * the channel it pushed over, immediately after the push), and a dial carrying a session the
1314
- * dispatcher is not filtering for would be answered by its `accept()` at best — a second session,
1319
+ * dispatcher is not filtering for would be answered by its `accept()` at best - a second session,
1315
1320
  * a second channel, and two live sessions for one task where one would do.
1316
1321
  *
1317
1322
  * v0.6.0 adds the *target's* key to `static_keys` when the caller supplies one, which is the half of
@@ -1343,8 +1348,7 @@ function offerP2PFor(published, { taskId, machineId, peerKey = null }) {
1343
1348
  *
1344
1349
  * The executor decides whether an offer is a first delivery or a duplicate on `dedupe_key` +
1345
1350
  * `attempt` (§6), and the relay emits its own SSE offer with those exact values. A push that
1346
- * disagreed by one character would make the machine execute the task twice — once from each path —
1347
- * so the dedupe key is computed with the relay's own {@link computeDedupeKey}/{@link computeCommandHash},
1351
+ * disagreed by one character would make the machine execute the task twice - once from each path - * so the dedupe key is computed with the relay's own {@link computeDedupeKey}/{@link computeCommandHash},
1348
1352
  * from the same `task_id`, `index`, `command_hash` inputs and `base_tree` the task was created with,
1349
1353
  * and `machine_id`/`index`/`attempt` come from the lease rather than from the caller's arguments.
1350
1354
  *
@@ -1419,7 +1423,7 @@ function buildOfferFrame({ taskId, lease, payload, originMachineId, peerKey = nu
1419
1423
  /**
1420
1424
  * Push one offer over the direct path. Best effort by construction: nothing here throws.
1421
1425
  *
1422
- * The session comes from the frame's own `p2p.session` — minted per machine by
1426
+ * The session comes from the frame's own `p2p.session` - minted per machine by
1423
1427
  * {@link offerP2PFor} from the punch id the relay is publishing for this same task. Dialling with a
1424
1428
  * session of the node's own invention would leave the two ends on two sessions, and an executor
1425
1429
  * whose reverse dial happened to arrive first would open a *second* channel beside this one instead of
@@ -1449,7 +1453,7 @@ async function pushOffer(node, frame, crypto = {}) {
1449
1453
  }
1450
1454
  // The dial's own options, built here rather than inline because the *presence* of `crypto` is the
1451
1455
  // decision: `P2PNode.dial` arms a handshake only when this dispatcher holds a key for that peer, and
1452
- // a peer with no published key is dialled exactly as v0.5.x dialled it — plaintext, recorded, and
1456
+ // a peer with no published key is dialled exactly as v0.5.x dialled it - plaintext, recorded, and
1453
1457
  // never a failure. That is §4's fallback clause applied to the push: a fleet must never lose a
1454
1458
  // machine to a security upgrade. It is not a silent downgrade, because the peer never offered a key
1455
1459
  // to downgrade *from*; a peer whose key we hold and whose handshake fails produces `ok: false` and
@@ -1505,14 +1509,14 @@ async function pushOffer(node, frame, crypto = {}) {
1505
1509
  *
1506
1510
  * Called after the relay has created the task, so the relay's own SSE offer has already gone out:
1507
1511
  * this is an *additional* delivery over the faster path, and the relay's copy is what makes a failed
1508
- * push harmless. Only leases in a runnable state are pushed — `refused` and `observing` machines
1512
+ * push harmless. Only leases in a runnable state are pushed - `refused` and `observing` machines
1509
1513
  * were never handed the command, and sending them an offer would be asking them to run work the
1510
1514
  * relay decided they must not run.
1511
1515
  *
1512
1516
  * v0.6.0: each target's static key is fetched from the relay's peer record before the push, which is
1513
1517
  * the one moment the dispatcher learns the key of a machine it did not already know. It is fetched
1514
1518
  * **here** rather than sourced from the stored `p2p` block because the block is written *before* the
1515
- * relay decides which machines the task goes to — the offer that carries the key is the one addressed
1519
+ * relay decides which machines the task goes to - the offer that carries the key is the one addressed
1516
1520
  * to the machine that key belongs to. A missing key is not an error: the push proceeds and the
1517
1521
  * executor records `P2P_CRYPTO_NO_KEY` for its plaintext channel, which is the documented outcome for
1518
1522
  * a peer that never announced.
@@ -1520,7 +1524,7 @@ async function pushOffer(node, frame, crypto = {}) {
1520
1524
  * @param {P2PNode} node
1521
1525
  * @param {object} options
1522
1526
  * @returns {Promise<Array<object>>} One entry per lease considered, in lease order. A machine that was
1523
- * not pushed to carries `ok: false` **and** a `reason_code` naming which gate refused it — this
1527
+ * not pushed to carries `ok: false` **and** a `reason_code` naming which gate refused it - this
1524
1528
  * block is a tool result, so a bare boolean here would be exactly the reduction the verdicts refuse.
1525
1529
  */
1526
1530
  async function pushOffers(node, { taskId, leases, payload, originMachineId, getJson = null }) {
@@ -1574,7 +1578,7 @@ async function pushOffers(node, { taskId, leases, payload, originMachineId, getJ
1574
1578
  // logs side by side. `null` when there was nothing to punch over (a v0.4.x-shaped frame).
1575
1579
  session: result.session,
1576
1580
  // True when the executor's dial got there first and this push travelled over the channel that
1577
- // dial opened — a two-way punch that worked, reported as such rather than as a plain push.
1581
+ // dial opened - a two-way punch that worked, reported as such rather than as a plain push.
1578
1582
  reused: result.reused === true,
1579
1583
  // v0.6.0: whether the offer actually travelled sealed, and why not when it did not. Reported
1580
1584
  // per machine because it is a per-machine fact: one peer can be encrypted and the next one a
@@ -1618,19 +1622,19 @@ async function readPeerStaticKey(getJson, machineId) {
1618
1622
  * WHEN THE NODE STARTS, AND WHY IT IS NOT `apply()`
1619
1623
  *
1620
1624
  * The v0.4.0 contract says the node is "started in `apply()`". It is not, and this is the one place
1621
- * this file departs from the contract's letter rather than its intent — reported as a deviation, not
1625
+ * this file departs from the contract's letter rather than its intent - reported as a deviation, not
1622
1626
  * settled quietly here. What *is* kept is everything the sentence is about: the direct path is owned
1623
1627
  * for the plugin's whole lifetime, it is disposed by a `ctx.effect` disposer, and a failure to start
1624
1628
  * is a field in `w2m_status` rather than an exception at load.
1625
1629
  *
1626
1630
  * The reason for the change is what "start" costs. Starting this node binds a UDP socket and kicks
1627
- * off a STUN sweep against four servers — network traffic with a multi-second tail — for a machine
1631
+ * off a STUN sweep against four servers - network traffic with a multi-second tail - for a machine
1628
1632
  * that may never dispatch a task in that session. `apply()` runs on every session in a host that
1629
1633
  * mounts this plugin, so an unconditional start is a rendezvous per session, and it puts traffic
1630
1634
  * into every test and every host whose runtime is deterministic about what it sends. `ensureStarted()`
1631
1635
  * therefore does the work the first time a tool actually needs the direct path: a dispatch
1632
1636
  * (`w2m_run`) or a question about it (`w2m_status`). A machine that never dispatches never punches,
1633
- * which is also the more honest reading of §1 — "never dials" is the property that matters, and this
1637
+ * which is also the more honest reading of §1 - "never dials" is the property that matters, and this
1634
1638
  * is closer to it than an announce nobody asked for.
1635
1639
  *
1636
1640
  * @param {object|null} ctx Cordis context; `ctx.effect` is used when present.
@@ -1674,7 +1678,7 @@ function createP2PRuntime(ctx, cfg, block = {}) {
1674
1678
  *
1675
1679
  * Assigned before the relay-mode early return so the tools can call it unconditionally: a caller
1676
1680
  * asks "is the direct path ready" and gets an answer either way, instead of having to know which
1677
- * mode it is in — the defect this shape fixes was a `relay`-mode dispatch calling a method that did
1681
+ * mode it is in - the defect this shape fixes was a `relay`-mode dispatch calling a method that did
1678
1682
  * not exist, which is a crash in the mode that is supposed to be the safe one.
1679
1683
  *
1680
1684
  * @type {() => Promise<object>}
@@ -1722,7 +1726,7 @@ function createP2PRuntime(ctx, cfg, block = {}) {
1722
1726
  *
1723
1727
  * Never throws. A caller cannot fix an unreadable key file from a tool call, and a dispatch that
1724
1728
  * refused because a *security upgrade* could not complete would be exactly the fleet-losing failure
1725
- * §4 forbids — so the failure becomes a reason string, the node runs without a key, and every offer
1729
+ * §4 forbids - so the failure becomes a reason string, the node runs without a key, and every offer
1726
1730
  * this machine publishes is plainly labelled as having none.
1727
1731
  *
1728
1732
  * @param {object|null} injected Test seam: a `DeviceKey`, or null to use the file.
@@ -1823,7 +1827,7 @@ async function initP2P(ctx, cfg, runtime, block) {
1823
1827
  /**
1824
1828
  * Ownership: one node, one effect, one disposer.
1825
1829
  *
1826
- * Registered *here*, after the node exists, rather than in `createP2PRuntime` — an effect that owns
1830
+ * Registered *here*, after the node exists, rather than in `createP2PRuntime` - an effect that owns
1827
1831
  * nothing is a disposer that releases nothing, and `relay` mode and a machine with no identity must
1828
1832
  * leave the host's effect list exactly as they found it. A host that provides no `ctx.effect` (an
1829
1833
  * older Cordis, or a bare test context) still gets a working node: the node itself is asked not to
@@ -1832,7 +1836,7 @@ async function initP2P(ctx, cfg, runtime, block) {
1832
1836
  * default.
1833
1837
  *
1834
1838
  * v0.5.10: `node.unref()` is a real call, not a description. Until it was added this comment
1835
- * claimed an `unref()` that existed nowhere in the plugin or the node — the socket went on holding
1839
+ * claimed an `unref()` that existed nowhere in the plugin or the node - the socket went on holding
1836
1840
  * the event loop, so a host without `ctx.effect` that started a node never exited (found by an
1837
1841
  * independent verifier against the `w2m_status` test's bare context).
1838
1842
  */
@@ -1863,13 +1867,13 @@ async function initP2P(ctx, cfg, runtime, block) {
1863
1867
  * Store one JSON frame that arrived over a direct channel, and acknowledge it.
1864
1868
  *
1865
1869
  * The writer is deliberately strict. A frame that is not a JSON object, or that names no
1866
- * `task_id`/`machine_id`, is refused and **nothing** is written — an inbox file is the only durable
1870
+ * `task_id`/`machine_id`, is refused and **nothing** is written - an inbox file is the only durable
1867
1871
  * trace of a peer's result, and a file whose name does not identify its contents is worse than no
1868
1872
  * file, because the next reader cannot tell a partial result from a complete one.
1869
1873
  *
1870
1874
  * `task.result` is the frame this exists for; `result.ack` and anything else a peer might send are
1871
1875
  * counted and ignored rather than treated as faults. The acknowledgement goes back on the same
1872
- * channel the frame arrived on, and a failed acknowledgement is recorded — the sender retries (the
1876
+ * channel the frame arrived on, and a failed acknowledgement is recorded - the sender retries (the
1873
1877
  * channel is reliable) and the file is already on disk, so a lost ack must not lose the result.
1874
1878
  *
1875
1879
  * @param {object} runtime
@@ -1930,7 +1934,7 @@ async function handleP2PMessage(runtime, event) {
1930
1934
  task_id: frame.task_id,
1931
1935
  frame,
1932
1936
  // A digest of the stored copy, so a reader can tell "the file changed under me" from "the peer
1933
- // sent something else". Not a signature — the channel is not authenticated at this layer.
1937
+ // sent something else". Not a signature - the channel is not authenticated at this layer.
1934
1938
  frame_sha256: createHash('sha256').update(canonicalJson(frame)).digest('hex'),
1935
1939
  };
1936
1940
  const file = path.join(
@@ -1970,7 +1974,7 @@ async function handleP2PMessage(runtime, event) {
1970
1974
  *
1971
1975
  * Every field is present on every call, including when the direct path is off: a reader must be able
1972
1976
  * to tell "relay mode" from "p2p was requested and could not start" without comparing two calls.
1973
- * `null` is used for "never measured", never 0 — the same rule the rest of this file follows for RTT.
1977
+ * `null` is used for "never measured", never 0 - the same rule the rest of this file follows for RTT.
1974
1978
  *
1975
1979
  * @param {ReturnType<typeof readConfig>} cfg
1976
1980
  * @param {object} runtime
@@ -2012,7 +2016,7 @@ async function readP2PStatus(cfg, runtime, peersAnnounced = null) {
2012
2016
  // v0.6.0: the crypto facts, named one by one rather than mirrored wholesale. `enabled` is about
2013
2017
  // *this* machine's key; `encrypted_channels` and `plaintext_channels` are about the channels it
2014
2018
  // has actually made, and they can both be non-zero on a mixed fleet. `error` is the reason there
2015
- // is no key when there is none — a reader has to be able to tell "this build has no crypto" from
2019
+ // is no key when there is none - a reader has to be able to tell "this build has no crypto" from
2016
2020
  // "the key file is unreadable" without reading a log.
2017
2021
  encryption: {
2018
2022
  enabled: runtime.encryption?.enabled === true,
@@ -2504,7 +2508,7 @@ async function request(options) {
2504
2508
  }
2505
2509
 
2506
2510
  // §2: base + path, joined by hand. `new URL(pathname, base)` would resolve against the origin
2507
- // and drop a deployment sub-path (`https://h/w2m` + `/v1/task` 鈫?`https://h/v1/task` 鈫?404).
2511
+ // and drop a deployment sub-path (`https://h/w2m` + `/v1/task` —?`https://h/v1/task` —?404).
2508
2512
  const target = toRequestUrl(joinUrl(rabbitUrl, pathname));
2509
2513
  for (const [key, value] of Object.entries(query ?? {})) {
2510
2514
  if (value !== undefined && value !== null) target.searchParams.set(key, String(value));
@@ -2584,8 +2588,8 @@ function endpointOf(target) {
2584
2588
  * Two sources, and the distinction is the point. An explicit `stateDir` is the operator pointing at
2585
2589
  * a pairing, so a missing or unreadable `device.json` there is **named as the fault** rather than
2586
2590
  * being turned into a mystery `401`. With no `stateDir`, this machine's own identity lives where the
2587
- * agent writes it by default — `$DSH_HOME/xclient/device.json`, the same file {@link readLocalIdentity}
2588
- * reads — and the calls go out with it. v0.4.0 made that fallback load-bearing rather than optional:
2591
+ * agent writes it by default - `$DSH_HOME/xclient/device.json`, the same file {@link readLocalIdentity}
2592
+ * reads - and the calls go out with it. v0.4.0 made that fallback load-bearing rather than optional:
2589
2593
  * every P2P announcement is authenticated as the announcing device, so a machine whose identity was
2590
2594
  * only reachable through `DSH_HOME` would have had a permanently unauthenticated rendezvous and a
2591
2595
  * direct path that never worked, while the relay path kept working and hid the reason.
@@ -2655,8 +2659,7 @@ async function readDevice(cfg) {
2655
2659
  /**
2656
2660
  * Require a `device_token`, naming where it was looked for when it is absent.
2657
2661
  *
2658
- * The `code` stays `W2M_NO_TOKEN` for every case — one class, "this machine cannot authenticate" —
2659
- * while `reason_code` names which of the three it is, because the three have different remedies: pair
2662
+ * The `code` stays `W2M_NO_TOKEN` for every case - one class, "this machine cannot authenticate" - * while `reason_code` names which of the three it is, because the three have different remedies: pair
2660
2663
  * the agent, repair the file, or finish a pairing that never completed.
2661
2664
  *
2662
2665
  * @param {{machine_id: string|null, device_token: string|null, path: string|null, present: boolean, error: string|null}} device
@@ -2817,13 +2820,13 @@ async function resolveTreeFingerprint(projectDir, timeoutMs) {
2817
2820
  * resolving a path, and a pre-flight that strips the path can only ever compare names, so it would
2818
2821
  * wave `E:\Temp\evil\node.exe` through while the agent refused it. Passing the argv through
2819
2822
  * unchanged is what keeps the two ends one rule. What the shared matcher then resolves, it resolves
2820
- * against **this** process's environment — the dispatcher's PATH — which is a real limit of a
2823
+ * against **this** process's environment - the dispatcher's PATH - which is a real limit of a
2821
2824
  * pre-flight (the executing machine's PATH is the one that will decide), and the reason the refusal
2822
2825
  * that matters is the agent's own.
2823
2826
  *
2824
2827
  * A refusal names itself: `reason_code` is the matcher's own code (so the two ends cannot report one
2825
2828
  * refusal under two spellings) and `detail` says what would change it. The two ways this control
2826
- * refuses are kept apart — a command outside the list and a list that cannot be parsed have
2829
+ * refuses are kept apart - a command outside the list and a list that cannot be parsed have
2827
2830
  * different remedies, and sharing one code would send the reader to the wrong one.
2828
2831
  *
2829
2832
  * @param {string[]} allowedCommands Entries as configured, e.g. `['node --test']`.
@@ -2903,7 +2906,7 @@ function compactEnvelope(raw, comparable) {
2903
2906
  function excerpt(value) {
2904
2907
  const buffer = Buffer.from(value, 'utf8');
2905
2908
  if (buffer.byteLength <= MAX_EXCERPT_BYTES) return value;
2906
- return `${buffer.subarray(0, MAX_EXCERPT_BYTES).toString('utf8')}\n - truncated at ${MAX_EXCERPT_BYTES} bytes]`;
2909
+ return `${buffer.subarray(0, MAX_EXCERPT_BYTES).toString('utf8')}\n - …[truncated at ${MAX_EXCERPT_BYTES} bytes]`;
2907
2910
  }
2908
2911
 
2909
2912
  /**
@@ -2952,7 +2955,7 @@ const OPTIONAL_FIELDS = new Set([
2952
2955
  'lockfiles', 'unpinned_deps', 'submodules', 'lfs', 'signal',
2953
2956
  // v0.4.0 §5: how the envelope travelled, and the path facts that came with it. Optional because a
2954
2957
  // v0.3.9 agent sends neither and must keep working unchanged; here rather than in
2955
- // `COMPARABLE_FIELDS` because **a path is not a result** — two byte-identical runs on two machines
2958
+ // `COMPARABLE_FIELDS` because **a path is not a result** - two byte-identical runs on two machines
2956
2959
  // must stay `consistent` when one of them punched a hole and the other used the relay.
2957
2960
  'transport', 'p2p',
2958
2961
  ]);
@@ -3164,7 +3167,7 @@ export async function apply(ctx, config = {}) {
3164
3167
  //
3165
3168
  // Composed here, next to the updater's own `ctx.effect`, and for the same reason: a socket and a
3166
3169
  // refresh timer have to be released when the plugin is unloaded, and an effect disposer is the
3167
- // only mechanism Cordis gives for that. Creating it never throws — a machine that cannot punch
3170
+ // only mechanism Cordis gives for that. Creating it never throws - a machine that cannot punch
3168
3171
  // still has to dispatch, and `w2m_status` is where the failure is reported.
3169
3172
  const p2p = createP2PRuntime(ctx, cfg, config.p2pBlock);
3170
3173
 
@@ -3180,14 +3183,13 @@ export async function apply(ctx, config = {}) {
3180
3183
  });
3181
3184
 
3182
3185
  // v0.6.1: the two browser routes behind the composer's target control. Additive and optional: a
3183
- // host without `connection.fetch` still gets every tool, and `w2m_status` reports the absence —
3184
- // which is the honest place for it, since a host that has no Connection at all is simply not a web
3186
+ // host without `connection.fetch` still gets every tool, and `w2m_status` reports the absence - // which is the honest place for it, since a host that has no Connection at all is simply not a web
3185
3187
  // composition and does not need a warning on every headless run.
3186
3188
  //
3187
3189
  // Nothing here reads the service: `registerTargetRoutes` declares the optional dependency with
3188
3190
  // `ctx.inject` and attaches from that callback (the comment there records why a plain
3189
3191
  // `ctx.connection` read fails the whole entry, and why the read cannot be synchronous anyway). The
3190
- // one mismatch worth a log line — a Connection that carries no fetch registry — is logged where it
3192
+ // one mismatch worth a log line - a Connection that carries no fetch registry - is logged where it
3191
3193
  // is observed, inside that callback, so this call returns a status object that stays live.
3192
3194
  const targetRoutes = registerTargetRoutes(ctx, {
3193
3195
  cfg: () => cfg,
@@ -3580,7 +3582,7 @@ export async function apply(ctx, config = {}) {
3580
3582
  //
3581
3583
  // The precedence rule is the whole contract, and it is one line: **the caller's own fields
3582
3584
  // always win**. `executor_machine_id` is checked below, and `target_machines` is never a tool
3583
- // argument — so a caller that named a machine (which, for this tool, means
3585
+ // argument - so a caller that named a machine (which, for this tool, means
3584
3586
  // `executor_machine_id` under broadcast) is untouched, and a caller that named none inherits
3585
3587
  // the selection. Nothing already published changes meaning: the selection only fills fields
3586
3588
  // that were absent, and the relay has accepted both of them since v0.3.3/v0.4.1.
@@ -3605,7 +3607,7 @@ export async function apply(ctx, config = {}) {
3605
3607
  * `applied` alone conflates three different situations an operator has to tell apart: nothing
3606
3608
  * was selected, the selection was overridden by the call's own `executor_machine_id`, or a
3607
3609
  * selection exists that names no machine at all and therefore routed nothing. The last one is
3608
- * the one that used to be invisible — a stored selection with no `machine_id` produces
3610
+ * the one that used to be invisible - a stored selection with no `machine_id` produces
3609
3611
  * `applied: false` and nothing else, which looks exactly like "the whole fleet ran it".
3610
3612
  */
3611
3613
  const selectionReason = selection === null
@@ -3692,14 +3694,14 @@ export async function apply(ctx, config = {}) {
3692
3694
 
3693
3695
  // §7: the two routing facts the offer carries to the machine that has to run the command.
3694
3696
  //
3695
- // `origin_machine_id` is *this* machine's id from `device.json` — where the work was typed, not
3696
- // where it runs — and it is omitted entirely (not sent as null) when this machine has no
3697
+ // `origin_machine_id` is *this* machine's id from `device.json` - where the work was typed, not
3698
+ // where it runs - and it is omitted entirely (not sent as null) when this machine has no
3697
3699
  // identity, because the relay's own validation refuses an empty string and "unknown origin" is
3698
3700
  // what a missing field already means on the wire.
3699
3701
  //
3700
3702
  // The identity comes from the runtime, which resolves it off the dispatch path; a dispatch that
3701
3703
  // arrives before that one small read has finished waits for it rather than racing it. The wait
3702
- // is bounded by the identity read itself and can never reject — see `createP2PRuntime`. This is
3704
+ // is bounded by the identity read itself and can never reject - see `createP2PRuntime`. This is
3703
3705
  // also the first use of the direct path, so it is where the node is started.
3704
3706
  await p2p.ensureStarted();
3705
3707
  const originMachineId =
@@ -3742,7 +3744,7 @@ export async function apply(ctx, config = {}) {
3742
3744
  // above refuses the combination, so this can never silently mean nothing.
3743
3745
  ...(mode === 'pipeline' && stagesPayload !== null ? { stages: stagesPayload } : {}),
3744
3746
  // v0.4.0: stated on every task, so `w2m_wait`'s `transport` and a machine's report can say
3745
- // which path the dispatcher expected to use — and so the executor in `direct` mode knows it
3747
+ // which path the dispatcher expected to use - and so the executor in `direct` mode knows it
3746
3748
  // must refuse an offer that did not arrive over the direct path.
3747
3749
  ...(originMachineId !== null ? { origin_machine_id: originMachineId } : {}),
3748
3750
  p2p: p2pOffer,
@@ -3788,7 +3790,7 @@ export async function apply(ctx, config = {}) {
3788
3790
  * so a push that fails, a peer that never announced and a socket that was never bound all leave
3789
3791
  * exactly the same outcome: the relay's own SSE offer is the delivery, and the machine's
3790
3792
  * `transport` says which path won. A failed push is therefore **not** an error and never fails
3791
- * this tool — but it is not hidden either, because `direct_pushes` names every attempt and the
3793
+ * this tool - but it is not hidden either, because `direct_pushes` names every attempt and the
3792
3794
  * reason it did not land.
3793
3795
  *
3794
3796
  * `relay` mode and a machine with no direct path both report `attempted: 0`, which is the
@@ -3823,7 +3825,7 @@ export async function apply(ctx, config = {}) {
3823
3825
  failed: pushes.filter((entry) => !entry.ok).length,
3824
3826
  pushes,
3825
3827
  // How many of the machines that were reached were reached over a channel *this* machine
3826
- // did not open — the executor dialled first, and the push landed on that path. Zero on a
3828
+ // did not open - the executor dialled first, and the push landed on that path. Zero on a
3827
3829
  // fleet where only the dispatcher initiates, which is what makes the two-way punch
3828
3830
  // visible in one dispatch's own answer instead of only in the ledger.
3829
3831
  reused_channels: pushes.filter((entry) => entry.reused === true).length,
@@ -4023,7 +4025,7 @@ export async function apply(ctx, config = {}) {
4023
4025
  * v0.4.0 §5: which path this machine's result travelled, and the path facts that came
4024
4026
  * with it. Read from the envelope because that is where the agent writes them, with the
4025
4027
  * per-machine record as the fallback (`GET /v1/tasks/{id}` copies both onto the machine
4026
- * entry as well). `null` means the machine did not say — which is exactly what a
4028
+ * entry as well). `null` means the machine did not say - which is exactly what a
4027
4029
  * v0.3.9 agent sends, so a missing value must not be dressed up as `'relay'`.
4028
4030
  */
4029
4031
  transport: envelope?.transport ?? machine?.transport ?? null,
@@ -4223,7 +4225,7 @@ export async function apply(ctx, config = {}) {
4223
4225
  const rtt = await readRtt(cfg);
4224
4226
  // §7: this tool is one of the two places the direct path is asked about, so it is one of the
4225
4227
  // two places the node is started. Never throws, and reports `enabled: false`/`running: false`
4226
- // rather than omitting itself — a reader has to be able to tell "relay mode" from "p2p was
4228
+ // rather than omitting itself - a reader has to be able to tell "relay mode" from "p2p was
4227
4229
  // requested and did not start".
4228
4230
  await p2p.ensureStarted();
4229
4231
  const p2pStatus = await readP2PStatus(cfg, p2p, Number.isInteger(relay.peers_announced) ? relay.peers_announced : null);
@@ -4303,8 +4305,8 @@ export async function apply(ctx, config = {}) {
4303
4305
  /**
4304
4306
  * v0.6.1: the composer's target control and the selections it holds.
4305
4307
  *
4306
- * Reported here because "the control is not in my composer" has exactly two causes — the
4307
- * host has no fetch-route registry, or the browser half is not loaded — and this is the
4308
+ * Reported here because "the control is not in my composer" has exactly two causes - the
4309
+ * host has no fetch-route registry, or the browser half is not loaded - and this is the
4308
4310
  * one call that can tell them apart from the host side. The selections themselves are
4309
4311
  * counts, not contents: which session picked which machine is the operator's business and
4310
4312
  * nothing the model needs.
@@ -4336,8 +4338,7 @@ export async function apply(ctx, config = {}) {
4336
4338
  * v0.8.5: **say it out loud.**
4337
4339
  *
4338
4340
  * This branch is the whole reason a machine can run v0.8.4 with the account-derived fleet key,
4339
- * hold a valid signed-in account, reach the relay over HTTP 200, and still be in no fleet —
4340
- * with nothing anywhere reporting a problem. The eight tools register, the plugin looks
4341
+ * hold a valid signed-in account, reach the relay over HTTP 200, and still be in no fleet - * with nothing anywhere reporting a problem. The eight tools register, the plugin looks
4341
4342
  * healthy, and the join sequence simply never runs.
4342
4343
  *
4343
4344
  * A silent no-op that looks like success is the worst failure mode this plugin has had. One
@@ -4661,6 +4662,335 @@ export async function apply(ctx, config = {}) {
4661
4662
  },
4662
4663
  presentCall: () => ({ card: 'generic', title: 'w2m stats', kind: 'read', rawInput: {} }),
4663
4664
  }));
4665
+
4666
+ /**
4667
+ * v0.9.6 — the individual memory layer.
4668
+ *
4669
+ * The relay keeps `command_hash`, `stdout_sha256` and `stdout_bytes` and never the text, deliberately:
4670
+ * the ledger is a tamper-evident consistency record, and a relay compromise discloses no command text.
4671
+ * The price is that nothing in the fleet could answer *"what did we already try on this machine, and
4672
+ * what happened?"* -- and that price was paid repeatedly, with the same facts rediscovered from scratch
4673
+ * across a dozen dispatches in one session.
4674
+ *
4675
+ * This tool is the read side of the fix. The write side is `appendMemory`, called by the agent as each
4676
+ * task finishes, in that machine's own state directory.
4677
+ *
4678
+ * ## What this first version does and does not reach
4679
+ *
4680
+ * It reaches **this machine's** memory directly: the file is local, so the read is a file read. It does
4681
+ * **not** yet read another machine's memory, and the reason is structural rather than an omission --
4682
+ * the relay carries no such file, an agent executes a dispatched command inside a sandbox whose writes
4683
+ * are scoped, and pretending otherwise would mean shipping a tool that silently returns nothing for
4684
+ * every machine but one.
4685
+ *
4686
+ * So a query for another machine says exactly that, names where to look instead, and stays useful: the
4687
+ * fleet's roster still comes from the relay, so the answer distinguishes "no such machine" from "that
4688
+ * machine keeps its own memory and this build cannot fetch it".
4689
+ */
4690
+ /**
4691
+ * The collective layer's filename, beside the profile that mounted this plugin.
4692
+ *
4693
+ * §3.2 of the design puts curated fleet knowledge on the archive host. This is the read half of it, and
4694
+ * it deliberately reads a *markdown document* rather than a store: the design's own argument is that a
4695
+ * memory which is merely a log is one nobody reads, so the collective layer is written for a person and
4696
+ * this tool returns it as text rather than pretending to have parsed it into facts. A parser over
4697
+ * hand-written prose would silently drop whatever it did not recognise, and the dropped part would be
4698
+ * exactly the part a human thought was worth writing down.
4699
+ */
4700
+ const COLLECTIVE_FILE = 'fleet-memory.md';
4701
+ /**
4702
+ * The collective document's path, or `null` when there is no profile to resolve it against.
4703
+ *
4704
+ * `resolveProfileDir` legitimately returns `null` -- a bare test context, or a host whose layout it
4705
+ * cannot read -- and the first version of this passed that straight into `path.join`, which threw
4706
+ * `ERR_INVALID_ARG_TYPE`. A read tool that crashes on a missing directory is worse than one that says
4707
+ * the directory is missing: the caller was asking a question, and an exception is not an answer.
4708
+ */
4709
+ const collectivePath = () => {
4710
+ const dir = resolveProfileDir(cfg);
4711
+ return typeof dir === 'string' && dir !== '' ? path.join(dir, COLLECTIVE_FILE) : null;
4712
+ };
4713
+
4714
+ ctx.tools.register(defineTool({
4715
+ name: 'w2m_memory',
4716
+ description:
4717
+ 'Ask what the fleet already knows and what this machine has already done. scope="local" reads ' +
4718
+ 'this machine\'s individual memory: one line per finished task with the command, exit code, ' +
4719
+ 'duration, transport and the tails of stdout/stderr. scope="fleet" reads the curated collective ' +
4720
+ 'memory: measured facts with their dates, decisions with their reasoning, and open problems. The ' +
4721
+ 'relay keeps only hashes, so these files are the only place a command\'s output survives. Use it ' +
4722
+ 'before re-running work -- it is what turns a dozen rediscovery dispatches into one lookup.',
4723
+ parameters: {
4724
+ scope: {
4725
+ type: 'string',
4726
+ enum: ['local', 'fleet'],
4727
+ description:
4728
+ 'local (default) = what this machine did; fleet = what the fleet knows, from the curated ' +
4729
+ 'collective memory.',
4730
+ default: 'local',
4731
+ },
4732
+ machine: {
4733
+ type: 'string',
4734
+ description:
4735
+ 'Which machine\'s memory to read. Defaults to this machine. Naming another machine is ' +
4736
+ 'answered with where its memory is kept, because this build can only read the local file.',
4737
+ },
4738
+ limit: {
4739
+ type: 'number',
4740
+ description: 'How many records to return, newest first. Defaults to 20.',
4741
+ default: 20,
4742
+ },
4743
+ include_previous: {
4744
+ type: 'boolean',
4745
+ description:
4746
+ 'Also read the rotated generation (`memory.jsonl.1`). The log is capped and rotates, so a ' +
4747
+ 'memory question about anything older than the current file needs this.',
4748
+ default: false,
4749
+ },
4750
+ record: {
4751
+ type: 'string',
4752
+ description:
4753
+ 'Record a conclusion against the most recent task: what its result meant. This is the only ' +
4754
+ 'field a machine is not allowed to write itself -- the machine stores the fact, a reader ' +
4755
+ 'asserts the meaning. Written as a new record so nothing already written is rewritten.',
4756
+ },
4757
+ },
4758
+ output: jsonOutput,
4759
+ async execute(args, exec) {
4760
+ throwIfAborted(exec?.signal);
4761
+ const limitRaw = Number(args?.limit);
4762
+ const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(Math.floor(limitRaw), 500) : 20;
4763
+
4764
+ const requested = typeof args?.machine === 'string' && args.machine.trim() !== '' ? args.machine.trim() : null;
4765
+ const identity = await readLocalIdentity(cfg);
4766
+ const self = identity?.machine_name ?? null;
4767
+ const selfId = identity?.machine_id ?? null;
4768
+
4769
+ /**
4770
+ * The collective layer, returned as the document it is.
4771
+ *
4772
+ * No parsing, on purpose. The file is written for a person -- facts with their evidence and the date
4773
+ * they were measured, decisions with the reasoning that produced them -- and a parser over prose
4774
+ * would silently drop whatever shape it did not expect. What it dropped would be exactly what a
4775
+ * human judged worth writing down, so the honest answer is the text plus where it came from.
4776
+ */
4777
+ if (args?.scope === 'fleet') {
4778
+ const file = collectivePath();
4779
+ let text = null;
4780
+ let error = null;
4781
+ if (file === null) {
4782
+ error = 'MEMORY_NO_COLLECTIVE';
4783
+ } else {
4784
+ try {
4785
+ text = await fs.readFile(file, 'utf8');
4786
+ } catch (readError) {
4787
+ error = readError?.code === 'ENOENT' ? 'MEMORY_NO_COLLECTIVE' : `MEMORY_READ_FAILED: ${readError?.code}`;
4788
+ }
4789
+ }
4790
+ const headings = text === null
4791
+ ? []
4792
+ : text.split('\n').filter((line) => /^#{1,3} /.test(line)).map((line) => line.replace(/^#+\s*/, '').trim());
4793
+ return JSON.stringify(
4794
+ {
4795
+ ok: true,
4796
+ scope: 'fleet',
4797
+ path: file,
4798
+ found: text !== null,
4799
+ bytes: text === null ? null : Buffer.byteLength(text, 'utf8'),
4800
+ // The section list is what makes a long document usable: a reader can see what the collective
4801
+ // memory covers without paying for the whole of it first.
4802
+ sections: headings,
4803
+ body: text,
4804
+ ...(error === null ? {} : { reason_code: error }),
4805
+ notes: [
4806
+ error === 'MEMORY_NO_COLLECTIVE'
4807
+ ? (file === null
4808
+ ? 'no profile directory could be resolved, so the collective memory has no known location'
4809
+ : `no collective memory at ${file}; it is written by hand on the archive host, and its absence means nothing has been curated there yet`)
4810
+ : null,
4811
+ 'this document is curated, not generated: every fact in it should carry the date it was measured and how',
4812
+ ].filter((n) => n !== null),
4813
+ },
4814
+ null,
4815
+ 2,
4816
+ );
4817
+ }
4818
+
4819
+ /**
4820
+ * Recording a conclusion.
4821
+ *
4822
+ * The one field a machine may not write for itself. `memoryRecord` always emits `conclusion: null`
4823
+ * because a machine that invented one would be asserting an interpretation it cannot have; this is
4824
+ * the path by which a reader -- a person, or the agent acting on their instruction -- supplies it.
4825
+ *
4826
+ * Appended as a new record rather than by rewriting the existing line. The log is append-only, and
4827
+ * in-place editing would break the property that makes it trustworthy: what was written is what
4828
+ * happened, and an amendment is a later statement about it. Rewriting would also lose the original
4829
+ * text, which is the thing a reader most needs when a conclusion turns out to be wrong.
4830
+ */
4831
+ const conclusionText = typeof args?.record === 'string' && args.record.trim() !== '' ? args.record.trim() : null;
4832
+ if (conclusionText !== null) {
4833
+ const stateDirForRecord = cfg.stateDir ?? null;
4834
+ if (stateDirForRecord === null) {
4835
+ return JSON.stringify(
4836
+ {
4837
+ ok: false,
4838
+ scope: 'local',
4839
+ reason_code: 'MEMORY_NO_STATE_DIR',
4840
+ detail: 'this plugin has no stateDir configured, so a conclusion has nowhere to be written',
4841
+ },
4842
+ null,
4843
+ 2,
4844
+ );
4845
+ }
4846
+ const recent = readMemorySync(stateDirForRecord, { limit: 1 });
4847
+ const subject = recent.records[0] ?? null;
4848
+ const written = appendMemory(stateDirForRecord, {
4849
+ at: new Date().toISOString(),
4850
+ kind: 'conclusion',
4851
+ task_id: subject?.task_id ?? null,
4852
+ machine_id: selfId,
4853
+ machine_name: self,
4854
+ // The conclusion is about the record named by `task_id`; the fields below repeat enough of it
4855
+ // that a reader does not have to join two lines to know what is being talked about.
4856
+ about: subject === null
4857
+ ? null
4858
+ : { argv: subject.argv ?? null, exit_code: subject.exit_code ?? null, at: subject.at ?? null },
4859
+ conclusion: conclusionText,
4860
+ tags: [],
4861
+ });
4862
+ return JSON.stringify(
4863
+ {
4864
+ ok: written.ok === true,
4865
+ scope: 'local',
4866
+ recorded: written.ok === true,
4867
+ task_id: subject?.task_id ?? null,
4868
+ conclusion: conclusionText,
4869
+ path: written.ok === true ? written.path : null,
4870
+ ...(written.ok === true ? {} : { reason_code: written.error }),
4871
+ notes: [
4872
+ subject === null
4873
+ ? 'no task has been recorded here yet, so this conclusion names no task; it is still kept, with about: null'
4874
+ : `recorded against task ${subject.task_id}`,
4875
+ 'appended, never rewritten: the original record is left as it was written',
4876
+ ],
4877
+ },
4878
+ null,
4879
+ 2,
4880
+ );
4881
+ }
4882
+
4883
+ // A query about another machine is answered honestly rather than with an empty list, which would
4884
+ // read as "that machine has done nothing" -- the opposite conclusion from "this build cannot see".
4885
+ if (requested !== null && requested !== self && requested !== selfId) {
4886
+ return JSON.stringify(
4887
+ {
4888
+ ok: true,
4889
+ scope: 'remote',
4890
+ requested_machine: requested,
4891
+ self: { machine_id: selfId, machine_name: self },
4892
+ records: null,
4893
+ reason_code: 'MEMORY_REMOTE_NOT_READABLE',
4894
+ detail:
4895
+ `the individual memory of "${requested}" is kept on that machine, in its own state ` +
4896
+ 'directory as memory.jsonl; this build reads only the local file, and the relay never ' +
4897
+ 'receives it',
4898
+ how_to_read_it: [
4899
+ `run \`w2m_run\` on ${requested} with a command that prints the tail of its memory.jsonl`,
4900
+ 'or read it directly over an administration channel to that machine',
4901
+ ],
4902
+ found: false,
4903
+ },
4904
+ null,
4905
+ 2,
4906
+ );
4907
+ }
4908
+
4909
+ const stateDir = cfg.stateDir ?? null;
4910
+ if (stateDir === null) {
4911
+ return JSON.stringify(
4912
+ {
4913
+ ok: true,
4914
+ scope: 'local',
4915
+ machine_id: selfId,
4916
+ machine_name: self,
4917
+ records: null,
4918
+ reason_code: 'MEMORY_NO_STATE_DIR',
4919
+ detail: 'this plugin has no stateDir configured, so there is nowhere the memory log would be',
4920
+ found: false,
4921
+ },
4922
+ null,
4923
+ 2,
4924
+ );
4925
+ }
4926
+
4927
+ const current = readMemorySync(stateDir, { limit });
4928
+ const previous = args?.include_previous === true ? readMemorySync(stateDir, { limit, previous: true }) : null;
4929
+
4930
+ /**
4931
+ * A small summary before the records themselves.
4932
+ *
4933
+ * The question that sent someone here is usually "have we run this before, and did it work", and a
4934
+ * count answers it faster than reading twenty lines. The counts are of the records *returned*, not
4935
+ * of the whole file, and that is stated -- a summary that silently described a different set than
4936
+ * the one shown is how a reader learns to distrust the summary.
4937
+ */
4938
+ const tally = (records) => {
4939
+ const by = {};
4940
+ for (const r of records ?? []) {
4941
+ const key = r?.status ?? 'unknown';
4942
+ by[key] = (by[key] ?? 0) + 1;
4943
+ }
4944
+ return by;
4945
+ };
4946
+
4947
+ return JSON.stringify(
4948
+ {
4949
+ ok: true,
4950
+ scope: 'local',
4951
+ machine_id: selfId,
4952
+ machine_name: self,
4953
+ state_dir: stateDir,
4954
+ memory_path: current.path,
4955
+ found: current.path !== null && (current.records.length > 0 || previous !== null),
4956
+ count: current.records.length,
4957
+ skipped_lines: current.skipped,
4958
+ truncated: current.truncated,
4959
+ status_tally: tally(current.records),
4960
+ records: current.records,
4961
+ ...(previous === null
4962
+ ? {}
4963
+ : {
4964
+ previous: {
4965
+ path: previous.path,
4966
+ count: previous.records.length,
4967
+ skipped_lines: previous.skipped,
4968
+ status_tally: tally(previous.records),
4969
+ records: previous.records,
4970
+ },
4971
+ }),
4972
+ notes: [
4973
+ current.path === null
4974
+ ? 'no memory has been written here yet: the log is created when a task finishes'
4975
+ : null,
4976
+ current.skipped > 0
4977
+ ? `${current.skipped} line(s) could not be parsed and were skipped; a partially written final line is the expected cause`
4978
+ : null,
4979
+ 'conclusion is null until something records what a result meant; the machine only stores the fact',
4980
+ 'the relay keeps hashes and never this text, so this file is the only durable copy of the output',
4981
+ ].filter((n) => n !== null),
4982
+ },
4983
+ null,
4984
+ 2,
4985
+ );
4986
+ },
4987
+ presentCall: (args) => ({
4988
+ card: 'generic',
4989
+ title: `w2m memory${typeof args?.machine === 'string' ? ` ${args.machine}` : ''}`,
4990
+ kind: 'read',
4991
+ rawInput: {},
4992
+ }),
4993
+ }));
4664
4994
  }
4665
4995
 
4666
4996
  /**
@@ -4735,14 +5065,14 @@ async function startLocalStack(ctx, cfg) {
4735
5065
  *
4736
5066
  * It called `createAgent` with `projectDir` and without `identity`, and passed a
4737
5067
  * `pairingCode` that `createAgent` does not accept. `createAgent` requires `project` and an
4738
- * `identity.machine_id`, so **`autoStartAgent: true` threw on its first call** — the code path
5068
+ * `identity.machine_id`, so **`autoStartAgent: true` threw on its first call** - the code path
4739
5069
  * that was supposed to bring an agent up had never run. That is why the fleet-join wire appeared
4740
5070
  * to be missing: the place it was meant to live could not be reached at all.
4741
5071
  *
4742
5072
  * Two more things were wrong with the shape:
4743
5073
  *
4744
5074
  * * it created a **local relay** unconditionally. For a machine joining a shared fleet that
4745
- * is the opposite of what is wanted — the machine must connect to the relay that already
5075
+ * is the opposite of what is wanted - the machine must connect to the relay that already
4746
5076
  * exists, not mint another one, and with `rabbitUrl` pointing at a remote host it would
4747
5077
  * have tried to bind a relay on that hostname;
4748
5078
  * * nothing paired the agent, so even a started agent could fetch no offers.