@openscout/protocol 0.2.98 → 0.2.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actors.d.ts +2 -2
- package/dist/actors.d.ts.map +1 -1
- package/dist/actors.js +1 -0
- package/dist/broker-routes.d.ts +8 -0
- package/dist/broker-routes.d.ts.map +1 -1
- package/dist/broker-routes.js +15 -0
- package/dist/channel-identity.d.ts +77 -0
- package/dist/channel-identity.d.ts.map +1 -1
- package/dist/channel-identity.js +134 -0
- package/dist/channel-invites.d.ts +179 -0
- package/dist/channel-invites.d.ts.map +1 -0
- package/dist/channel-invites.js +166 -0
- package/dist/channel-reception.d.ts +78 -0
- package/dist/channel-reception.d.ts.map +1 -0
- package/dist/channel-reception.js +219 -0
- package/dist/common.d.ts +1 -1
- package/dist/common.d.ts.map +1 -1
- package/dist/conversation-projection.d.ts +1 -1
- package/dist/conversation-projection.js +1 -1
- package/dist/herdr-workspace-digest.d.ts +151 -0
- package/dist/herdr-workspace-digest.d.ts.map +1 -0
- package/dist/herdr-workspace-digest.js +343 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/machines.d.ts +233 -0
- package/dist/machines.d.ts.map +1 -0
- package/dist/machines.js +550 -0
- package/dist/mesh.d.ts +8 -0
- package/dist/mesh.d.ts.map +1 -1
- package/dist/runtime-catalog-contract.d.ts +1 -1
- package/dist/runtime-catalog-contract.d.ts.map +1 -1
- package/dist/runtime-catalog-contract.js +1 -1
- package/dist/runtime-catalog.generated.d.ts +34 -15
- package/dist/runtime-catalog.generated.d.ts.map +1 -1
- package/dist/runtime-catalog.generated.js +46 -19
- package/dist/runtime-execution.d.ts +1 -1
- package/dist/runtime-execution.d.ts.map +1 -1
- package/dist/scout-delivery.d.ts +1 -0
- package/dist/scout-delivery.d.ts.map +1 -1
- package/dist/terminal-host-topology.d.ts +20 -1
- package/dist/terminal-host-topology.d.ts.map +1 -1
- package/dist/terminal-host-topology.js +8 -2
- package/dist/transports.d.ts +39 -1
- package/dist/transports.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/actors.ts +2 -0
- package/src/broker-routes.ts +17 -0
- package/src/channel-identity.ts +151 -0
- package/src/channel-invites.ts +343 -0
- package/src/channel-reception.ts +308 -0
- package/src/common.ts +1 -0
- package/src/conversation-projection.ts +1 -1
- package/src/herdr-workspace-digest.ts +458 -0
- package/src/index.ts +4 -0
- package/src/machines.ts +743 -0
- package/src/mesh.ts +8 -0
- package/src/runtime-catalog-contract.ts +1 -1
- package/src/runtime-catalog.generated.ts +46 -19
- package/src/runtime-catalog.v1.json +18 -8
- package/src/scout-delivery.ts +1 -0
- package/src/terminal-host-topology.ts +32 -2
- package/src/transports.ts +48 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel invitations: a portable, scoped, revocable capability that lets a
|
|
3
|
+
* teammate bring an agent session they are *already running* into a Scout
|
|
4
|
+
* channel.
|
|
5
|
+
*
|
|
6
|
+
* Three boundaries shape this module.
|
|
7
|
+
*
|
|
8
|
+
* 1. **The secret never lands in a record.** Only `tokenHash` is persisted, so
|
|
9
|
+
* a snapshot, a journal line, or a log dump cannot be replayed into channel
|
|
10
|
+
* membership. The raw token exists in the invitation URL and nowhere else.
|
|
11
|
+
* 2. **Membership is not reception.** Redeeming an invitation makes an actor a
|
|
12
|
+
* channel participant. Whether that actor can actually *receive* is a
|
|
13
|
+
* separate, live judgement made from endpoint evidence — see
|
|
14
|
+
* {@link deriveChannelReception}.
|
|
15
|
+
* 3. **Reachability is stated, not assumed.** A loopback route is recorded as
|
|
16
|
+
* `local_only` so a surface can say plainly that the link will not work for
|
|
17
|
+
* a teammate on another machine.
|
|
18
|
+
*
|
|
19
|
+
* Invites live in the owning conversation's metadata under
|
|
20
|
+
* {@link CHANNEL_INVITES_METADATA_KEY}; the broker remains the only writer.
|
|
21
|
+
*/
|
|
22
|
+
export const CHANNEL_INVITES_METADATA_KEY = "channelInvites";
|
|
23
|
+
export const CHANNEL_INVITE_SCOPE = "channel_participation";
|
|
24
|
+
export function channelInviteState(invite, nowMs) {
|
|
25
|
+
if (invite.revokedAt !== undefined)
|
|
26
|
+
return "revoked";
|
|
27
|
+
if (invite.expiresAt !== null && invite.expiresAt <= nowMs)
|
|
28
|
+
return "expired";
|
|
29
|
+
if (invite.maxRedemptions !== null
|
|
30
|
+
&& invite.redemptions.length >= invite.maxRedemptions) {
|
|
31
|
+
return "exhausted";
|
|
32
|
+
}
|
|
33
|
+
return "active";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Decide whether one redemption attempt may proceed.
|
|
37
|
+
*
|
|
38
|
+
* Retries are first-class: an identity that already redeemed this invitation
|
|
39
|
+
* is accepted again with its original redemption, even once the invitation is
|
|
40
|
+
* exhausted. Re-running an agent's join command must never mint a second
|
|
41
|
+
* identity, burn a second slot, or replace a live session.
|
|
42
|
+
*/
|
|
43
|
+
export function evaluateChannelInviteRedemption(input) {
|
|
44
|
+
const { invite, request, nowMs } = input;
|
|
45
|
+
const actorId = request.actorId?.trim();
|
|
46
|
+
if (!actorId) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
rejection: {
|
|
50
|
+
reason: "missing_identity",
|
|
51
|
+
message: "Redemption needs the actor id of the agent session that is joining.",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (request.channelId?.trim() && request.channelId.trim() !== invite.channelId) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
rejection: {
|
|
59
|
+
reason: "channel_mismatch",
|
|
60
|
+
message: "This invitation does not belong to the requested channel.",
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (request.scope?.trim() && request.scope.trim() !== invite.scope) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
rejection: {
|
|
68
|
+
reason: "scope_mismatch",
|
|
69
|
+
message: `This invitation grants ${invite.scope} only.`,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const existing = findChannelInviteRedemption(invite, request);
|
|
74
|
+
if (existing) {
|
|
75
|
+
// Revocation still bites: a revoked invitation stops being a usable
|
|
76
|
+
// capability even for an identity that once held it.
|
|
77
|
+
if (invite.revokedAt !== undefined) {
|
|
78
|
+
return { ok: false, rejection: channelInviteRejection("revoked") };
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, existing };
|
|
81
|
+
}
|
|
82
|
+
const state = channelInviteState(invite, nowMs);
|
|
83
|
+
if (state !== "active") {
|
|
84
|
+
return { ok: false, rejection: channelInviteRejection(state) };
|
|
85
|
+
}
|
|
86
|
+
return { ok: true, existing: null };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The redemption already recorded for this identity, if any.
|
|
90
|
+
*
|
|
91
|
+
* **Session identity is decisive.** When a request carries a session id, only a
|
|
92
|
+
* redemption for that exact session counts as the same joiner. A durable agent
|
|
93
|
+
* running two concrete sessions is two distinct attachments: this feature
|
|
94
|
+
* exists to attach *the exact session* a teammate is already working in, so
|
|
95
|
+
* letting a second session inherit the first one's membership through a shared
|
|
96
|
+
* agent id would hand it the wrong context and silently skip a use of the
|
|
97
|
+
* invitation.
|
|
98
|
+
*
|
|
99
|
+
* Only when no session is carried does the durable actor or agent id decide.
|
|
100
|
+
*/
|
|
101
|
+
export function findChannelInviteRedemption(invite, request) {
|
|
102
|
+
const sessionId = request.sessionId?.trim();
|
|
103
|
+
if (sessionId) {
|
|
104
|
+
return invite.redemptions.find((redemption) => redemption.sessionId?.trim() === sessionId) ?? null;
|
|
105
|
+
}
|
|
106
|
+
const actorId = request.actorId?.trim();
|
|
107
|
+
const agentId = request.agentId?.trim();
|
|
108
|
+
return invite.redemptions.find((redemption) => {
|
|
109
|
+
if (actorId && redemption.actorId === actorId)
|
|
110
|
+
return true;
|
|
111
|
+
return Boolean(agentId) && redemption.agentId?.trim() === agentId;
|
|
112
|
+
}) ?? null;
|
|
113
|
+
}
|
|
114
|
+
function channelInviteRejection(state) {
|
|
115
|
+
switch (state) {
|
|
116
|
+
case "expired":
|
|
117
|
+
return { reason: "expired", message: "This invitation has expired. Ask for a new one." };
|
|
118
|
+
case "revoked":
|
|
119
|
+
return { reason: "revoked", message: "This invitation was revoked." };
|
|
120
|
+
case "exhausted":
|
|
121
|
+
return {
|
|
122
|
+
reason: "exhausted",
|
|
123
|
+
message: "This invitation has already been used the maximum number of times.",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export function unknownChannelInviteRejection() {
|
|
128
|
+
// Deliberately identical for a malformed, unknown, and deleted token: a
|
|
129
|
+
// probe must not learn which invitations exist.
|
|
130
|
+
return { reason: "unknown_token", message: "This invitation link is not valid." };
|
|
131
|
+
}
|
|
132
|
+
/** Read the invitations stored on a conversation record. */
|
|
133
|
+
export function readChannelInvites(metadata) {
|
|
134
|
+
const raw = metadata?.[CHANNEL_INVITES_METADATA_KEY];
|
|
135
|
+
if (!Array.isArray(raw))
|
|
136
|
+
return [];
|
|
137
|
+
return raw.filter((entry) => Boolean(entry)
|
|
138
|
+
&& typeof entry === "object"
|
|
139
|
+
&& typeof entry.id === "string"
|
|
140
|
+
&& typeof entry.tokenHash === "string"
|
|
141
|
+
&& Array.isArray(entry.redemptions));
|
|
142
|
+
}
|
|
143
|
+
/** Match a presented token against stored invitations by its digest. */
|
|
144
|
+
export function findChannelInviteByTokenHash(invites, tokenHash) {
|
|
145
|
+
const normalized = tokenHash.trim().toLowerCase();
|
|
146
|
+
if (!normalized)
|
|
147
|
+
return null;
|
|
148
|
+
return invites.find((invite) => invite.tokenHash.toLowerCase() === normalized) ?? null;
|
|
149
|
+
}
|
|
150
|
+
export function channelInvitePublicView(invite, nowMs) {
|
|
151
|
+
return {
|
|
152
|
+
id: invite.id,
|
|
153
|
+
channelId: invite.channelId,
|
|
154
|
+
createdByActorId: invite.createdByActorId,
|
|
155
|
+
scope: invite.scope,
|
|
156
|
+
state: channelInviteState(invite, nowMs),
|
|
157
|
+
tokenHint: invite.tokenHint,
|
|
158
|
+
createdAt: invite.createdAt,
|
|
159
|
+
expiresAt: invite.expiresAt,
|
|
160
|
+
maxRedemptions: invite.maxRedemptions,
|
|
161
|
+
redemptionCount: invite.redemptions.length,
|
|
162
|
+
...(invite.invitee ? { invitee: invite.invitee } : {}),
|
|
163
|
+
route: invite.route,
|
|
164
|
+
redemptions: invite.redemptions,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { AgentState, DeliveryTransport } from "./common.js";
|
|
2
|
+
/**
|
|
3
|
+
* Honest reception state for a channel member.
|
|
4
|
+
*
|
|
5
|
+
* Channel membership is a durable fact. Whether a member can actually receive
|
|
6
|
+
* a message right now is a live judgement, and the two must never be conflated
|
|
7
|
+
* — a roster that renders every member as "listening" because a row exists is
|
|
8
|
+
* the specific failure this module prevents.
|
|
9
|
+
*
|
|
10
|
+
* Two independent axes carry the truth:
|
|
11
|
+
*
|
|
12
|
+
* - {@link ChannelReceptionRouteKind} — what the attached transport *can* do.
|
|
13
|
+
* A `claude_resume` or `codex_exec` endpoint has no persistent listener at
|
|
14
|
+
* all: a delivery wakes a fresh run. That is a legitimate route, but calling
|
|
15
|
+
* it "listening" would be a lie.
|
|
16
|
+
* - {@link ChannelReceptionState} — what the route is doing at this instant,
|
|
17
|
+
* read from endpoint state and heartbeat freshness.
|
|
18
|
+
*/
|
|
19
|
+
export type ChannelReceptionState =
|
|
20
|
+
/** A member with no agent session attached yet — the invite is unredeemed. */
|
|
21
|
+
"waiting_for_agent"
|
|
22
|
+
/** An attached session that is still warming up or waking. */
|
|
23
|
+
| "connecting"
|
|
24
|
+
/** A live route that will carry a message now. */
|
|
25
|
+
| "ready_to_receive"
|
|
26
|
+
/** Attached, but no usable route exists — explain why, never imply delivery. */
|
|
27
|
+
| "unavailable"
|
|
28
|
+
/** A route that existed and has since gone quiet or been stopped. */
|
|
29
|
+
| "disconnected";
|
|
30
|
+
export type ChannelReceptionRouteKind =
|
|
31
|
+
/** The agent holds an open channel and sees a post without being started. */
|
|
32
|
+
"persistent"
|
|
33
|
+
/**
|
|
34
|
+
* Delivery reaches the invited session by resuming or steering it. Context is
|
|
35
|
+
* preserved and the session is never replaced, but nothing is watching
|
|
36
|
+
* between messages.
|
|
37
|
+
*/
|
|
38
|
+
| "wake_on_delivery"
|
|
39
|
+
/** No route at all. */
|
|
40
|
+
| "none";
|
|
41
|
+
export declare function channelReceptionRouteKind(transport: DeliveryTransport | null | undefined): ChannelReceptionRouteKind;
|
|
42
|
+
/** How long an idle persistent route may stay silent before we stop vouching for it. */
|
|
43
|
+
export declare const CHANNEL_RECEPTION_STALE_AFTER_MS: number;
|
|
44
|
+
export interface ChannelReceptionEndpointEvidence {
|
|
45
|
+
state: AgentState;
|
|
46
|
+
transport: DeliveryTransport;
|
|
47
|
+
sessionId?: string | null;
|
|
48
|
+
lastSeenAt?: number | null;
|
|
49
|
+
}
|
|
50
|
+
export interface ChannelReceptionEvidence {
|
|
51
|
+
/** The member's attached harness session, when the invite was redeemed. */
|
|
52
|
+
attachedSessionId?: string | null;
|
|
53
|
+
endpoint?: ChannelReceptionEndpointEvidence | null;
|
|
54
|
+
nowMs: number;
|
|
55
|
+
staleAfterMs?: number;
|
|
56
|
+
}
|
|
57
|
+
export interface ChannelReception {
|
|
58
|
+
state: ChannelReceptionState;
|
|
59
|
+
routeKind: ChannelReceptionRouteKind;
|
|
60
|
+
/** True only when a process is attached and watching right now. */
|
|
61
|
+
listening: boolean;
|
|
62
|
+
/** Badge-length label. */
|
|
63
|
+
summary: string;
|
|
64
|
+
/** One sentence a person can act on. Always populated. */
|
|
65
|
+
detail: string;
|
|
66
|
+
/** When the evidence behind this reading was last observed. */
|
|
67
|
+
evidenceAt: number | null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Derive a member's reception state from live evidence only.
|
|
71
|
+
*
|
|
72
|
+
* Every branch returns a `detail` sentence, including the happy path: a
|
|
73
|
+
* surface should never have to invent an explanation, and a `wake_on_delivery`
|
|
74
|
+
* route always says so out loud.
|
|
75
|
+
*/
|
|
76
|
+
export declare function deriveChannelReception(evidence: ChannelReceptionEvidence): ChannelReception;
|
|
77
|
+
export declare function channelReceptionStateLabel(state: ChannelReceptionState): string;
|
|
78
|
+
//# sourceMappingURL=channel-reception.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-reception.d.ts","sourceRoot":"","sources":["../src/channel-reception.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEjE;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,qBAAqB;AAC/B,8EAA8E;AAC5E,mBAAmB;AACrB,8DAA8D;GAC5D,YAAY;AACd,kDAAkD;GAChD,kBAAkB;AACpB,gFAAgF;GAC9E,aAAa;AACf,qEAAqE;GACnE,cAAc,CAAC;AAEnB,MAAM,MAAM,yBAAyB;AACnC,6EAA6E;AAC3E,YAAY;AACd;;;;GAIG;GACD,kBAAkB;AACpB,uBAAuB;GACrB,MAAM,CAAC;AAwBX,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,GAC9C,yBAAyB,CAG3B;AAED,wFAAwF;AACxF,eAAO,MAAM,gCAAgC,QAAiB,CAAC;AAE/D,MAAM,WAAW,gCAAgC;IAC/C,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,wBAAwB;IACvC,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,gCAAgC,GAAG,IAAI,CAAC;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,qBAAqB,CAAC;IAC7B,SAAS,EAAE,yBAAyB,CAAC;IACrC,mEAAmE;IACnE,SAAS,EAAE,OAAO,CAAC;IACnB,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,wBAAwB,GACjC,gBAAgB,CA8KlB;AAYD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,qBAAqB,GAAG,MAAM,CAQ/E"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transports where a process is actually attached and watching. Everything
|
|
3
|
+
* else either spawns per delivery or injects into a surface with no listener
|
|
4
|
+
* contract, and is reported as `wake_on_delivery`.
|
|
5
|
+
*/
|
|
6
|
+
const PERSISTENT_RECEIVE_TRANSPORTS = new Set([
|
|
7
|
+
"local_socket",
|
|
8
|
+
"websocket",
|
|
9
|
+
"pairing_bridge",
|
|
10
|
+
"peer_broker",
|
|
11
|
+
"claude_channel",
|
|
12
|
+
"claude_stream_json",
|
|
13
|
+
"codex_app_server",
|
|
14
|
+
"pi_rpc",
|
|
15
|
+
"grok_acp",
|
|
16
|
+
"kimi_acp",
|
|
17
|
+
"cursor_acp",
|
|
18
|
+
"opencode_acp",
|
|
19
|
+
"devin_acp",
|
|
20
|
+
"cursor_sdk_local",
|
|
21
|
+
]);
|
|
22
|
+
export function channelReceptionRouteKind(transport) {
|
|
23
|
+
if (!transport)
|
|
24
|
+
return "none";
|
|
25
|
+
return PERSISTENT_RECEIVE_TRANSPORTS.has(transport) ? "persistent" : "wake_on_delivery";
|
|
26
|
+
}
|
|
27
|
+
/** How long an idle persistent route may stay silent before we stop vouching for it. */
|
|
28
|
+
export const CHANNEL_RECEPTION_STALE_AFTER_MS = 10 * 60 * 1000;
|
|
29
|
+
/**
|
|
30
|
+
* Derive a member's reception state from live evidence only.
|
|
31
|
+
*
|
|
32
|
+
* Every branch returns a `detail` sentence, including the happy path: a
|
|
33
|
+
* surface should never have to invent an explanation, and a `wake_on_delivery`
|
|
34
|
+
* route always says so out loud.
|
|
35
|
+
*/
|
|
36
|
+
export function deriveChannelReception(evidence) {
|
|
37
|
+
const endpoint = evidence.endpoint ?? null;
|
|
38
|
+
const attachedSessionId = evidence.attachedSessionId?.trim() || null;
|
|
39
|
+
const routeKind = channelReceptionRouteKind(endpoint?.transport);
|
|
40
|
+
const evidenceAt = endpoint?.lastSeenAt ?? null;
|
|
41
|
+
if (!endpoint) {
|
|
42
|
+
return attachedSessionId
|
|
43
|
+
? {
|
|
44
|
+
state: "unavailable",
|
|
45
|
+
routeKind: "none",
|
|
46
|
+
listening: false,
|
|
47
|
+
summary: "No route",
|
|
48
|
+
detail: `Session ${attachedSessionId} is attached to this channel but the broker has no registered endpoint for it, so messages cannot be delivered.`,
|
|
49
|
+
evidenceAt,
|
|
50
|
+
}
|
|
51
|
+
: {
|
|
52
|
+
state: "waiting_for_agent",
|
|
53
|
+
routeKind: "none",
|
|
54
|
+
listening: false,
|
|
55
|
+
summary: "Waiting for agent",
|
|
56
|
+
detail: "This member has joined the channel but no agent session has redeemed the invitation yet.",
|
|
57
|
+
evidenceAt,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const base = { routeKind, evidenceAt };
|
|
61
|
+
// An endpoint is only evidence about *this* member when the channel knows
|
|
62
|
+
// which session it attached. Without that link there is a route on the
|
|
63
|
+
// machine but nothing tying it to this membership, so it cannot be read as
|
|
64
|
+
// this member being reachable.
|
|
65
|
+
if (!attachedSessionId) {
|
|
66
|
+
return {
|
|
67
|
+
...base,
|
|
68
|
+
state: "waiting_for_agent",
|
|
69
|
+
listening: false,
|
|
70
|
+
summary: "Waiting for agent",
|
|
71
|
+
detail: `This member has not redeemed an invitation from a session yet. A ${endpoint.transport} route exists elsewhere for them, but nothing attaches it to this channel, so a message here has no session to arrive in.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// The invitation attached one concrete session. A live endpoint belonging to
|
|
75
|
+
// a different session is somebody else's evidence: reporting it as this
|
|
76
|
+
// member's readiness is exactly the false listening claim to avoid.
|
|
77
|
+
const endpointSessionId = endpoint.sessionId?.trim() || null;
|
|
78
|
+
if (endpointSessionId && endpointSessionId !== attachedSessionId) {
|
|
79
|
+
return {
|
|
80
|
+
...base,
|
|
81
|
+
state: "disconnected",
|
|
82
|
+
listening: false,
|
|
83
|
+
summary: "Session replaced",
|
|
84
|
+
detail: `This channel attached session ${attachedSessionId}, but the live ${endpoint.transport} route now belongs to ${endpointSessionId}. Re-redeem the invitation from the session you want in the room.`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
switch (endpoint.state) {
|
|
88
|
+
case "attaching":
|
|
89
|
+
case "waking":
|
|
90
|
+
return {
|
|
91
|
+
...base,
|
|
92
|
+
state: "connecting",
|
|
93
|
+
listening: false,
|
|
94
|
+
summary: "Connecting",
|
|
95
|
+
detail: `The ${endpoint.transport} route is ${endpoint.state}; messages sent now will be delivered once it settles.`,
|
|
96
|
+
};
|
|
97
|
+
case "registered":
|
|
98
|
+
// `registered` means the broker knows how to reach this endpoint, not
|
|
99
|
+
// that a session is attached and watching. Only the live states below
|
|
100
|
+
// are evidence of an active receive route.
|
|
101
|
+
return {
|
|
102
|
+
...base,
|
|
103
|
+
state: "connecting",
|
|
104
|
+
listening: false,
|
|
105
|
+
summary: "Registered",
|
|
106
|
+
detail: `Session ${attachedSessionId} is registered over ${endpoint.transport} but has not been observed attached and watching, so it is not yet an active receive route.`,
|
|
107
|
+
};
|
|
108
|
+
case "offline":
|
|
109
|
+
case "stopped":
|
|
110
|
+
return {
|
|
111
|
+
...base,
|
|
112
|
+
state: "disconnected",
|
|
113
|
+
listening: false,
|
|
114
|
+
summary: "Disconnected",
|
|
115
|
+
detail: `The attached session's ${endpoint.transport} route is ${endpoint.state}. Restart or re-attach the session to receive messages.`,
|
|
116
|
+
};
|
|
117
|
+
case "superseded":
|
|
118
|
+
return {
|
|
119
|
+
...base,
|
|
120
|
+
state: "disconnected",
|
|
121
|
+
listening: false,
|
|
122
|
+
summary: "Superseded",
|
|
123
|
+
detail: "A newer route replaced this one. The member's membership stands, but this endpoint no longer receives.",
|
|
124
|
+
};
|
|
125
|
+
case "unreachable":
|
|
126
|
+
case "failed":
|
|
127
|
+
return {
|
|
128
|
+
...base,
|
|
129
|
+
state: "unavailable",
|
|
130
|
+
listening: false,
|
|
131
|
+
summary: endpoint.state === "failed" ? "Failed" : "Unreachable",
|
|
132
|
+
detail: `The broker last saw this ${endpoint.transport} route as ${endpoint.state}; it cannot currently accept a delivery.`,
|
|
133
|
+
};
|
|
134
|
+
default:
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
// Past this point every branch makes a positive claim about the attached
|
|
138
|
+
// session. An endpoint that does not say which session it serves cannot
|
|
139
|
+
// support one: the route may well be fine, but "session X is receiving" is a
|
|
140
|
+
// claim about a specific session and this is not evidence for it. The
|
|
141
|
+
// negative states above are unaffected -- a dead route is dead either way.
|
|
142
|
+
if (!endpointSessionId) {
|
|
143
|
+
return {
|
|
144
|
+
...base,
|
|
145
|
+
state: "connecting",
|
|
146
|
+
listening: false,
|
|
147
|
+
summary: "Unconfirmed",
|
|
148
|
+
detail: `A ${endpoint.transport} route is registered for this member, but it does not identify which session it serves, so Scout cannot confirm that session ${attachedSessionId} is the one receiving.`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (routeKind === "wake_on_delivery") {
|
|
152
|
+
return {
|
|
153
|
+
...base,
|
|
154
|
+
state: "ready_to_receive",
|
|
155
|
+
listening: false,
|
|
156
|
+
summary: "Wakes on delivery",
|
|
157
|
+
// Say what actually happens. These transports resume or steer the exact
|
|
158
|
+
// invited session, so context survives -- claiming a message "starts a
|
|
159
|
+
// new run" would misdescribe them. What they lack is a listener between
|
|
160
|
+
// messages, and that is the only claim being withheld here.
|
|
161
|
+
detail: `Messages reach this member by ${endpoint.transport === "tmux" ? "steering" : "resuming"} session ${attachedSessionId} over ${endpoint.transport}. That session keeps its context and is never replaced, but nothing is watching the channel between messages.`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const staleAfterMs = evidence.staleAfterMs ?? CHANNEL_RECEPTION_STALE_AFTER_MS;
|
|
165
|
+
const lastSeenAt = endpoint.lastSeenAt ?? null;
|
|
166
|
+
// Registration is not observation. A persistent route claims a process is
|
|
167
|
+
// watching right now; with no timestamp the broker has never seen it do so,
|
|
168
|
+
// and an unobserved row is not grounds for telling a person their agent is
|
|
169
|
+
// listening. A wake-on-delivery route makes no such claim, so its
|
|
170
|
+
// registration alone remains sufficient.
|
|
171
|
+
if (lastSeenAt === null) {
|
|
172
|
+
return {
|
|
173
|
+
...base,
|
|
174
|
+
state: "connecting",
|
|
175
|
+
listening: false,
|
|
176
|
+
summary: "Unconfirmed",
|
|
177
|
+
detail: `The ${endpoint.transport} route is registered for this session but has never been observed live, so Scout cannot yet confirm it is listening.`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (evidence.nowMs - lastSeenAt > staleAfterMs) {
|
|
181
|
+
return {
|
|
182
|
+
...base,
|
|
183
|
+
state: "disconnected",
|
|
184
|
+
listening: false,
|
|
185
|
+
summary: "Silent",
|
|
186
|
+
detail: `The ${endpoint.transport} route is registered but has not been seen for ${formatDuration(evidence.nowMs - lastSeenAt)}, so it is no longer treated as listening.`,
|
|
187
|
+
evidenceAt: lastSeenAt,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
...base,
|
|
192
|
+
state: "ready_to_receive",
|
|
193
|
+
listening: true,
|
|
194
|
+
summary: "Ready to receive",
|
|
195
|
+
detail: `An attached ${endpoint.transport} session is watching this channel and will see new messages without being started.`,
|
|
196
|
+
evidenceAt: lastSeenAt,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function formatDuration(ms) {
|
|
200
|
+
const minutes = Math.floor(ms / 60_000);
|
|
201
|
+
if (minutes < 1)
|
|
202
|
+
return "under a minute";
|
|
203
|
+
if (minutes < 60)
|
|
204
|
+
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
205
|
+
const hours = Math.floor(minutes / 60);
|
|
206
|
+
if (hours < 24)
|
|
207
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
208
|
+
const days = Math.floor(hours / 24);
|
|
209
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
210
|
+
}
|
|
211
|
+
export function channelReceptionStateLabel(state) {
|
|
212
|
+
switch (state) {
|
|
213
|
+
case "waiting_for_agent": return "Waiting for agent";
|
|
214
|
+
case "connecting": return "Connecting";
|
|
215
|
+
case "ready_to_receive": return "Ready to receive";
|
|
216
|
+
case "unavailable": return "Unavailable";
|
|
217
|
+
case "disconnected": return "Disconnected";
|
|
218
|
+
}
|
|
219
|
+
}
|
package/dist/common.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type DeliveryStatus = "accepted" | "peer_acked" | "running" | "completed"
|
|
|
6
6
|
export type DeliveryFailureReason = "peer_unreachable" | "peer_rejected" | "agent_offline" | "timeout" | "harness_error" | "dispatch" | "cancelled";
|
|
7
7
|
export type DeliveryPolicy = "best_effort" | "must_ack" | "durable" | "ephemeral";
|
|
8
8
|
export type DeliveryTargetKind = "participant" | "agent" | "bridge" | "device" | "voice_session" | "webhook";
|
|
9
|
-
export type DeliveryTransport = "local_socket" | "websocket" | "pairing_bridge" | "peer_broker" | "http" | "webhook" | "telegram" | "discord" | "sms" | "email" | "tts" | "native_voice" | "claude_channel" | "claude_stream_json" | "codex_app_server" | "codex_exec" | "claude_resume" | "pi_rpc" | "grok_acp" | "kimi_acp" | "cursor_acp" | "opencode_acp" | "tmux" | "cursor_exec" | "cursor_cli_text" | "cursor_cli_stream_json" | "cursor_sdk_local";
|
|
9
|
+
export type DeliveryTransport = "local_socket" | "websocket" | "pairing_bridge" | "peer_broker" | "http" | "webhook" | "telegram" | "discord" | "sms" | "email" | "tts" | "native_voice" | "claude_channel" | "claude_stream_json" | "codex_app_server" | "codex_exec" | "claude_resume" | "pi_rpc" | "grok_acp" | "kimi_acp" | "cursor_acp" | "opencode_acp" | "devin_acp" | "tmux" | "cursor_exec" | "cursor_cli_text" | "cursor_cli_stream_json" | "cursor_sdk_local";
|
|
10
10
|
export type DeliveryReason = "conversation_visibility" | "direct_message" | "mention" | "thread_reply" | "invocation" | "bridge_outbound" | "speech";
|
|
11
11
|
export type AdvertiseScope = "local" | "mesh";
|
|
12
12
|
export type ShareMode = "local" | "summary" | "shared";
|
package/dist/common.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAE7B,MAAM,MAAM,SAAS,GACjB,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,QAAQ,CAAC;AAEb,MAAM,MAAM,UAAU,GAClB,YAAY,GACZ,WAAW,GACX,QAAQ,GACR,SAAS,GACT,MAAM,GACN,QAAQ,GACR,SAAS,GACT,SAAS,GACT,aAAa,GACb,QAAQ,GACR,YAAY,GACZ,SAAS,CAAC;AAEd,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE5E,MAAM,MAAM,cAAc,GAEtB,UAAU,GACV,YAAY,GACZ,SAAS,GACT,WAAW,GACX,UAAU,GACV,QAAQ,GACR,WAAW,GAEX,SAAS,GACT,QAAQ,GACR,MAAM,GACN,cAAc,CAAC;AAEnB,MAAM,MAAM,qBAAqB,GAC7B,kBAAkB,GAClB,eAAe,GACf,eAAe,GACf,SAAS,GACT,eAAe,GACf,UAAU,GACV,WAAW,CAAC;AAEhB,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,UAAU,GAAG,SAAS,GAAG,WAAW,CAAC;AAElF,MAAM,MAAM,kBAAkB,GAC1B,aAAa,GACb,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,eAAe,GACf,SAAS,CAAC;AAEd,MAAM,MAAM,iBAAiB,GACzB,cAAc,GACd,WAAW,GACX,gBAAgB,GAChB,aAAa,GACb,MAAM,GACN,SAAS,GACT,UAAU,GACV,SAAS,GACT,KAAK,GACL,OAAO,GACP,KAAK,GACL,cAAc,GACd,gBAAgB,GAChB,oBAAoB,GACpB,kBAAkB,GAClB,YAAY,GACZ,eAAe,GACf,QAAQ,GACR,UAAU,GACV,UAAU,GACV,YAAY,GACZ,cAAc,GACd,MAAM,GACN,aAAa,GACb,iBAAiB,GACjB,wBAAwB,GACxB,kBAAkB,CAAC;AAEvB,MAAM,MAAM,cAAc,GACtB,yBAAyB,GACzB,gBAAgB,GAChB,SAAS,GACT,cAAc,GACd,YAAY,GACZ,iBAAiB,GACjB,QAAQ,CAAC;AAEb,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,MAAM,CAAC;AAE9C,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB"}
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAE7B,MAAM,MAAM,SAAS,GACjB,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,QAAQ,CAAC;AAEb,MAAM,MAAM,UAAU,GAClB,YAAY,GACZ,WAAW,GACX,QAAQ,GACR,SAAS,GACT,MAAM,GACN,QAAQ,GACR,SAAS,GACT,SAAS,GACT,aAAa,GACb,QAAQ,GACR,YAAY,GACZ,SAAS,CAAC;AAEd,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE5E,MAAM,MAAM,cAAc,GAEtB,UAAU,GACV,YAAY,GACZ,SAAS,GACT,WAAW,GACX,UAAU,GACV,QAAQ,GACR,WAAW,GAEX,SAAS,GACT,QAAQ,GACR,MAAM,GACN,cAAc,CAAC;AAEnB,MAAM,MAAM,qBAAqB,GAC7B,kBAAkB,GAClB,eAAe,GACf,eAAe,GACf,SAAS,GACT,eAAe,GACf,UAAU,GACV,WAAW,CAAC;AAEhB,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,UAAU,GAAG,SAAS,GAAG,WAAW,CAAC;AAElF,MAAM,MAAM,kBAAkB,GAC1B,aAAa,GACb,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,eAAe,GACf,SAAS,CAAC;AAEd,MAAM,MAAM,iBAAiB,GACzB,cAAc,GACd,WAAW,GACX,gBAAgB,GAChB,aAAa,GACb,MAAM,GACN,SAAS,GACT,UAAU,GACV,SAAS,GACT,KAAK,GACL,OAAO,GACP,KAAK,GACL,cAAc,GACd,gBAAgB,GAChB,oBAAoB,GACpB,kBAAkB,GAClB,YAAY,GACZ,eAAe,GACf,QAAQ,GACR,UAAU,GACV,UAAU,GACV,YAAY,GACZ,cAAc,GACd,WAAW,GACX,MAAM,GACN,aAAa,GACb,iBAAiB,GACjB,wBAAwB,GACxB,kBAAkB,CAAC;AAEvB,MAAM,MAAM,cAAc,GACtB,yBAAyB,GACzB,gBAAgB,GAChB,SAAS,GACT,cAAc,GACd,YAAY,GACZ,iBAAiB,GACjB,QAAQ,CAAC;AAEb,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,MAAM,CAAC;AAE9C,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ConversationKind } from "./conversations.js";
|
|
2
2
|
import type { ObservedActivity } from "./observed-status.js";
|
|
3
|
-
export declare const CONVERSATION_PROJECTION_VERSION =
|
|
3
|
+
export declare const CONVERSATION_PROJECTION_VERSION = 2;
|
|
4
4
|
export type ConversationProjectionEntityKind = "scout_conversation" | "observed_session";
|
|
5
5
|
export type ConversationProjectionVisibility = "visible" | "hidden";
|
|
6
6
|
export type ConversationProjectionItem = {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Herdr workspace digest — the presentation layer over {@link HerdrSessionTopology}.
|
|
3
|
+
*
|
|
4
|
+
* The raw projection is faithful but shapeless: a complex session is several
|
|
5
|
+
* workspaces × tabs × panes, and handing that whole tree to an operator (or to
|
|
6
|
+
* an assistant that has to answer for it) buries the one thing they came for.
|
|
7
|
+
* A digest ranks, groups, bounds, and describes it.
|
|
8
|
+
*
|
|
9
|
+
* Four commitments, in order of how much they matter:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Attention leads.** A `blocked` pane is the only thing in a herdr session
|
|
12
|
+
* that is actually waiting on a person. It comes first, always, before any
|
|
13
|
+
* inventory. Everything else is context for it.
|
|
14
|
+
* 2. **Directory is the grouping axis, not the tab.** Operators think "which
|
|
15
|
+
* repo", herdr thinks "which tab". Grouping is by the pane's literal working
|
|
16
|
+
* directory — no repo-root inference, because a guessed grouping presented as
|
|
17
|
+
* a fact is worse than an honest fragmented one.
|
|
18
|
+
* 3. **Shape is described, not dumped.** A tab's layout becomes one readable
|
|
19
|
+
* phrase ("2×2 grid", "3 columns (2 · 1 · 1)") derived from the pane rects.
|
|
20
|
+
* 4. **Bounded and honest.** Panes are capped and truncation is declared;
|
|
21
|
+
* `unknown` never collapses into `done`; a persisted projection is labeled
|
|
22
|
+
* last-known rather than passed off as live.
|
|
23
|
+
*
|
|
24
|
+
* There are no mutation verbs here, for the same reason there are none in the
|
|
25
|
+
* topology projection: herdr owns workspaces, tabs and panes, and Scout's layer
|
|
26
|
+
* is coordination over whatever host is present. A digest pane carries the
|
|
27
|
+
* herdr `target` string so a caller can construct a handoff; performing it is
|
|
28
|
+
* the herdr client's job.
|
|
29
|
+
*/
|
|
30
|
+
import type { HerdrAgentStatus, HerdrSessionTopology, HerdrTabLayout, HerdrTopologyUnavailableReason } from "./terminal-host-topology.js";
|
|
31
|
+
/** Total emitted panes across every group before the digest declares truncation. */
|
|
32
|
+
export declare const HERDR_DIGEST_PANE_CAP = 40;
|
|
33
|
+
/** Ceiling on each of the two lead lists; attention is a shortlist, not an inventory. */
|
|
34
|
+
export declare const HERDR_DIGEST_LEAD_CAP = 12;
|
|
35
|
+
export type HerdrStatusCounts = Record<HerdrAgentStatus, number>;
|
|
36
|
+
export type HerdrDigestPane = {
|
|
37
|
+
/**
|
|
38
|
+
* What `herdr agent <verb>` accepts as a target for this pane: the stable
|
|
39
|
+
* terminal id when herdr reported one, else the pane id. Null on a persisted
|
|
40
|
+
* projection, where neither is live — a pane you cannot address yet.
|
|
41
|
+
*/
|
|
42
|
+
target: string | null;
|
|
43
|
+
paneId: string;
|
|
44
|
+
workspaceId: string;
|
|
45
|
+
tabId: string;
|
|
46
|
+
label: string | null;
|
|
47
|
+
/** Detected agent label ("claude", "codex"), or null for a plain shell. */
|
|
48
|
+
agent: string | null;
|
|
49
|
+
status: HerdrAgentStatus;
|
|
50
|
+
/** Working directory: the foreground process's when known, else the pane's. */
|
|
51
|
+
directory: string | null;
|
|
52
|
+
focused: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Scrollback backlog depth in lines. The cheapest activity signal a
|
|
55
|
+
* projection carries — a deep backlog on an idle pane is work that already
|
|
56
|
+
* happened, not work happening. Null when herdr did not report scroll state.
|
|
57
|
+
*/
|
|
58
|
+
backlog: number | null;
|
|
59
|
+
};
|
|
60
|
+
export type HerdrDigestGroup = {
|
|
61
|
+
/** The panes' literal shared working directory; null when they report none. */
|
|
62
|
+
directory: string | null;
|
|
63
|
+
/** Last path segment — how an operator names the place. */
|
|
64
|
+
name: string;
|
|
65
|
+
counts: HerdrStatusCounts;
|
|
66
|
+
panes: HerdrDigestPane[];
|
|
67
|
+
/** Panes in this group dropped by the pane cap. */
|
|
68
|
+
omitted: number;
|
|
69
|
+
/**
|
|
70
|
+
* What was dropped, by status. A collapsed label has to carry its own scent:
|
|
71
|
+
* a reader decides whether the hidden rows matter from the label alone, so
|
|
72
|
+
* "2 idle not shown" is a usable label and "2 not shown" is not.
|
|
73
|
+
*/
|
|
74
|
+
omittedCounts: HerdrStatusCounts;
|
|
75
|
+
};
|
|
76
|
+
export type HerdrDigestShape = {
|
|
77
|
+
workspaceId: string;
|
|
78
|
+
workspaceLabel: string;
|
|
79
|
+
tabId: string;
|
|
80
|
+
tabLabel: string;
|
|
81
|
+
paneCount: number;
|
|
82
|
+
/** One readable phrase for the arrangement, e.g. "2×2 grid". */
|
|
83
|
+
arrangement: string;
|
|
84
|
+
zoomed: boolean;
|
|
85
|
+
focused: boolean;
|
|
86
|
+
};
|
|
87
|
+
export type HerdrWorkspaceDigest = {
|
|
88
|
+
session: string;
|
|
89
|
+
/** True only for a projection read from a running herdr server. */
|
|
90
|
+
live: boolean;
|
|
91
|
+
observedAt: number;
|
|
92
|
+
/** Set when the projection is the persisted last-known state, not a live read. */
|
|
93
|
+
savedAt: number | null;
|
|
94
|
+
/**
|
|
95
|
+
* Why a non-live projection is not live. `unreadable` means a server for the
|
|
96
|
+
* session IS running and did not answer — the digest must not present that as
|
|
97
|
+
* a stopped session. Null on a live read.
|
|
98
|
+
*/
|
|
99
|
+
unavailable: HerdrTopologyUnavailableReason | null;
|
|
100
|
+
totals: {
|
|
101
|
+
workspaces: number;
|
|
102
|
+
tabs: number;
|
|
103
|
+
panes: number;
|
|
104
|
+
/** Panes herdr recognized an agent in. The rest are plain shells. */
|
|
105
|
+
agents: number;
|
|
106
|
+
};
|
|
107
|
+
counts: HerdrStatusCounts;
|
|
108
|
+
/** Blocked panes — the only entries that represent something waiting on a person. */
|
|
109
|
+
needsYou: HerdrDigestPane[];
|
|
110
|
+
/** Panes herdr observed as working, bounded. */
|
|
111
|
+
working: HerdrDigestPane[];
|
|
112
|
+
groups: HerdrDigestGroup[];
|
|
113
|
+
shapes: HerdrDigestShape[];
|
|
114
|
+
truncated: boolean;
|
|
115
|
+
/** Caveats the reader must carry with the numbers. Never decorative. */
|
|
116
|
+
notes: string[];
|
|
117
|
+
};
|
|
118
|
+
export declare function herdrStatusRank(status: HerdrAgentStatus): number;
|
|
119
|
+
/**
|
|
120
|
+
* Describe a tab's arrangement from its pane rects.
|
|
121
|
+
*
|
|
122
|
+
* Panes sharing an x start a column; panes sharing a y start a row. A layout
|
|
123
|
+
* whose columns all hold the same number of panes is a grid and is named as
|
|
124
|
+
* one; anything else is named by its columns and their depths, which is both
|
|
125
|
+
* shorter and more truthful than inventing a name for an irregular split.
|
|
126
|
+
*/
|
|
127
|
+
export declare function describeHerdrArrangement(layout: HerdrTabLayout | null, paneCount: number): string;
|
|
128
|
+
/**
|
|
129
|
+
* Build the digest for one herdr session topology.
|
|
130
|
+
*
|
|
131
|
+
* Pure: the same topology always digests to the same value, so the whole
|
|
132
|
+
* presentation is testable without a herdr server.
|
|
133
|
+
*/
|
|
134
|
+
export declare function digestHerdrTopology(topology: HerdrSessionTopology, options?: {
|
|
135
|
+
paneCap?: number;
|
|
136
|
+
leadCap?: number;
|
|
137
|
+
}): HerdrWorkspaceDigest;
|
|
138
|
+
/**
|
|
139
|
+
* Render a digest as the text an operator should actually read.
|
|
140
|
+
*
|
|
141
|
+
* Ordering is the whole design: what is waiting, then what is moving, then
|
|
142
|
+
* where things live, then how the screen is arranged. A reader who stops after
|
|
143
|
+
* the first section has still got the answer that mattered.
|
|
144
|
+
*
|
|
145
|
+
* Section heads are verb phrases rather than bare nouns, and there is no
|
|
146
|
+
* trailing caveat block: a qualifier that changes how a count should be read
|
|
147
|
+
* sits directly under that count, because a scanning reader reaches the top of
|
|
148
|
+
* the output and not the bottom of it.
|
|
149
|
+
*/
|
|
150
|
+
export declare function renderHerdrWorkspaceDigest(digest: HerdrWorkspaceDigest): string;
|
|
151
|
+
//# sourceMappingURL=herdr-workspace-digest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"herdr-workspace-digest.d.ts","sourceRoot":"","sources":["../src/herdr-workspace-digest.ts"],"names":[],"mappings":"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"}
|