@nelsonlongxiang/dsh-open-a2a-net 0.6.24 → 0.6.26

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.
Files changed (38) hide show
  1. package/README.md +15 -11
  2. package/README_zh.md +3 -2
  3. package/assets/nexusDist/assets/index-DszoLz8i.js +5 -0
  4. package/assets/nexusDist/assets/index-uQf7NwjF.css +1 -0
  5. package/assets/nexusDist/index.html +2 -2
  6. package/cli/a2a.mjs +300 -0
  7. package/lib/card.js +17 -0
  8. package/lib/cli-deposit.js +141 -0
  9. package/lib/exposure-store.js +82 -0
  10. package/lib/index.js +904 -57
  11. package/lib/owner-settings.js +100 -0
  12. package/lib/peer-store.js +65 -9
  13. package/lib/task-ledger.js +44 -3
  14. package/lib/types/cli-deposit.d.ts +53 -0
  15. package/lib/types/exposure-store.d.ts +45 -0
  16. package/lib/types/index.d.ts +5 -0
  17. package/lib/types/owner-settings.d.ts +19 -0
  18. package/lib/types/peer-store.d.ts +22 -3
  19. package/lib/types/task-ledger.d.ts +18 -0
  20. package/lib/types/types.d.ts +10 -0
  21. package/nexus-stage/src/canvas-ops.ts +2 -2
  22. package/nexus-stage/src/federation.ts +6 -0
  23. package/nexus-stage/src/main.ts +97 -4
  24. package/nexus-stage/src/planning-menus.ts +16 -4
  25. package/nexus-stage/src/planning-types.ts +3 -0
  26. package/nexus-stage/src/planning-view.ts +208 -18
  27. package/nexus-stage/src/planning.css +4 -0
  28. package/nexus-stage/src/wb-chat.ts +670 -0
  29. package/nexus-stage/src/wb-directory.ts +222 -0
  30. package/nexus-stage/src/wb-ledger.ts +266 -0
  31. package/nexus-stage/src/wb-rail.ts +147 -0
  32. package/nexus-stage/src/workbench.css +190 -84
  33. package/nexus-stage/src/workbench.ts +385 -721
  34. package/nexus-stage/src/world.ts +5 -1
  35. package/package.json +18 -22
  36. package/assets/nexusDist/assets/index-034h1cNc.js +0 -3
  37. package/assets/nexusDist/assets/index-PG9EieGX.css +0 -1
  38. package/lib/types/client.d.ts +0 -73
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Owner-settings persistence (gh#34): the three unlock knobs a remote
3
+ * build-out needs — teamJoinAllowlist / teamScopeInfo / announce — written
4
+ * into the HOME patch layer (DSH_HOME/cordis.patch.yml), the authoritative
5
+ * config surface (the 3081 lesson: the home-level patch wins over the
6
+ * profile layer). A conservative line editor: it locates the a2a entry's
7
+ * config: block and sets exactly these three keys, never touching anything
8
+ * else; a missing entry is appended in the canonical short-id form.
9
+ * @module @nelsonlongxiang/dsh-open-a2a-net/owner-settings
10
+ */
11
+ /** The exact keys this module may touch, in wire order. */
12
+ const KEYS = ['teamJoinAllowlist', 'teamScopeInfo', 'announce'];
13
+ export function applyOwnerSettings(patchYaml, changes) {
14
+ const lines = patchYaml.split('\n');
15
+ // Find the a2a entry: `- id: a2a` / `- id: 'a2a'` variants.
16
+ let entryStart = -1;
17
+ for (let i = 0; i < lines.length; i += 1) {
18
+ const m = /^(\s*)-\s*id:\s*'?(a2a)'?\s*$/.exec(lines[i] ?? '');
19
+ if (m !== null) {
20
+ entryStart = i;
21
+ break;
22
+ }
23
+ }
24
+ let restartNeeded = false;
25
+ if (changes.announce !== undefined)
26
+ restartNeeded = true;
27
+ const writes = [];
28
+ if (changes.teamJoinAllowlist !== undefined) {
29
+ writes.push(changes.teamJoinAllowlist.length === 0
30
+ ? ' teamJoinAllowlist: []'
31
+ : ` teamJoinAllowlist: [${changes.teamJoinAllowlist.map(name => `'${name.replace(/'/g, "''")}'`).join(', ')}]`);
32
+ }
33
+ if (changes.teamScopeInfo !== undefined)
34
+ writes.push(` teamScopeInfo: ${changes.teamScopeInfo ? 'true' : 'false'}`);
35
+ if (changes.announce !== undefined)
36
+ writes.push(` announce: ${changes.announce ? 'true' : 'false'}`);
37
+ if (writes.length === 0)
38
+ return { yaml: patchYaml, restartNeeded };
39
+ if (entryStart === -1) {
40
+ // Append a canonical entry (the home patch is a top-level list).
41
+ const trailing = patchYaml.endsWith('\n') ? '' : '\n';
42
+ return { yaml: `${patchYaml}${trailing}- id: a2a\n config:\n${writes.join('\n')}\n`, restartNeeded };
43
+ }
44
+ // Locate the config: block inside the entry (the entry's own indentation
45
+ // is two spaces under the `- id:` line).
46
+ let i = entryStart + 1;
47
+ let configAt = -1;
48
+ while (i < lines.length) {
49
+ const line = lines[i] ?? '';
50
+ if (/^\S/.test(line))
51
+ break; // next top-level entry
52
+ if (/^\s{2}config:\s*$/.test(line)) {
53
+ configAt = i;
54
+ break;
55
+ }
56
+ i += 1;
57
+ }
58
+ if (configAt === -1) {
59
+ // Insert a config: block right after the id line.
60
+ lines.splice(entryStart + 1, 0, ' config:', ...writes);
61
+ return { yaml: lines.join('\n'), restartNeeded };
62
+ }
63
+ // Rewrite exactly our keys inside the block; keep foreign lines verbatim.
64
+ const blockIndent = ' ';
65
+ let insertAt = configAt + 1;
66
+ for (const key of KEYS) {
67
+ let found = false;
68
+ let j = configAt + 1;
69
+ while (j < lines.length) {
70
+ const line = lines[j] ?? '';
71
+ if (/^\S/.test(line) || /^\s{2}\S/.test(line) && !/^\s{4}/.test(line) && !/^\s{2}config:/.test(lines[configAt] ?? '')) {
72
+ if (!line.startsWith(blockIndent) && !/^\s*$/.test(line) && j > configAt) {
73
+ // leaving the block when a line is less indented than the keys
74
+ if (!line.startsWith(' '))
75
+ break;
76
+ }
77
+ }
78
+ const kv = new RegExp(`^\\s*${key}:`).exec(line);
79
+ if (kv !== null) {
80
+ const write = writes.find(w => w.trim().startsWith(`${key}:`));
81
+ if (write !== undefined) {
82
+ lines[j] = write;
83
+ found = true;
84
+ }
85
+ break;
86
+ }
87
+ if (/^\s*-\s*id:/.test(line))
88
+ break;
89
+ j += 1;
90
+ }
91
+ if (!found) {
92
+ const write = writes.find(w => w.trim().startsWith(`${key}:`));
93
+ if (write !== undefined) {
94
+ lines.splice(insertAt, 0, write);
95
+ insertAt += 1;
96
+ }
97
+ }
98
+ }
99
+ return { yaml: lines.join('\n'), restartNeeded };
100
+ }
package/lib/peer-store.js CHANGED
@@ -48,8 +48,38 @@ export class PeerStore {
48
48
  // The config is authoritative for a seed: it is (re)marked as one —
49
49
  // immune to eviction — and restarts from the initial score regardless
50
50
  // of the persisted one. Replacing the record wholesale keeps PeerRecord
51
- // fields readonly.
52
- this.peers.set(seed, { url: seed, score: INITIAL_SCORE, seed: true });
51
+ // fields readonly. The contact stamps are observations, not config:
52
+ // they carry over, so a restart does not blank a seed's lamp (gh#24).
53
+ const prior = this.peers.get(seed);
54
+ this.peers.set(seed, {
55
+ url: seed,
56
+ score: INITIAL_SCORE,
57
+ seed: true,
58
+ ...(prior?.lastContactAt !== undefined ? { lastContactAt: prior.lastContactAt } : {}),
59
+ ...(prior?.lastContactOk !== undefined ? { lastContactOk: prior.lastContactOk } : {}),
60
+ });
61
+ }
62
+ // The config is authoritative BOTH ways (board-stale-peer fix, 0.6.26):
63
+ // a persisted record may carry seed:true from an OLD config generation
64
+ // whose operator already removed the URL — left alone, a dead URL keeps
65
+ // its immunity forever and the workbench board shows a peer that can
66
+ // never evict (owner sighting: `.157:13082` survived its config removal
67
+ // for days). Downgrade those to referral-learned, keeping the score so
68
+ // noteFailure can decay and evict them honestly.
69
+ const current = new Set(seeds.filter(seed => seed !== ''));
70
+ for (const [url, peer] of this.peers) {
71
+ if (peer.seed && !current.has(url)) {
72
+ // Downgrade keeps every learned fact (score, session, contact
73
+ // stamps) — only the immunity bit changes.
74
+ this.peers.set(url, {
75
+ url,
76
+ score: peer.score,
77
+ seed: false,
78
+ ...(peer.session !== undefined ? { session: peer.session } : {}),
79
+ ...(peer.lastContactAt !== undefined ? { lastContactAt: peer.lastContactAt } : {}),
80
+ ...(peer.lastContactOk !== undefined ? { lastContactOk: peer.lastContactOk } : {}),
81
+ });
82
+ }
53
83
  }
