harnery 0.45.2 → 0.46.0
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/commander.d.ts +13 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commands/admission.d.ts.map +1 -1
- package/dist/commands/admission.js +214 -27
- package/dist/commands/agents.d.ts +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +102 -25
- package/dist/commands/browse.d.ts +3 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +172 -8
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +8 -2
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +40 -0
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +4 -3
- package/dist/commands/review-pack.d.ts.map +1 -1
- package/dist/commands/review-pack.js +33 -6
- package/dist/core/agents/index.d.ts +1 -0
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -4
- package/dist/core/agents/render/session-context.d.ts +0 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +1 -6
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -4
- package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +6 -2
- package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
- package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference-contract.js +1 -0
- package/dist/core/agents/state/parent-reference.d.ts +18 -0
- package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference.js +119 -0
- package/dist/core/config.d.ts +39 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +63 -0
- package/dist/core/diagnostics/path-identity.d.ts +1 -1
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
- package/dist/core/diagnostics/path-identity.js +51 -6
- package/dist/core/events/v3/canonical.js +43 -14
- package/dist/core/events/v3/index.d.ts +1 -1
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +1 -1
- package/dist/core/events/v3/producers/recorder.d.ts +34 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +117 -13
- package/dist/core/events/v3/reader.d.ts.map +1 -1
- package/dist/core/events/v3/reader.js +53 -13
- package/dist/core/events/v3/validate.d.ts.map +1 -1
- package/dist/core/events/v3/validate.js +36 -1
- package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
- package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
- package/dist/core/hooks/adapter/native-parent.js +91 -0
- package/dist/core/hooks/cli.js +79 -4
- package/dist/core/hooks/resolve/owner.d.ts +0 -11
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +0 -23
- package/dist/core/hooks/shell-admission.d.ts +84 -0
- package/dist/core/hooks/shell-admission.d.ts.map +1 -0
- package/dist/core/hooks/shell-admission.js +622 -0
- package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
- package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
- package/dist/core/hooks/shell-commit-tree.js +104 -0
- package/dist/core/inbox/contract.d.ts +12 -0
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/contract.js +16 -0
- package/dist/core/inbox/peer-messages.d.ts +41 -16
- package/dist/core/inbox/peer-messages.d.ts.map +1 -1
- package/dist/core/inbox/peer-messages.js +372 -15
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +83 -3
- package/dist/core/inbox/targets.d.ts +17 -3
- package/dist/core/inbox/targets.d.ts.map +1 -1
- package/dist/core/inbox/targets.js +77 -2
- package/dist/core/presence/git.d.ts +18 -12
- package/dist/core/presence/git.d.ts.map +1 -1
- package/dist/core/presence/git.js +32 -38
- package/dist/core/presence/index.d.ts +2 -2
- package/dist/core/presence/index.d.ts.map +1 -1
- package/dist/core/presence/index.js +8 -6
- package/dist/core/presence/mail.d.ts +133 -0
- package/dist/core/presence/mail.d.ts.map +1 -0
- package/dist/core/presence/mail.js +464 -0
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/storage/modes.d.ts +4 -0
- package/dist/core/storage/modes.d.ts.map +1 -1
- package/dist/core/storage/modes.js +4 -0
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +5 -4
- package/dist/lib/admission-history.d.ts +96 -0
- package/dist/lib/admission-history.d.ts.map +1 -0
- package/dist/lib/admission-history.js +196 -0
- package/dist/lib/admission.d.ts +56 -7
- package/dist/lib/admission.d.ts.map +1 -1
- package/dist/lib/admission.js +161 -19
- package/dist/lib/browser/client.d.ts +18 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +100 -12
- package/dist/lib/browser/content-checks.d.ts +3 -1
- package/dist/lib/browser/content-checks.d.ts.map +1 -1
- package/dist/lib/browser/content-checks.js +10 -2
- package/dist/lib/browser/critique.d.ts +32 -0
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +72 -3
- package/dist/lib/browser/geometry.d.ts +2 -0
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +61 -15
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -1
- package/dist/lib/browser/page-review-contracts.d.ts +6 -1
- package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
- package/dist/lib/browser/page-review-contracts.js +6 -1
- package/dist/lib/browser/page-review-judge.d.ts +3 -1
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +41 -11
- package/dist/lib/browser/page-review-pack.d.ts +3 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
- package/dist/lib/browser/page-review-pack.js +5 -2
- package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +11 -4
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +73 -16
- package/dist/lib/browser/runts.d.ts +4 -2
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +84 -33
- package/dist/lib/browser/windows-chrome.d.ts +14 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +42 -0
- package/dist/lib/browser/windows-login-sync.d.ts +110 -0
- package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
- package/dist/lib/browser/windows-login-sync.js +171 -0
- package/dist/lib/cookies/client.d.ts +30 -5
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +55 -10
- package/dist/lib/cookies/index.d.ts +1 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +1 -1
- package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
- package/dist/lib/docs-metadata-sync.js +41 -2
- package/dist/lib/durable-job.d.ts +8 -0
- package/dist/lib/durable-job.d.ts.map +1 -1
- package/dist/lib/durable-job.js +10 -0
- package/dist/lib/http/client.d.ts +9 -8
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +44 -2
- package/dist/lib/instructions/templates.js +4 -4
- package/package.json +2 -2
- package/src/commander.ts +13 -0
- package/src/commands/admission.ts +272 -29
- package/src/commands/agents.ts +132 -27
- package/src/commands/browse.ts +208 -6
- package/src/commands/fetch.ts +1 -0
- package/src/commands/ledger-v3.ts +8 -2
- package/src/commands/presence.ts +49 -0
- package/src/commands/qa-run.ts +4 -3
- package/src/commands/review-pack.ts +41 -5
- package/src/core/agents/index.ts +6 -0
- package/src/core/agents/live-authority-v3.ts +11 -4
- package/src/core/agents/render/session-context.ts +1 -7
- package/src/core/agents/session-events.ts +18 -3
- package/src/core/agents/state/heartbeat-reader.ts +3 -0
- package/src/core/agents/state/live-coordination-view.ts +13 -6
- package/src/core/agents/state/parent-reference-contract.ts +14 -0
- package/src/core/agents/state/parent-reference.ts +179 -0
- package/src/core/config.ts +85 -0
- package/src/core/diagnostics/path-identity.ts +52 -6
- package/src/core/events/v3/canonical.ts +46 -13
- package/src/core/events/v3/index.ts +3 -0
- package/src/core/events/v3/producers/recorder.ts +151 -13
- package/src/core/events/v3/reader.ts +55 -11
- package/src/core/events/v3/validate.ts +42 -1
- package/src/core/hooks/adapter/native-parent.ts +109 -0
- package/src/core/hooks/cli.ts +83 -4
- package/src/core/hooks/resolve/owner.ts +0 -23
- package/src/core/hooks/shell-admission.ts +634 -0
- package/src/core/hooks/shell-commit-tree.ts +102 -0
- package/src/core/inbox/contract.ts +18 -0
- package/src/core/inbox/peer-messages.ts +501 -15
- package/src/core/inbox/service.ts +108 -3
- package/src/core/inbox/targets.ts +94 -3
- package/src/core/presence/git.ts +45 -37
- package/src/core/presence/index.ts +16 -8
- package/src/core/presence/mail.ts +620 -0
- package/src/core/storage/modes.ts +6 -0
- package/src/core/storage/segments.ts +5 -4
- package/src/lib/admission-history.ts +267 -0
- package/src/lib/admission.ts +217 -19
- package/src/lib/browser/client.ts +147 -35
- package/src/lib/browser/content-checks.ts +14 -3
- package/src/lib/browser/critique.ts +113 -3
- package/src/lib/browser/geometry.ts +63 -14
- package/src/lib/browser/index.ts +5 -0
- package/src/lib/browser/page-review-contracts.ts +6 -1
- package/src/lib/browser/page-review-judge.ts +58 -13
- package/src/lib/browser/page-review-pack.ts +13 -2
- package/src/lib/browser/qa-run-contracts.ts +30 -4
- package/src/lib/browser/qa-run.ts +78 -17
- package/src/lib/browser/runts.ts +85 -37
- package/src/lib/browser/windows-chrome.ts +50 -0
- package/src/lib/browser/windows-login-sync.ts +275 -0
- package/src/lib/cookies/client.ts +62 -10
- package/src/lib/cookies/index.ts +3 -0
- package/src/lib/docs-metadata-sync.ts +44 -2
- package/src/lib/durable-job.ts +15 -0
- package/src/lib/http/client.ts +63 -1
- package/src/lib/instructions/templates.ts +4 -4
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { basename, shellCommandFromTool, tokenize } from "./shell-tokens.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deny `git commit-tree` in an agent's shell command. In a checkout shared by
|
|
5
|
+
* several agents, a peer's commit can hold the Git index lock for minutes
|
|
6
|
+
* while its pre-commit checks run. An agent that builds its own commit from a
|
|
7
|
+
* separate index with `git commit-tree` and pushes it gets around that lock,
|
|
8
|
+
* and with it every pre-commit check and the commit-time claim guard. The
|
|
9
|
+
* right move is to wait for the peer's commit to finish.
|
|
10
|
+
*
|
|
11
|
+
* A guardrail against the reflex, not a security boundary: it reads command
|
|
12
|
+
* positions only (so quoted examples and other tools pass), skips Git's
|
|
13
|
+
* global options, and looks inside `bash -c '...'`.
|
|
14
|
+
* HARNERY_ALLOW_COMMIT_TREE=1 on the command is the deliberate override.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const OVERRIDE = "HARNERY_ALLOW_COMMIT_TREE=1";
|
|
18
|
+
const SEPARATORS = new Set([";", "|", "||", "&", "&&", "(", ")", "{", "}"]);
|
|
19
|
+
const KEYWORDS = new Set(["do", "if", "then", "elif", "else", "!", "while", "until", "time"]);
|
|
20
|
+
const WRAPPERS = new Set(["command", "exec", "builtin", "nohup", "sudo", "doas", "env", "nice"]);
|
|
21
|
+
// Git global options that consume the following token as their value.
|
|
22
|
+
const GIT_VALUE_OPTIONS = new Set([
|
|
23
|
+
"-C",
|
|
24
|
+
"-c",
|
|
25
|
+
"--git-dir",
|
|
26
|
+
"--work-tree",
|
|
27
|
+
"--namespace",
|
|
28
|
+
"--exec-path",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function gitSubcommand(args: { value: string; operator?: boolean }[]): string | null {
|
|
32
|
+
for (let j = 0; j < args.length; j++) {
|
|
33
|
+
const arg = args[j]!;
|
|
34
|
+
if (arg.operator) return null;
|
|
35
|
+
if (arg.value.startsWith("-")) {
|
|
36
|
+
if (GIT_VALUE_OPTIONS.has(arg.value)) j++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
return arg.value;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function invokesCommitTree(source: string, depth = 0): boolean {
|
|
45
|
+
if (depth > 8) return false;
|
|
46
|
+
const tokens = tokenize(source);
|
|
47
|
+
if (tokens.some((t) => !t.quoted && t.value === OVERRIDE)) return false;
|
|
48
|
+
let start = true;
|
|
49
|
+
let afterWrapper = false;
|
|
50
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
51
|
+
const token = tokens[i]!;
|
|
52
|
+
if (token.operator) {
|
|
53
|
+
if (SEPARATORS.has(token.value)) {
|
|
54
|
+
start = true;
|
|
55
|
+
afterWrapper = false;
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!start) continue;
|
|
60
|
+
if (!token.quoted) {
|
|
61
|
+
if (KEYWORDS.has(token.value)) continue;
|
|
62
|
+
if (/^[A-Za-z_][A-Za-z_0-9]*=/.test(token.value)) continue;
|
|
63
|
+
if (afterWrapper && token.value.startsWith("-")) continue;
|
|
64
|
+
}
|
|
65
|
+
const name = basename(token.value);
|
|
66
|
+
if (WRAPPERS.has(name)) {
|
|
67
|
+
afterWrapper = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
start = false;
|
|
71
|
+
afterWrapper = false;
|
|
72
|
+
if (token.quoted) continue;
|
|
73
|
+
if (name === "git" && gitSubcommand(tokens.slice(i + 1)) === "commit-tree") return true;
|
|
74
|
+
if (name === "git-commit-tree") return true;
|
|
75
|
+
if (["bash", "sh", "dash", "zsh", "ksh"].includes(name)) {
|
|
76
|
+
for (let j = i + 1; j < tokens.length && !tokens[j]!.operator; j++) {
|
|
77
|
+
if (/^-[a-z]*c[a-z]*$/.test(tokens[j]!.value)) {
|
|
78
|
+
const script = tokens[j + 1];
|
|
79
|
+
if (script && !script.operator && invokesCommitTree(script.value, depth + 1)) return true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function shellCommitTreeReason(
|
|
89
|
+
toolName: unknown,
|
|
90
|
+
toolInput: unknown,
|
|
91
|
+
binName: string,
|
|
92
|
+
): string | null {
|
|
93
|
+
const command = shellCommandFromTool(toolName, toolInput);
|
|
94
|
+
if (command === null || !invokesCommitTree(command)) return null;
|
|
95
|
+
return (
|
|
96
|
+
`git commit-tree blocked: building a commit by hand gets around the Git index lock, and with ` +
|
|
97
|
+
`it every pre-commit check and the claim guard. When a peer's commit holds the lock, wait ` +
|
|
98
|
+
`for it to finish (\`${binName} admission status\` shows whether its checks are queued for a ` +
|
|
99
|
+
`slot), then commit normally. If you genuinely need plumbing, prefix the command with ` +
|
|
100
|
+
`${OVERRIDE}.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -22,6 +22,8 @@ export interface HarneryInboxMessageRecordV1 {
|
|
|
22
22
|
/** A required null identifies an operator; strings identify real agent instances. */
|
|
23
23
|
sender_instance_id: string | null;
|
|
24
24
|
sender_display_name: string;
|
|
25
|
+
/** Set only on a message imported from another machine (ADR 0222). */
|
|
26
|
+
sender_machine?: string;
|
|
25
27
|
recipient_instance_id: string;
|
|
26
28
|
recipient_display_name: string;
|
|
27
29
|
created_at: string;
|
|
@@ -68,6 +70,22 @@ export class HarneryInboxCapacityError extends Error {
|
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
export class HarneryInboxMessageConflictError extends Error {
|
|
74
|
+
readonly reason_code = "message_identity_conflict";
|
|
75
|
+
|
|
76
|
+
constructor(readonly message_id: string) {
|
|
77
|
+
super(`inbox message identity conflict: ${message_id}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class HarneryInboxMessageHistoryMissingError extends Error {
|
|
82
|
+
readonly reason_code = "message_history_unknown";
|
|
83
|
+
|
|
84
|
+
constructor(readonly message_id: string) {
|
|
85
|
+
super(`inbox message history unknown: ${message_id}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
export interface HarneryInboxSurfaceBudget {
|
|
72
90
|
max_count: number;
|
|
73
91
|
max_bytes: number;
|
|
@@ -1,7 +1,28 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type AgentParentReference,
|
|
5
|
+
resolveAgentParentReference,
|
|
6
|
+
} from "../agents/state/parent-reference.ts";
|
|
1
7
|
import { resolveBinName } from "../config.ts";
|
|
2
|
-
import type
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
8
|
+
import { type CoordinationViewV3, readCoordinationViewV3 } from "../events/v3/coordination-view.ts";
|
|
9
|
+
import { liveInstanceIdV3 } from "../events/v3/live-route-observer.ts";
|
|
10
|
+
import { publishMail, queueMail } from "../presence/mail.ts";
|
|
11
|
+
import { updateCrashSafeJsonlFile } from "../storage/durable-history.ts";
|
|
12
|
+
import {
|
|
13
|
+
HarneryInboxCapacityError,
|
|
14
|
+
type HarneryInboxLimits,
|
|
15
|
+
HarneryInboxMessageConflictError,
|
|
16
|
+
HarneryInboxMessageHistoryMissingError,
|
|
17
|
+
type HarneryInboxMessageRecordV1,
|
|
18
|
+
} from "./contract.ts";
|
|
19
|
+
import { HarneryInboxService, stableInboxMessageId } from "./service.ts";
|
|
20
|
+
import {
|
|
21
|
+
type MessageTargetInput,
|
|
22
|
+
type MessageTargetResult,
|
|
23
|
+
resolveMessageTarget,
|
|
24
|
+
resolveRemoteMessageTarget,
|
|
25
|
+
} from "./targets.ts";
|
|
5
26
|
|
|
6
27
|
const DAY = 24 * 60 * 60_000;
|
|
7
28
|
|
|
@@ -27,23 +48,487 @@ export function peerInbox(coordRoot: string): HarneryInboxService {
|
|
|
27
48
|
|
|
28
49
|
/** CLI and web use this one path. A receipt proves a synced append, never that
|
|
29
50
|
* a process is awake or that the recipient has noticed the message. */
|
|
30
|
-
export
|
|
51
|
+
export interface PeerMessageInput {
|
|
31
52
|
coordRoot: string;
|
|
32
53
|
target: MessageTargetInput;
|
|
33
54
|
senderInstanceId: string | null;
|
|
34
55
|
senderName: string;
|
|
35
56
|
body: string;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
/** Reuse the same caller-owned ID after a crash, with identical intent. */
|
|
58
|
+
operationId?: string;
|
|
59
|
+
/** A caller with an ambiguous prior dispatch must not create a new plan. */
|
|
60
|
+
operationReplayOnly?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type PeerTarget = Extract<MessageTargetResult, { ok: true }>["target"];
|
|
64
|
+
export type PeerRecipientResult =
|
|
65
|
+
| {
|
|
66
|
+
ok: true;
|
|
67
|
+
target: PeerTarget;
|
|
68
|
+
state: "written-live" | "queued" | "remote" | "duplicate";
|
|
69
|
+
message_id: string;
|
|
70
|
+
body_bytes: number;
|
|
71
|
+
synced: true;
|
|
72
|
+
published?: boolean;
|
|
73
|
+
publish_error?: string;
|
|
74
|
+
delivery_state?: "pending" | "delivered" | "expired";
|
|
75
|
+
}
|
|
76
|
+
| {
|
|
77
|
+
ok: false;
|
|
78
|
+
code: string;
|
|
79
|
+
message: string;
|
|
80
|
+
target?: PeerTarget;
|
|
81
|
+
candidates?: string[];
|
|
82
|
+
failure_stage?: "addressing" | "transport" | "operation";
|
|
83
|
+
};
|
|
84
|
+
type ParentCopyResult = PeerRecipientResult | { state: "skipped"; reason: string };
|
|
85
|
+
export type PeerMessageResult = PeerRecipientResult & {
|
|
86
|
+
operation_id: string;
|
|
87
|
+
child: PeerRecipientResult;
|
|
88
|
+
parent_reference: AgentParentReference;
|
|
89
|
+
parent_copy: ParentCopyResult;
|
|
90
|
+
complete: boolean;
|
|
91
|
+
};
|
|
92
|
+
interface RecipientPlan {
|
|
93
|
+
target: PeerTarget;
|
|
94
|
+
generation_id?: string;
|
|
95
|
+
body: string;
|
|
96
|
+
}
|
|
97
|
+
interface RoutingPlan {
|
|
98
|
+
kind: "plan";
|
|
99
|
+
schema: "harnery.peer-routing/v1";
|
|
100
|
+
request_sha256: string;
|
|
101
|
+
sender_instance_id: string | null;
|
|
102
|
+
sender_name: string;
|
|
103
|
+
require_live: boolean;
|
|
104
|
+
child: RecipientPlan;
|
|
105
|
+
parent_reference: AgentParentReference;
|
|
106
|
+
parent: RecipientPlan | ParentCopyResult;
|
|
107
|
+
}
|
|
108
|
+
type OperationRecord =
|
|
109
|
+
| RoutingPlan
|
|
110
|
+
| { kind: "started"; recipient: "child" | "parent" }
|
|
111
|
+
| { kind: "result"; recipient: "child" | "parent"; result: PeerRecipientResult };
|
|
112
|
+
const OPERATION_LIMITS = { max_record_bytes: 24_000, max_records: 5 };
|
|
113
|
+
|
|
114
|
+
/** One frozen routing intent, child first, then at most its direct parent.
|
|
115
|
+
* Receipts establish durable writes, never attention or a process wake. */
|
|
116
|
+
export function sendPeerMessage(input: PeerMessageInput): PeerMessageResult {
|
|
117
|
+
const operationId = input.operationId ?? randomUUID();
|
|
118
|
+
const unknown: AgentParentReference = { state: "unknown", reason: "original_target_unresolved" };
|
|
119
|
+
const refused = (code: string, message: string): PeerMessageResult => {
|
|
120
|
+
const child = { ok: false as const, code, message };
|
|
121
|
+
return {
|
|
122
|
+
...child,
|
|
123
|
+
child,
|
|
124
|
+
operation_id: operationId,
|
|
125
|
+
parent_reference: unknown,
|
|
126
|
+
parent_copy: { state: "skipped", reason: "original_target_unresolved" },
|
|
127
|
+
complete: false,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
if (!/^[A-Za-z0-9._:-]{1,160}$/.test(operationId))
|
|
131
|
+
return refused("invalid_operation_id", "Supply a bounded opaque operation ID.");
|
|
132
|
+
if (input.operationReplayOnly && !input.operationId)
|
|
133
|
+
return refused("invalid_operation_id", "Replay-only requires the original operation ID.");
|
|
134
|
+
if (!input.body.trim()) return refused("empty_message", "message is required (and non-empty)");
|
|
135
|
+
const request = hash(
|
|
136
|
+
JSON.stringify({
|
|
137
|
+
target: {
|
|
138
|
+
name: input.target.name ?? null,
|
|
139
|
+
instance: input.target.instanceId ?? null,
|
|
140
|
+
generation: input.target.generationId ?? null,
|
|
141
|
+
require_live: input.target.requireLive ?? false,
|
|
142
|
+
},
|
|
143
|
+
sender: input.senderInstanceId,
|
|
144
|
+
sender_name: input.senderName,
|
|
145
|
+
body: input.body,
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
const path = join(input.coordRoot, ".harnery", "peer-operations", `${hash(operationId)}.jsonl`);
|
|
149
|
+
let plan: RoutingPlan;
|
|
150
|
+
try {
|
|
151
|
+
const prepared = updateCrashSafeJsonlFile<OperationRecord, RoutingPlan | PeerRecipientResult>(
|
|
152
|
+
path,
|
|
153
|
+
OPERATION_LIMITS,
|
|
154
|
+
(records) => {
|
|
155
|
+
if (records.length) {
|
|
156
|
+
validateOperationRecords(records, operationId);
|
|
157
|
+
const existing = records[0];
|
|
158
|
+
if (existing?.kind !== "plan" || existing.schema !== "harnery.peer-routing/v1")
|
|
159
|
+
throw new Error("routing operation has no valid frozen plan");
|
|
160
|
+
if (existing.request_sha256 !== request)
|
|
161
|
+
return {
|
|
162
|
+
result: {
|
|
163
|
+
ok: false,
|
|
164
|
+
code: "operation_conflict",
|
|
165
|
+
message: "Operation ID already binds different sender, body or target intent.",
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
return { result: existing };
|
|
169
|
+
}
|
|
170
|
+
if (input.operationReplayOnly)
|
|
171
|
+
return {
|
|
172
|
+
result: {
|
|
173
|
+
ok: false,
|
|
174
|
+
code: "message_history_unknown",
|
|
175
|
+
message: "The original routing plan is unavailable; no recipient was resubmitted.",
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
const view = readCoordinationViewV3(input.coordRoot);
|
|
179
|
+
let target = resolveMessageTarget(input.coordRoot, input.target, { view });
|
|
180
|
+
if (!target.ok && target.code === "no_peer") {
|
|
181
|
+
target = resolveRemoteMessageTarget(input.coordRoot, input.target) ?? target;
|
|
182
|
+
}
|
|
183
|
+
if (!target.ok) return { result: target };
|
|
184
|
+
const generation = selectedGeneration(view, target.target.instance_id);
|
|
185
|
+
const reference = target.target.machine
|
|
186
|
+
? { state: "unknown" as const, reason: "remote_parent_evidence_unavailable" }
|
|
187
|
+
: resolveAgentParentReference(
|
|
188
|
+
input.coordRoot,
|
|
189
|
+
{ instance_id: target.target.instance_id, generation_id: generation?.generation_id },
|
|
190
|
+
{ view },
|
|
191
|
+
);
|
|
192
|
+
const child = {
|
|
193
|
+
target: target.target,
|
|
194
|
+
generation_id: generation?.generation_id,
|
|
195
|
+
body: input.body,
|
|
196
|
+
};
|
|
197
|
+
let parent: RoutingPlan["parent"] = {
|
|
198
|
+
state: "skipped",
|
|
199
|
+
reason: reference.state === "verified" ? "parent_not_addressable" : reference.reason,
|
|
200
|
+
};
|
|
201
|
+
if (reference.state === "verified" && reference.addressable) {
|
|
202
|
+
const parentTarget = resolveMessageTarget(
|
|
203
|
+
input.coordRoot,
|
|
204
|
+
{
|
|
205
|
+
instanceId: reference.parent_v3_instance_id,
|
|
206
|
+
generationId: reference.parent_generation_id ?? undefined,
|
|
207
|
+
requireLive: input.target.requireLive,
|
|
208
|
+
now: input.target.now,
|
|
209
|
+
},
|
|
210
|
+
{ view },
|
|
211
|
+
);
|
|
212
|
+
const body = `[Parent copy for agent-${target.target.name} (${target.target.instance_id})]\n${input.body}`;
|
|
213
|
+
parent = !parentTarget.ok
|
|
214
|
+
? parentTarget
|
|
215
|
+
: Buffer.byteLength(body) > PEER_INBOX_LIMITS.max_message_body_bytes
|
|
216
|
+
? {
|
|
217
|
+
ok: false,
|
|
218
|
+
code: "parent_copy_overflow",
|
|
219
|
+
message:
|
|
220
|
+
"The parent marker and unchanged message exceed the 4000-byte message limit.",
|
|
221
|
+
target: parentTarget.target,
|
|
222
|
+
}
|
|
223
|
+
: {
|
|
224
|
+
target: parentTarget.target,
|
|
225
|
+
generation_id: reference.parent_generation_id ?? undefined,
|
|
226
|
+
body,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const created: RoutingPlan = {
|
|
230
|
+
kind: "plan",
|
|
231
|
+
schema: "harnery.peer-routing/v1",
|
|
232
|
+
request_sha256: request,
|
|
233
|
+
sender_instance_id: input.senderInstanceId,
|
|
234
|
+
sender_name: input.senderName,
|
|
235
|
+
require_live: input.target.requireLive ?? false,
|
|
236
|
+
child,
|
|
237
|
+
parent_reference: reference,
|
|
238
|
+
parent,
|
|
239
|
+
};
|
|
240
|
+
return { append: [created], result: created };
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
if (!("kind" in prepared))
|
|
244
|
+
return {
|
|
245
|
+
...prepared,
|
|
246
|
+
child: prepared,
|
|
247
|
+
operation_id: operationId,
|
|
248
|
+
parent_reference: unknown,
|
|
249
|
+
parent_copy: { state: "skipped", reason: "original_target_unresolved" },
|
|
250
|
+
complete: false,
|
|
251
|
+
};
|
|
252
|
+
plan = prepared;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return refused(
|
|
255
|
+
"operation_unavailable",
|
|
256
|
+
error instanceof Error ? error.message : "Routing plan unavailable.",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const resolvePlanned = (recipient: RecipientPlan) =>
|
|
260
|
+
recipient.target.machine
|
|
261
|
+
? resolveRemoteMessageTarget(input.coordRoot, {
|
|
262
|
+
instanceId: recipient.target.instance_id,
|
|
263
|
+
requireLive: plan.require_live,
|
|
264
|
+
now: input.target.now,
|
|
265
|
+
})
|
|
266
|
+
: resolveMessageTarget(input.coordRoot, {
|
|
267
|
+
instanceId: recipient.target.instance_id,
|
|
268
|
+
generationId: recipient.generation_id,
|
|
269
|
+
requireLive: plan.require_live,
|
|
270
|
+
now: input.target.now,
|
|
271
|
+
});
|
|
272
|
+
const deliver = (
|
|
273
|
+
role: "child" | "parent",
|
|
274
|
+
recipient: RecipientPlan,
|
|
275
|
+
childOutcome?: PeerRecipientResult,
|
|
276
|
+
): PeerRecipientResult => {
|
|
277
|
+
try {
|
|
278
|
+
// Persist intent before transport. An interrupted prior attempt is only
|
|
279
|
+
// reconcilable from retained transport history, never from its absence.
|
|
280
|
+
const started = updateCrashSafeJsonlFile<
|
|
281
|
+
OperationRecord,
|
|
282
|
+
{ replay: boolean } | PeerRecipientResult
|
|
283
|
+
>(path, OPERATION_LIMITS, (records) => {
|
|
284
|
+
validateOperationRecords(records, operationId);
|
|
285
|
+
const prior = records.find((r) => r.kind === "result" && r.recipient === role);
|
|
286
|
+
if (prior?.kind === "result") return { result: prior.result };
|
|
287
|
+
// Historical receipts win; a new parent append requires the original
|
|
288
|
+
// child identity to remain safely addressable now.
|
|
289
|
+
if (role === "parent") {
|
|
290
|
+
const currentChild = resolvePlanned(plan.child);
|
|
291
|
+
if (
|
|
292
|
+
(childOutcome && !childOutcome.ok && childOutcome.failure_stage !== "transport") ||
|
|
293
|
+
!currentChild?.ok ||
|
|
294
|
+
currentChild.target.machine !== plan.child.target.machine
|
|
295
|
+
) {
|
|
296
|
+
return {
|
|
297
|
+
result: {
|
|
298
|
+
ok: false,
|
|
299
|
+
target: recipient.target,
|
|
300
|
+
failure_stage: "addressing",
|
|
301
|
+
code: "parent_copy_suppressed",
|
|
302
|
+
message: "Parent copy suppressed because the original task lost safe addressing.",
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (records.some((r) => r.kind === "started" && r.recipient === role))
|
|
308
|
+
return { result: { replay: true } };
|
|
309
|
+
return { append: [{ kind: "started", recipient: role }], result: { replay: false } };
|
|
310
|
+
});
|
|
311
|
+
if ("ok" in started) return started;
|
|
312
|
+
return updateCrashSafeJsonlFile<OperationRecord, PeerRecipientResult>(
|
|
313
|
+
path,
|
|
314
|
+
OPERATION_LIMITS,
|
|
315
|
+
(records) => {
|
|
316
|
+
validateOperationRecords(records, operationId);
|
|
317
|
+
const frozen = records[0];
|
|
318
|
+
if (frozen?.kind !== "plan" || frozen.request_sha256 !== request)
|
|
319
|
+
throw new Error("routing operation plan changed");
|
|
320
|
+
const prior = records.find((r) => r.kind === "result" && r.recipient === role);
|
|
321
|
+
if (prior?.kind === "result") return { result: prior.result };
|
|
322
|
+
const current = resolvePlanned(recipient);
|
|
323
|
+
let result: PeerRecipientResult;
|
|
324
|
+
if (!current?.ok || current.target.machine !== recipient.target.machine) {
|
|
325
|
+
result =
|
|
326
|
+
current && !current.ok
|
|
327
|
+
? { ...current, target: recipient.target, failure_stage: "addressing" }
|
|
328
|
+
: {
|
|
329
|
+
ok: false,
|
|
330
|
+
target: recipient.target,
|
|
331
|
+
code: "recipient_changed",
|
|
332
|
+
failure_stage: "addressing",
|
|
333
|
+
message:
|
|
334
|
+
"The frozen recipient is no longer safely addressable on the same machine.",
|
|
335
|
+
};
|
|
336
|
+
} else {
|
|
337
|
+
try {
|
|
338
|
+
result = deliverPeerRecipient(
|
|
339
|
+
input.coordRoot,
|
|
340
|
+
plan,
|
|
341
|
+
recipient,
|
|
342
|
+
operationId,
|
|
343
|
+
current.target.delivery,
|
|
344
|
+
started.replay,
|
|
345
|
+
);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
result = recipientError(error, recipient.target);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// A concurrent first attempt may still finish. Retain the start and
|
|
351
|
+
// report uncertainty, without turning absence into a permanent result.
|
|
352
|
+
if (!result.ok && result.code === "message_history_unknown") return { result };
|
|
353
|
+
return { append: [{ kind: "result", recipient: role, result }], result };
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return recipientError(error, recipient.target, "operation");
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
const child = deliver("child", plan.child);
|
|
361
|
+
const parent = "body" in plan.parent ? deliver("parent", plan.parent, child) : plan.parent;
|
|
362
|
+
return {
|
|
363
|
+
...child,
|
|
364
|
+
child,
|
|
365
|
+
operation_id: operationId,
|
|
366
|
+
parent_reference: plan.parent_reference,
|
|
367
|
+
parent_copy: parent,
|
|
368
|
+
complete: child.ok && (!("ok" in parent) || parent.ok),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function selectedGeneration(view: CoordinationViewV3, instance: string) {
|
|
373
|
+
const id = liveInstanceIdV3(instance);
|
|
374
|
+
return (
|
|
375
|
+
view.instances[id] ??
|
|
376
|
+
Object.values(view.terminal_generations)
|
|
377
|
+
.filter((row) => row.instance_id === id)
|
|
378
|
+
.sort(
|
|
379
|
+
(a, b) =>
|
|
380
|
+
b.source_position.segment_ordinal - a.source_position.segment_ordinal ||
|
|
381
|
+
b.source_position.byte_offset - a.source_position.byte_offset,
|
|
382
|
+
)[0]
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function hash(value: string): string {
|
|
387
|
+
return createHash("sha256").update(value).digest("hex");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function validateOperationRecords(records: readonly OperationRecord[], operationId: string): void {
|
|
391
|
+
const plan = records[0];
|
|
392
|
+
if (
|
|
393
|
+
plan?.kind !== "plan" ||
|
|
394
|
+
plan.schema !== "harnery.peer-routing/v1" ||
|
|
395
|
+
typeof plan.request_sha256 !== "string" ||
|
|
396
|
+
typeof plan.sender_name !== "string" ||
|
|
397
|
+
(plan.sender_instance_id !== null && typeof plan.sender_instance_id !== "string") ||
|
|
398
|
+
typeof plan.require_live !== "boolean"
|
|
399
|
+
)
|
|
400
|
+
throw new Error("Invalid routing operation plan.");
|
|
401
|
+
const validRecipient = (r: RecipientPlan) =>
|
|
402
|
+
typeof r.body === "string" &&
|
|
403
|
+
r.target &&
|
|
404
|
+
typeof r.target.instance_id === "string" &&
|
|
405
|
+
typeof r.target.name === "string" &&
|
|
406
|
+
(r.generation_id === undefined || typeof r.generation_id === "string");
|
|
407
|
+
const validParent =
|
|
408
|
+
"body" in plan.parent
|
|
409
|
+
? validRecipient(plan.parent)
|
|
410
|
+
: "ok" in plan.parent
|
|
411
|
+
? plan.parent.ok === false &&
|
|
412
|
+
typeof plan.parent.code === "string" &&
|
|
413
|
+
typeof plan.parent.message === "string"
|
|
414
|
+
: plan.parent.state === "skipped" && typeof plan.parent.reason === "string";
|
|
415
|
+
if (!validRecipient(plan.child) || !validParent)
|
|
416
|
+
throw new Error("Invalid frozen recipient plan.");
|
|
417
|
+
const started = new Set<string>();
|
|
418
|
+
const completed = new Set<string>();
|
|
419
|
+
for (const row of records.slice(1)) {
|
|
420
|
+
if (row.kind === "plan" || !["child", "parent"].includes(row.recipient))
|
|
421
|
+
throw new Error("Invalid routing operation record.");
|
|
422
|
+
if (row.kind === "started") {
|
|
423
|
+
if (started.has(row.recipient)) throw new Error("Duplicate recipient start.");
|
|
424
|
+
started.add(row.recipient);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (row.kind !== "result" || !started.has(row.recipient) || completed.has(row.recipient))
|
|
428
|
+
throw new Error("Invalid recipient result sequence.");
|
|
429
|
+
completed.add(row.recipient);
|
|
430
|
+
const recipient = row.recipient === "child" ? plan.child : plan.parent;
|
|
431
|
+
const result = row.result;
|
|
432
|
+
if (!("body" in recipient) || !result || typeof result.ok !== "boolean")
|
|
433
|
+
throw new Error("Invalid recipient result.");
|
|
434
|
+
if (result.ok) {
|
|
435
|
+
if (
|
|
436
|
+
result.target?.instance_id !== recipient.target.instance_id ||
|
|
437
|
+
result.message_id !== stableInboxMessageId(operationId, recipient.target.instance_id) ||
|
|
438
|
+
result.body_bytes !== Buffer.byteLength(recipient.body) ||
|
|
439
|
+
result.synced !== true ||
|
|
440
|
+
!["written-live", "queued", "remote", "duplicate"].includes(result.state)
|
|
441
|
+
)
|
|
442
|
+
throw new Error("Recipient receipt does not match frozen intent.");
|
|
443
|
+
} else if (typeof result.code !== "string" || typeof result.message !== "string") {
|
|
444
|
+
throw new Error("Invalid recipient failure.");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function recipientError(
|
|
450
|
+
error: unknown,
|
|
451
|
+
target: PeerTarget,
|
|
452
|
+
failureStage: "transport" | "operation" = "transport",
|
|
453
|
+
): PeerRecipientResult {
|
|
454
|
+
return {
|
|
455
|
+
ok: false,
|
|
456
|
+
target,
|
|
457
|
+
failure_stage: failureStage,
|
|
458
|
+
code:
|
|
459
|
+
error instanceof HarneryInboxCapacityError
|
|
460
|
+
? `inbox_${error.reason_code}`
|
|
461
|
+
: error instanceof HarneryInboxMessageHistoryMissingError ||
|
|
462
|
+
error instanceof HarneryInboxMessageConflictError
|
|
463
|
+
? error.reason_code
|
|
464
|
+
: "recipient_delivery_failed",
|
|
465
|
+
message: error instanceof Error ? error.message : "Recipient delivery failed.",
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function deliverPeerRecipient(
|
|
470
|
+
coordRoot: string,
|
|
471
|
+
plan: RoutingPlan,
|
|
472
|
+
recipient: RecipientPlan,
|
|
473
|
+
operationId: string,
|
|
474
|
+
delivery: PeerTarget["delivery"],
|
|
475
|
+
replayOnly: boolean,
|
|
476
|
+
): PeerRecipientResult {
|
|
477
|
+
const target = recipient.target;
|
|
478
|
+
const messageId = stableInboxMessageId(operationId, target.instance_id);
|
|
479
|
+
if (target.machine) {
|
|
480
|
+
// Cross-machine (ADR 0222): queue in this machine's mail ref and push it
|
|
481
|
+
// now. A failed push keeps the message queued for the next hook publish.
|
|
482
|
+
const queued = queueMail(coordRoot, {
|
|
483
|
+
messageId,
|
|
484
|
+
replayOnly,
|
|
485
|
+
toMachine: target.machine,
|
|
486
|
+
senderInstanceId: plan.sender_instance_id,
|
|
487
|
+
senderName: plan.sender_name,
|
|
488
|
+
recipientInstanceId: target.instance_id,
|
|
489
|
+
recipientName: target.name,
|
|
490
|
+
body: recipient.body,
|
|
491
|
+
maxBodyBytes: PEER_INBOX_LIMITS.max_message_body_bytes,
|
|
492
|
+
});
|
|
493
|
+
const envelope = queued.envelope;
|
|
494
|
+
if (queued.delivery_state === "expired")
|
|
495
|
+
return {
|
|
496
|
+
ok: false,
|
|
497
|
+
target,
|
|
498
|
+
code: "remote_message_expired",
|
|
499
|
+
message: "The original remote message expired; it was not republished.",
|
|
500
|
+
};
|
|
501
|
+
const published =
|
|
502
|
+
queued.delivery_state === "delivered"
|
|
503
|
+
? null
|
|
504
|
+
: publishMail(coordRoot, { sync: true, force: true });
|
|
505
|
+
return {
|
|
506
|
+
ok: true as const,
|
|
507
|
+
target,
|
|
508
|
+
state: queued.state === "duplicate" ? "duplicate" : "remote",
|
|
509
|
+
message_id: envelope.message_id,
|
|
510
|
+
body_bytes: envelope.body_bytes,
|
|
511
|
+
synced: true as const,
|
|
512
|
+
delivery_state: queued.delivery_state,
|
|
513
|
+
published: published?.status === "published",
|
|
514
|
+
...(published?.status === "error" ? { publish_error: published.error } : {}),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
const receipt = peerInbox(coordRoot).send({
|
|
518
|
+
message_id: messageId,
|
|
519
|
+
replay_only: replayOnly,
|
|
520
|
+
sender_instance_id: plan.sender_instance_id,
|
|
521
|
+
sender_display_name: plan.sender_name,
|
|
522
|
+
recipient_instance_id: target.instance_id,
|
|
523
|
+
recipient_display_name: target.name,
|
|
524
|
+
body: recipient.body,
|
|
45
525
|
});
|
|
46
|
-
return {
|
|
526
|
+
return {
|
|
527
|
+
ok: true,
|
|
528
|
+
target,
|
|
529
|
+
...receipt,
|
|
530
|
+
state: receipt.state === "duplicate" ? "duplicate" : delivery,
|
|
531
|
+
};
|
|
47
532
|
}
|
|
48
533
|
|
|
49
534
|
export function formatPeerInbox(
|
|
@@ -62,7 +547,8 @@ export function formatPeerInbox(
|
|
|
62
547
|
const body = messages
|
|
63
548
|
.map(
|
|
64
549
|
(message) =>
|
|
65
|
-
` ${message.message_id} from ${message.sender_instance_id === null ? "Operator" : `agent-${message.sender_display_name}`}
|
|
550
|
+
` ${message.message_id} from ${message.sender_instance_id === null ? "Operator" : `agent-${message.sender_display_name}`}` +
|
|
551
|
+
`${message.sender_machine ? ` on ${message.sender_machine}` : ""} (${message.created_at}):\n` +
|
|
66
552
|
message.body
|
|
67
553
|
.split("\n")
|
|
68
554
|
.map((line) => ` ${line}`)
|