54
84
  }
55
85
  /**
@@ -106,18 +136,24 @@ export class PeerStore {
106
136
  this.persist();
107
137
  }
108
138
  /**
109
- * Note a failed fetch of one peer. A non-seed's score drops; an
110
- * at-or-below-floor peer is evicted, so a bad referral disappears rather
111
- * than degrading forever. Seeds are config-owned and never penalized.
139
+ * Note a failed fetch of one peer. Every peer records the failed attempt
140
+ * (the host-rail lamp turns red for a seed and a learned peer alike,
141
+ * gh#24); only a non-seed's score drops, and an at-or-below-floor peer is
142
+ * evicted, so a bad referral disappears rather than degrading forever.
143
+ * Seeds are config-owned and never penalized.
112
144
  * @param url - the peer that failed.
113
145
  */
114
146
  noteFailure(url) {
115
147
  const peer = this.peers.get(url);
116
- if (peer === undefined || peer.seed)
148
+ if (peer === undefined)
117
149
  return;
118
- peer.score -= FAILURE_PENALTY;
119
- if (peer.score <= EVICTION_FLOOR)
120
- this.peers.delete(url);
150
+ peer.lastContactAt = Date.now();
151
+ peer.lastContactOk = false;
152
+ if (!peer.seed) {
153
+ peer.score -= FAILURE_PENALTY;
154
+ if (peer.score <= EVICTION_FLOOR)
155
+ this.peers.delete(url);
156
+ }
121
157
  this.persist();
122
158
  }
123
159
  /**
@@ -129,8 +165,24 @@ export class PeerStore {
129
165
  if (peer === undefined)
130
166
  return;
131
167
  peer.score += SUCCESS_REWARD;
168
+ peer.lastContactAt = Date.now();
169
+ peer.lastContactOk = true;
132
170
  this.persist();
133
171
  }
172
+ /**
173
+ * The epoch ms of the last contact attempt with one tracked peer (#20 W-C
174
+ * host-rail health lamp), or undefined when the store never touched it.
175
+ */
176
+ lastContact(url) {
177
+ return this.peers.get(url)?.lastContactAt;
178
+ }
179
+ /**
180
+ * Whether the last contact attempt succeeded (gh#24) — undefined when no
181
+ * attempt was ever recorded.
182
+ */
183
+ lastContactOk(url) {
184
+ return this.peers.get(url)?.lastContactOk;
185
+ }
134
186
  /**
135
187
  * The current quality score of one tracked peer, for state reporting.
136
188
  * @param url - the peer URL.
@@ -189,6 +241,10 @@ export class PeerStore {
189
241
  score: typeof peer.score === 'number' && Number.isFinite(peer.score) ? peer.score : INITIAL_SCORE,
190
242
  seed: peer.seed === true,
191
243
  ...(typeof peer.session === 'string' && peer.session !== '' ? { session: peer.session } : {}),
244
+ // gh#24: the contact stamp round-trips (a restart must not forget
245
+ // when we last reached the peer, nor whether it answered).
246
+ ...(typeof peer.lastContactAt === 'number' && Number.isFinite(peer.lastContactAt) ? { lastContactAt: peer.lastContactAt } : {}),
247
+ ...(typeof peer.lastContactOk === 'boolean' ? { lastContactOk: peer.lastContactOk } : {}),
192
248
  });
193
249
  }
194
250
  }
@@ -60,6 +60,29 @@ export function briefOf(message) {
60
60
  * lazy — every read/write entry point sweeps first.
61
61
  */
62
62
  export const TASK_STALE_TTL_MS = 24 * 60 * 60_000;
63
+ const ARCHIVE_ENDS = ['receipt', 'echo', 'abandoned', 'cancelled'];
64
+ /** Summary marker {@link TaskLedger.resolveEcho} writes on echo-settled rows. */
65
+ const ECHO_SUMMARY = '(settled by content echo)';
66
+ /**
67
+ * How one archived row ended (gh#31). The recorded field wins; legacy rows
68
+ * fall back to the summary markers this ledger writes itself. The fallback
69
+ * reads a receipt-settled row's summary, which is the peer's text, so it is
70
+ * only as exact as the legacy data allows.
71
+ * @param row - the archived record.
72
+ * @returns the archive path that ended it.
73
+ */
74
+ export function archivedEndedBy(row) {
75
+ if (row.endedBy !== undefined)
76
+ return row.endedBy;
77
+ const summary = row.summary ?? '';
78
+ if (summary.startsWith('caller-cancelled'))
79
+ return 'cancelled';
80
+ if (summary.startsWith('caller-abandoned'))
81
+ return 'abandoned';
82
+ if (summary === ECHO_SUMMARY)
83
+ return 'echo';
84
+ return 'receipt';
85
+ }
63
86
  /**
64
87
  * Bounded outbound-task ledger with whole-file persistence. Pure of any
65
88
  * routing knowledge: callers {@link track} a task when a route leaves it
@@ -247,6 +270,8 @@ export class TaskLedger {
247
270
  resolvedAt,
248
271
  summary: `caller-abandoned${cause}`,
249
272
  outcome: 'abandoned',
273
+ endedBy: 'abandoned',
274
+ ...(record.direction !== undefined ? { direction: record.direction } : {}),
250
275
  });
251
276
  this.persist();
252
277
  return { outcome: 'cleared', taskId, archivedAt: resolvedAt };
@@ -286,6 +311,8 @@ export class TaskLedger {
286
311
  resolvedAt,
287
312
  summary: `caller-cancelled${cause}`,
288
313
  outcome: 'abandoned',
314
+ endedBy: 'cancelled',
315
+ ...(record.direction !== undefined ? { direction: record.direction } : {}),
289
316
  });
290
317
  this.persist();
291
318
  return {
@@ -334,6 +361,8 @@ export class TaskLedger {
334
361
  ...(summary !== '' ? { summary } : {}),
335
362
  ...(envelopeOutcome !== undefined ? { outcome: envelopeOutcome } : {}),
336
363
  ...(elapsedMs !== undefined ? { elapsedMs } : {}),
364
+ endedBy: 'receipt',
365
+ ...(record.direction !== undefined ? { direction: record.direction } : {}),
337
366
  });
338
367
  this.persist();
339
368
  return {
@@ -354,7 +383,11 @@ export class TaskLedger {
354
383
  // rewrite the abandonment outcome — the caller stopped waiting by choice.
355
384
  // Record that the target did answer later; keep the decision verbatim and
356
385
  // pin the projected verdict to 'abandoned'.
357
- const callerEnded = CALLER_ENDED_PREFIX.test(settled.summary ?? '');
386
+ // gh#31: the recorded end wins; the summary prefix only decides legacy
387
+ // rows (a receipt-settled row's summary is the peer's own text).
388
+ const callerEnded = settled.endedBy !== undefined
389
+ ? settled.endedBy === 'abandoned' || settled.endedBy === 'cancelled'
390
+ : CALLER_ENDED_PREFIX.test(settled.summary ?? '');
358
391
  this.archived = [
359
392
  {
360
393
  ...settled,
@@ -409,7 +442,9 @@ export class TaskLedger {
409
442
  startedAt: record.startedAt,
410
443
  ...(record.contextId !== undefined ? { contextId: record.contextId } : {}),
411
444
  resolvedAt: this.now(),
412
- summary: '(settled by content echo)',
445
+ summary: ECHO_SUMMARY,
446
+ endedBy: 'echo',
447
+ ...(record.direction !== undefined ? { direction: record.direction } : {}),
413
448
  });
414
449
  settled.push(record.taskId);
415
450
  }
@@ -468,6 +503,8 @@ export class TaskLedger {
468
503
  ...(typeof entry.summary === 'string' && entry.summary !== '' ? { summary: entry.summary } : {}),
469
504
  ...(typeof entry.outcome === 'string' && RECEIPT_OUTCOMES.includes(entry.outcome) ? { outcome: entry.outcome } : {}),
470
505
  ...(typeof entry.lateReceiptAt === 'number' && Number.isFinite(entry.lateReceiptAt) ? { lateReceiptAt: entry.lateReceiptAt } : {}),
506
+ ...(typeof entry.endedBy === 'string' && ARCHIVE_ENDS.includes(entry.endedBy) ? { endedBy: entry.endedBy } : {}),
507
+ ...(entry.direction === 'inbound' || entry.direction === 'outbound' ? { direction: entry.direction } : {}),
471
508
  });
472
509
  continue;
473
510
  }
@@ -478,7 +515,9 @@ export class TaskLedger {
478
515
  startedAt: typeof entry.startedAt === 'number' && Number.isFinite(entry.startedAt) ? entry.startedAt : this.now(),
479
516
  ...(typeof entry.contextId === 'string' && entry.contextId !== '' ? { contextId: entry.contextId } : {}),
480
517
  ...(typeof entry.brief === 'string' && entry.brief !== '' ? { brief: entry.brief } : {}),
481
- ...(entry.direction === 'inbound' ? { direction: 'inbound' } : {}),
518
+ // gh#31: both sides round-trip — /state omits direction only when
519
+ // it is unknown, so a restored outbound row must not lose it.
520
+ ...(entry.direction === 'inbound' || entry.direction === 'outbound' ? { direction: entry.direction } : {}),
482
521
  ...(entry.receiptExpected === false ? { receiptExpected: false } : {}),
483
522
  status: entry.status === 'dead' ? 'dead' : 'pending',
484
523
  ...(entry.status === 'dead'
@@ -509,6 +548,8 @@ export class TaskLedger {
509
548
  ...(typeof entry.summary === 'string' && entry.summary !== '' ? { summary: entry.summary } : {}),
510
549
  ...(typeof entry.outcome === 'string' && RECEIPT_OUTCOMES.includes(entry.outcome) ? { outcome: entry.outcome } : {}),
511
550
  ...(typeof entry.lateReceiptAt === 'number' && Number.isFinite(entry.lateReceiptAt) ? { lateReceiptAt: entry.lateReceiptAt } : {}),
551
+ ...(typeof entry.endedBy === 'string' && ARCHIVE_ENDS.includes(entry.endedBy) ? { endedBy: entry.endedBy } : {}),
552
+ ...(entry.direction === 'inbound' || entry.direction === 'outbound' ? { direction: entry.direction } : {}),
512
553
  });
513
554
  }
514
555
  }
@@ -0,0 +1,53 @@
1
+ /** Owner ruling ③: the default AND maximum online window. */
2
+ export declare const CLI_DEPOSIT_TTL_MS: number;
3
+ /** Owner ruling ①: <agent-name>/<uuid·8B>. The name is [a-z0-9-]{1,32}. */
4
+ export declare const CLI_HANDLE_RE: RegExp;
5
+ /** One deposited CLI identity. */
6
+ export interface CliDeposit {
7
+ readonly handle: string;
8
+ /** The bare agent-name zone (card.team). */
9
+ readonly name: string;
10
+ /** The CLI's signed card, verbatim wire JSON (served back as-is). */
11
+ readonly cardJson: string;
12
+ readonly depositedAt: number;
13
+ readonly expiresAt: number;
14
+ }
15
+ /** One mailbox entry for a deposited handle. */
16
+ export interface CliMail {
17
+ readonly at: number;
18
+ readonly from: string;
19
+ readonly text: string;
20
+ }
21
+ export declare class CliDepositStore {
22
+ private readonly path;
23
+ private deposits;
24
+ private mailbox;
25
+ private now;
26
+ constructor(path: string, options?: {
27
+ now?: () => number;
28
+ });
29
+ /** Deposit (or refresh) one CLI card. TTL is clamped to the 4h ruling. */
30
+ deposit(cardJson: string): {
31
+ ok: true;
32
+ handle: string;
33
+ expiresAt: number;
34
+ } | {
35
+ ok: false;
36
+ error: string;
37
+ };
38
+ revoke(handle: string): boolean;
39
+ /** Active deposits (sweeping expired ones — ruling ③ auto-deregister). */
40
+ list(): readonly CliDeposit[];
41
+ get(handle: string): CliDeposit | undefined;
42
+ /** Card-serve rows: the handle + display name, marked cli (no wake). */
43
+ cardRows(): Array<{
44
+ team: string;
45
+ name: string;
46
+ description: string;
47
+ }>;
48
+ mail(handle: string, from: string, text: string): void;
49
+ readMail(handle: string): readonly CliMail[];
50
+ private sweep;
51
+ private persist;
52
+ private restore;
53
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * #19 exposure tiers (ruling final, D3/D4): per-session visibility tier on
3
+ * the A2A network — 'linked' (the default floor: visible to linked callers
4
+ * per the W3 gate) or 'fleet' (the member's voluntary opt-UP: visible to
5
+ * any fleet peer). v1 carries exactly these two tiers (D5: no private
6
+ * residue below linked).
7
+ *
8
+ * D3: joins default to linked — existing members transfer automatically
9
+ * (tier absence IS linked, so no migration is needed); a member may raise
10
+ * itself to fleet and lower itself back (fleet is an opt-up, linked is the
11
+ * floor, never a ceiling decision taken from the member).
12
+ *
13
+ * D4 (成文, zero group knobs): a GROUP's visibility equals its STRICTEST
14
+ * member tier — one linked member keeps the group linked-level. The rule
15
+ * binds every future group surface (canvas listing, fleet feeds); v1 has
16
+ * no group-level tier API by design, so the rule needs no enforcement
17
+ * seam yet.
18
+ * @module @nelsonlongxiang/dsh-open-a2a-net/exposure-store
19
+ */
20
+ /** The two v1 tiers (D5: nothing below linked exists). */
21
+ export type ExposureTier = 'linked' | 'fleet';
22
+ /** The persisted tier document (whole-file read/write, small by design). */
23
+ export interface ExposureSnapshot {
24
+ readonly tiers?: Readonly<Record<string, ExposureTier>>;
25
+ }
26
+ export declare class ExposureStore {
27
+ private readonly path;
28
+ private tiers;
29
+ /**
30
+ * @param path - persistence file (`<dsh-home>/a2a/exposure.json`); empty = no persistence.
31
+ */
32
+ constructor(path: string);
33
+ /**
34
+ * The session's tier — 'linked' unless explicitly raised to 'fleet'
35
+ * (D3: existing members transfer as linked automatically).
36
+ */
37
+ tierOf(id: string): ExposureTier;
38
+ /**
39
+ * Set the session's tier. Members may opt up to fleet and back down to
40
+ * linked; there is no tier below linked (D5) and no group-level knob
41
+ * (D4 — group visibility derives from the strictest member).
42
+ */
43
+ setTier(id: string, tier: ExposureTier): void;
44
+ private persist;
45
+ }
@@ -103,6 +103,11 @@ export interface Config {
103
103
  * self-asserted already-linked name must not pass on shape alone).
104
104
  */
105
105
  readonly originClaimWindow: boolean;
106
+ /**
107
+ * #19 D2: card-face sessionTeams excerpt exposure — 'full' | 'masked'
108
+ * | 'off' (owner-level; default full). See the schema comment.
109
+ */
110
+ readonly exposureExcerpt: 'full' | 'masked' | 'off';
106
111
  /**
107
112
  * Override for the OriginClaim callerUrl anchor. Loopback-bound hosts
108
113
  * behind a pocket tunnel must name the card URL peers can actually
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Owner-settings persistence (gh#34): the three unlock knobs a remote
3
+ * build-out needs — teamJoinAllowlist / teamScopeInfo / announce — written
4
+ * into the HOME patch layer (DSH_HOME/cordis.patch.yml), the authoritative
5
+ * config surface (the 3081 lesson: the home-level patch wins over the
6
+ * profile layer). A conservative line editor: it locates the a2a entry's
7
+ * config: block and sets exactly these three keys, never touching anything
8
+ * else; a missing entry is appended in the canonical short-id form.
9
+ * @module @nelsonlongxiang/dsh-open-a2a-net/owner-settings
10
+ */
11
+ export interface OwnerSettings {
12
+ teamJoinAllowlist?: string[];
13
+ teamScopeInfo?: boolean;
14
+ announce?: boolean;
15
+ }
16
+ export declare function applyOwnerSettings(patchYaml: string, changes: OwnerSettings): {
17
+ yaml: string;
18
+ restartNeeded: boolean;
19
+ };
@@ -25,6 +25,13 @@ export interface PeerRecord {
25
25
  readonly seed: boolean;
26
26
  /** The signed-card session the peer served last, when a fetch revealed it. */
27
27
  readonly session?: string;
28
+ /** Epoch ms of the last contact attempt (success or failure) — the host
29
+ * rail's health lamp reads the recency of this (#20 W-C). */
30
+ lastContactAt?: number;
31
+ /** Whether that last attempt SUCCEEDED (gh#24): the lamp distinguishes a
32
+ * fresh success (green) from a fresh failure (red); recency alone made
33
+ * a dead peer glow green. */
34
+ lastContactOk?: boolean;
28
35
  }
29
36
  /** The persisted store document (whole-file read/write, small by design). */
30
37
  export interface PeerStoreSnapshot {
@@ -77,9 +84,11 @@ export declare class PeerStore {
77
84
  */
78
85
  drop(url: string): void;
79
86
  /**
80
- * Note a failed fetch of one peer. A non-seed's score drops; an
81
- * at-or-below-floor peer is evicted, so a bad referral disappears rather
82
- * than degrading forever. Seeds are config-owned and never penalized.
87
+ * Note a failed fetch of one peer. Every peer records the failed attempt
88
+ * (the host-rail lamp turns red for a seed and a learned peer alike,
89
+ * gh#24); only a non-seed's score drops, and an at-or-below-floor peer is
90
+ * evicted, so a bad referral disappears rather than degrading forever.
91
+ * Seeds are config-owned and never penalized.
83
92
  * @param url - the peer that failed.
84
93
  */
85
94
  noteFailure(url: string): void;
@@ -88,6 +97,16 @@ export declare class PeerStore {
88
97
  * @param url - the peer that answered with a verified card.
89
98
  */
90
99
  noteSuccess(url: string): void;
100
+ /**
101
+ * The epoch ms of the last contact attempt with one tracked peer (#20 W-C
102
+ * host-rail health lamp), or undefined when the store never touched it.
103
+ */
104
+ lastContact(url: string): number | undefined;
105
+ /**
106
+ * Whether the last contact attempt succeeded (gh#24) — undefined when no
107
+ * attempt was ever recorded.
108
+ */
109
+ lastContactOk(url: string): boolean | undefined;
91
110
  /**
92
111
  * The current quality score of one tracked peer, for state reporting.
93
112
  * @param url - the peer URL.
@@ -52,6 +52,8 @@ export declare const TASK_STALE_TTL_MS: number;
52
52
  export type TaskStatus = 'pending' | 'dead';
53
53
  /** F5 layer-3: why a dead row aged out (stamped by the sweep at flip time). */
54
54
  export type DeadReason = 'fire-forget' | 'unconsumed';
55
+ /** gh#31: which archive path ended a row — a receipt, a content echo, or the caller. */
56
+ export type ArchiveEnd = 'receipt' | 'echo' | 'abandoned' | 'cancelled';
55
57
  /** One tracked outbound task still in the owed book (unsettled). */
56
58
  export interface TaskRecord {
57
59
  readonly taskId: string;
@@ -111,7 +113,23 @@ export interface ArchivedRecord {
111
113
  * recorded as metadata only, never promoted to resolution.
112
114
  */
113
115
  readonly lateReceiptAt?: number;
116
+ /**
117
+ * gh#31: which archive path ended the row, recorded at archive time. Absent
118
+ * on rows archived before the field — {@link archivedEndedBy} derives it.
119
+ */
120
+ readonly endedBy?: ArchiveEnd;
121
+ /** gh#31: which side of the wire the row served, copied from the owed row (absent on rows archived before the field). */
122
+ readonly direction?: 'inbound' | 'outbound';
114
123
  }
124
+ /**
125
+ * How one archived row ended (gh#31). The recorded field wins; legacy rows
126
+ * fall back to the summary markers this ledger writes itself. The fallback
127
+ * reads a receipt-settled row's summary, which is the peer's text, so it is
128
+ * only as exact as the legacy data allows.
129
+ * @param row - the archived record.
130
+ * @returns the archive path that ended it.
131
+ */
132
+ export declare function archivedEndedBy(row: ArchivedRecord): ArchiveEnd;
115
133
  /** Result of an explicit caller-side abandon. Total and never-thrown. */
116
134
  export type AbandonOutcome = 'cleared' | 'already-terminal' | 'unknown';
117
135
  /** Structured verdict for one {@link TaskLedger.abandon} call (consumer contract: idempotent). */
@@ -21,6 +21,16 @@ export interface A2aPeerCard {
21
21
  * not by this card's signature.
22
22
  */
23
23
  readonly peers?: readonly string[];
24
+ /**
25
+ * gh#41 fleet planning: this host's own canvas-team organization, served
26
+ * fresh on the FULL card (linked callers only) — each entry names a team
27
+ * and its member handles so any linked peer's board can render the org.
28
+ */
29
+ readonly canvasOrg?: ReadonlyArray<{
30
+ name: string;
31
+ team: string;
32
+ members: readonly string[];
33
+ }>;
24
34
  /**
25
35
  * Joined session teams this peer currently dispatches. Unsigned and
26
36
  * optional: served fresh at read time, each entry is a team name the
@@ -20,8 +20,8 @@ export type RosterOp = { op: 'remove' | 'add'; id: string }
20
20
  /** The five user-facing write actions (one per design.md §3.3 row), plus the two network join/leave actions the panel-slim migration moved into the planning view. */
21
21
  export type CanvasAction =
22
22
  | { type: 'create-team'; name: string; ids: readonly string[]; /** true when the model had no such team before this action (a serial failure then leaves a ghost EMPTY team on the host — the wire compensates with a remove). */ created?: boolean }
23
- | { type: 'add-member'; team: string; ids: readonly string[] }
24
- | { type: 'remove-member'; team: string; ids: readonly string[] }
23
+ | { type: 'add-member'; team: string; ids: readonly string[]; /** gh#41: set when the target frame is a REMOTE peer's team — the wire routes the op through the host fleet relay. */ fleetPeer?: string }
24
+ | { type: 'remove-member'; team: string; ids: readonly string[]; /** gh#41: see add-member. */ fleetPeer?: string }
25
25
  | { type: 'remove-team'; name: string }
26
26
  /** `ops` comes from reorderOps() at emit time; the wire stays model-free. */
27
27
  | { type: 'reorder'; team: string; ops: ReadonlyArray<RosterOp> }
@@ -26,6 +26,12 @@ export interface InFlightRow {
26
26
  export interface PeerRow {
27
27
  url: string
28
28
  score?: number
29
+ /** gh#20 W-A: this host's link view — 'linked' (operator-established
30
+ * basis holds) | 'stranger' (no link); absent = the host predates the
31
+ * field (render neutral). */
32
+ link?: 'linked' | 'stranger'
33
+ /** Signature-valid cached card held (the badge tick/cross). */
34
+ verified?: boolean
29
35
  }
30
36
 
31
37
  /** A team frame's titlebar anchor {fx + fw/2, fy + 11} (edges.ts lockstep). */