harnery 0.45.2 → 0.46.1
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/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +36 -16
- package/dist/core/agents/rules/stop-hook.d.ts +6 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +20 -1
- 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 +103 -5
- 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/resolve/transcript.d.ts +12 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +54 -0
- 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/rules/claim-conflict.ts +50 -18
- package/src/core/agents/rules/stop-hook.ts +28 -1
- 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 +110 -4
- package/src/core/hooks/resolve/owner.ts +0 -23
- package/src/core/hooks/resolve/transcript.ts +61 -0
- 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
|
@@ -6,6 +6,8 @@ export interface HarneryInboxServiceOptions {
|
|
|
6
6
|
id?: () => string;
|
|
7
7
|
}
|
|
8
8
|
export interface HarneryInboxSendInput {
|
|
9
|
+
message_id?: string;
|
|
10
|
+
replay_only?: boolean;
|
|
9
11
|
sender_instance_id: string | null;
|
|
10
12
|
sender_display_name: string;
|
|
11
13
|
recipient_instance_id: string;
|
|
@@ -13,15 +15,20 @@ export interface HarneryInboxSendInput {
|
|
|
13
15
|
body: string;
|
|
14
16
|
}
|
|
15
17
|
export interface HarneryInboxSendReceipt {
|
|
16
|
-
state: "queued";
|
|
18
|
+
state: "queued" | "duplicate";
|
|
17
19
|
message_id: string;
|
|
18
20
|
body_bytes: number;
|
|
19
21
|
synced: true;
|
|
20
22
|
}
|
|
23
|
+
/** A caller freezes the operation and recipient before its first attempt. */
|
|
24
|
+
export declare function stableInboxMessageId(operationId: string, recipientInstanceId: string): string;
|
|
21
25
|
export declare class HarneryInboxService {
|
|
22
26
|
#private;
|
|
23
27
|
constructor(options: HarneryInboxServiceOptions);
|
|
24
28
|
send(input: HarneryInboxSendInput): HarneryInboxSendReceipt;
|
|
29
|
+
/** Append a message created on another machine, keeping its id. Importers
|
|
30
|
+
* on one machine can race, so the duplicate check runs under the lease. */
|
|
31
|
+
receive(record: HarneryInboxMessageRecordV1): "received" | "duplicate";
|
|
25
32
|
records(recipientInstanceId: string): HarneryInboxRecordV1[];
|
|
26
33
|
pending(recipientInstanceId: string): HarneryInboxMessageRecordV1[];
|
|
27
34
|
status(recipientInstanceId: string): HarneryInboxStatusV1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/core/inbox/service.ts"],"names":[],"mappings":"AASA,OAAO,EAIL,KAAK,kBAAkB,
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/core/inbox/service.ts"],"names":[],"mappings":"AASA,OAAO,EAIL,KAAK,kBAAkB,EAGvB,KAAK,2BAA2B,EAChC,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,EAC/B,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,IAAI,CAAC;CACd;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,GAAG,MAAM,CAO7F;AAED,qBAAa,mBAAmB;;gBAGlB,OAAO,EAAE,0BAA0B;IAK/C,IAAI,CAAC,KAAK,EAAE,qBAAqB,GAAG,uBAAuB;IAsE3D;+EAC2E;IAC3E,OAAO,CAAC,MAAM,EAAE,2BAA2B,GAAG,UAAU,GAAG,WAAW;IA4CtE,OAAO,CAAC,mBAAmB,EAAE,MAAM,GAAG,oBAAoB,EAAE;IAS5D,OAAO,CAAC,mBAAmB,EAAE,MAAM,GAAG,2BAA2B,EAAE;IAInE,MAAM,CAAC,mBAAmB,EAAE,MAAM,GAAG,oBAAoB;IA0BnD,OAAO,CACX,mBAAmB,EAAE,MAAM,EAC3B,IAAI,EAAE,CAAC,QAAQ,EAAE,SAAS,2BAA2B,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EAChF,MAAM,GAAE,yBAIP,GACA,OAAO,CAAC,yBAAyB,CAAC;IAiBrC;0EACsE;IACtE,cAAc,CACZ,mBAAmB,EAAE,MAAM,EAC3B,MAAM,GAAE,yBAIP,GACA,2BAA2B,EAAE;IAKhC;;2DAEuD;IACvD,kBAAkB,CAAC,mBAAmB,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI;CA2BrF;AA8CD,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,GAAG,MAAM,CAGhF;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,SAAS,oBAAoB,EAAE,GAAG;IACjE,OAAO,EAAE,2BAA2B,EAAE,CAAC;IACvC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAsBA"}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, statSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { readCrashSafeJsonlFile, updateCrashSafeJsonlFile, } from "../storage/durable-history.js";
|
|
5
|
-
import { HARNERY_INBOX_RECORD_SCHEMA, HARNERY_INBOX_STATUS_SCHEMA, HarneryInboxCapacityError, } from "./contract.js";
|
|
5
|
+
import { HARNERY_INBOX_RECORD_SCHEMA, HARNERY_INBOX_STATUS_SCHEMA, HarneryInboxCapacityError, HarneryInboxMessageConflictError, HarneryInboxMessageHistoryMissingError, } from "./contract.js";
|
|
6
|
+
/** A caller freezes the operation and recipient before its first attempt. */
|
|
7
|
+
export function stableInboxMessageId(operationId, recipientInstanceId) {
|
|
8
|
+
const identity = JSON.stringify([
|
|
9
|
+
"harnery.inbox-operation/v1",
|
|
10
|
+
opaqueId(operationId, "operation"),
|
|
11
|
+
opaqueId(recipientInstanceId, "recipient instance"),
|
|
12
|
+
]);
|
|
13
|
+
return `msg_op_${createHash("sha256").update(identity).digest("hex")}`;
|
|
14
|
+
}
|
|
6
15
|
export class HarneryInboxService {
|
|
7
16
|
#options;
|
|
8
17
|
constructor(options) {
|
|
@@ -10,6 +19,12 @@ export class HarneryInboxService {
|
|
|
10
19
|
this.#options = options;
|
|
11
20
|
}
|
|
12
21
|
send(input) {
|
|
22
|
+
if (input.replay_only !== undefined && typeof input.replay_only !== "boolean") {
|
|
23
|
+
throw new Error("invalid replay_only flag");
|
|
24
|
+
}
|
|
25
|
+
if (input.replay_only && input.message_id === undefined) {
|
|
26
|
+
throw new Error("replay requires explicit message id");
|
|
27
|
+
}
|
|
13
28
|
const recipient = opaqueId(input.recipient_instance_id, "recipient instance");
|
|
14
29
|
const bodyBytes = Buffer.byteLength(input.body);
|
|
15
30
|
const limits = this.#options.limits;
|
|
@@ -19,7 +34,9 @@ export class HarneryInboxService {
|
|
|
19
34
|
const record = {
|
|
20
35
|
schema: HARNERY_INBOX_RECORD_SCHEMA,
|
|
21
36
|
kind: "message",
|
|
22
|
-
message_id:
|
|
37
|
+
message_id: input.message_id === undefined
|
|
38
|
+
? `msg_${this.#options.id?.() ?? randomUUID()}`
|
|
39
|
+
: opaqueId(input.message_id, "message"),
|
|
23
40
|
sender_instance_id: senderId(input.sender_instance_id),
|
|
24
41
|
sender_display_name: boundedDisplay(input.sender_display_name),
|
|
25
42
|
recipient_instance_id: recipient,
|
|
@@ -31,6 +48,21 @@ export class HarneryInboxService {
|
|
|
31
48
|
const path = inboxPath(this.#options.coord_root, recipient);
|
|
32
49
|
return updateInboxHistory(path, readOptions(limits), (records) => {
|
|
33
50
|
const projection = project(records);
|
|
51
|
+
const existing = records.find((item) => item.kind === "message" && item.message_id === record.message_id);
|
|
52
|
+
if (existing) {
|
|
53
|
+
assertSameMessage(existing, record);
|
|
54
|
+
return {
|
|
55
|
+
result: {
|
|
56
|
+
state: "duplicate",
|
|
57
|
+
message_id: existing.message_id,
|
|
58
|
+
body_bytes: existing.body_bytes,
|
|
59
|
+
synced: true,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (input.replay_only) {
|
|
64
|
+
throw new HarneryInboxMessageHistoryMissingError(record.message_id);
|
|
65
|
+
}
|
|
34
66
|
capacityCheck(projection, record, path, limits);
|
|
35
67
|
return {
|
|
36
68
|
append: [record],
|
|
@@ -43,6 +75,40 @@ export class HarneryInboxService {
|
|
|
43
75
|
};
|
|
44
76
|
});
|
|
45
77
|
}
|
|
78
|
+
/** Append a message created on another machine, keeping its id. Importers
|
|
79
|
+
* on one machine can race, so the duplicate check runs under the lease. */
|
|
80
|
+
receive(record) {
|
|
81
|
+
const recipient = opaqueId(record.recipient_instance_id, "recipient instance");
|
|
82
|
+
const limits = this.#options.limits;
|
|
83
|
+
if (record.body_bytes === 0 || record.body_bytes > limits.max_message_body_bytes) {
|
|
84
|
+
throw new HarneryInboxCapacityError("message_body_limit", record.body_bytes, limits.max_message_body_bytes);
|
|
85
|
+
}
|
|
86
|
+
const incoming = {
|
|
87
|
+
schema: HARNERY_INBOX_RECORD_SCHEMA,
|
|
88
|
+
kind: "message",
|
|
89
|
+
message_id: opaqueId(record.message_id, "message"),
|
|
90
|
+
sender_instance_id: senderId(record.sender_instance_id),
|
|
91
|
+
sender_display_name: boundedDisplay(record.sender_display_name),
|
|
92
|
+
...(record.sender_machine ? { sender_machine: boundedDisplay(record.sender_machine) } : {}),
|
|
93
|
+
recipient_instance_id: recipient,
|
|
94
|
+
recipient_display_name: boundedDisplay(record.recipient_display_name),
|
|
95
|
+
created_at: record.created_at,
|
|
96
|
+
body: record.body,
|
|
97
|
+
body_bytes: record.body_bytes,
|
|
98
|
+
};
|
|
99
|
+
validateRecord(incoming);
|
|
100
|
+
const path = inboxPath(this.#options.coord_root, recipient);
|
|
101
|
+
return updateInboxHistory(path, readOptions(limits), (records) => {
|
|
102
|
+
const projection = project(records);
|
|
103
|
+
const existing = records.find((item) => item.kind === "message" && item.message_id === incoming.message_id);
|
|
104
|
+
if (existing) {
|
|
105
|
+
assertSameMessage(existing, incoming);
|
|
106
|
+
return { result: "duplicate" };
|
|
107
|
+
}
|
|
108
|
+
capacityCheck(projection, incoming, path, limits);
|
|
109
|
+
return { append: [incoming], result: "received" };
|
|
110
|
+
});
|
|
111
|
+
}
|
|
46
112
|
records(recipientInstanceId) {
|
|
47
113
|
const records = readCrashSafeJsonlFile(inboxPath(this.#options.coord_root, opaqueId(recipientInstanceId, "recipient instance")), readOptions(this.#options.limits));
|
|
48
114
|
for (const record of records)
|
|
@@ -125,6 +191,20 @@ export class HarneryInboxService {
|
|
|
125
191
|
}
|
|
126
192
|
}
|
|
127
193
|
}
|
|
194
|
+
function assertSameMessage(existing, incoming) {
|
|
195
|
+
const fields = [
|
|
196
|
+
"sender_instance_id",
|
|
197
|
+
"sender_display_name",
|
|
198
|
+
"sender_machine",
|
|
199
|
+
"recipient_instance_id",
|
|
200
|
+
"recipient_display_name",
|
|
201
|
+
"body",
|
|
202
|
+
"body_bytes",
|
|
203
|
+
];
|
|
204
|
+
if (fields.some((field) => existing[field] !== incoming[field])) {
|
|
205
|
+
throw new HarneryInboxMessageConflictError(incoming.message_id);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
128
208
|
/** The shared writer refuses an occupied lease before invoking the update.
|
|
129
209
|
* Briefly retry only that exact refusal so concurrent peer senders both get
|
|
130
210
|
* a chance to append. All validation, authority and I/O failures still fail
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { type CoordinationViewV3 } from "../events/v3/coordination-view.js";
|
|
1
2
|
export interface MessageTargetInput {
|
|
2
3
|
name?: string;
|
|
3
4
|
instanceId?: string;
|
|
5
|
+
/** Refuse a later generation of the same immutable instance. */
|
|
6
|
+
generationId?: string;
|
|
4
7
|
requireLive?: boolean;
|
|
5
8
|
now?: Date;
|
|
6
9
|
}
|
|
@@ -9,14 +12,25 @@ export type MessageTargetResult = {
|
|
|
9
12
|
target: {
|
|
10
13
|
instance_id: string;
|
|
11
14
|
name: string;
|
|
12
|
-
delivery: "written-live" | "queued";
|
|
15
|
+
delivery: "written-live" | "queued" | "remote";
|
|
16
|
+
/** Set when the recipient lives on another machine (ADR 0222). */
|
|
17
|
+
machine?: string;
|
|
13
18
|
};
|
|
14
19
|
} | {
|
|
15
20
|
ok: false;
|
|
16
|
-
code: "no_peer" | "ambiguous_peer" | "not_live" | "authority_unsafe";
|
|
21
|
+
code: "no_peer" | "ambiguous_peer" | "not_live" | "authority_unsafe" | "generation_mismatch";
|
|
17
22
|
message: string;
|
|
18
23
|
candidates?: string[];
|
|
19
24
|
};
|
|
20
25
|
/** Resolve one immutable recipient without extending presence or claiming attention. */
|
|
21
|
-
export declare function resolveMessageTarget(coordRoot: string, input: MessageTargetInput
|
|
26
|
+
export declare function resolveMessageTarget(coordRoot: string, input: MessageTargetInput, options?: {
|
|
27
|
+
view?: CoordinationViewV3;
|
|
28
|
+
}): MessageTargetResult;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a recipient on another machine, for a name or instance this
|
|
31
|
+
* machine's ledger has never seen. Candidates come from peer presence (live
|
|
32
|
+
* or stale) and from senders this machine has received mail from. Returns
|
|
33
|
+
* null when nothing matches, so the caller can report the local miss.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveRemoteMessageTarget(coordRoot: string, input: MessageTargetInput): MessageTargetResult | null;
|
|
22
36
|
//# sourceMappingURL=targets.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"targets.d.ts","sourceRoot":"","sources":["../../../src/core/inbox/targets.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"targets.d.ts","sourceRoot":"","sources":["../../../src/core/inbox/targets.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,kBAAkB,EAGxB,MAAM,mCAAmC,CAAC;AAS3C,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED,MAAM,MAAM,mBAAmB,GAC3B;IACE,EAAE,EAAE,IAAI,CAAC;IACT,MAAM,EAAE;QACN,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,cAAc,GAAG,QAAQ,GAAG,QAAQ,CAAC;QAC/C,kEAAkE;QAClE,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,IAAI,EAAE,SAAS,GAAG,gBAAgB,GAAG,UAAU,GAAG,kBAAkB,GAAG,qBAAqB,CAAC;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB,CAAC;AAEN,wFAAwF;AACxF,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,kBAAkB,EACzB,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,kBAAkB,CAAA;CAAO,GAC1C,mBAAmB,CAkGrB;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,kBAAkB,GACxB,mBAAmB,GAAG,IAAI,CA2D5B"}
|
|
@@ -2,8 +2,10 @@ import { resolveName } from "../agents/state/names.js";
|
|
|
2
2
|
import { coordFreshnessSeconds } from "../config.js";
|
|
3
3
|
import { readCoordinationViewV3, requireAuthoritySafeCoordinationViewV3, } from "../events/v3/coordination-view.js";
|
|
4
4
|
import { liveInstanceIdV3, nativeInstanceIdV3, observeLiveEventLedgerRouteV3, } from "../events/v3/live-route-observer.js";
|
|
5
|
+
import { readRemoteMachines } from "../presence/index.js";
|
|
6
|
+
import { knownRemoteSenders } from "../presence/mail.js";
|
|
5
7
|
/** Resolve one immutable recipient without extending presence or claiming attention. */
|
|
6
|
-
export function resolveMessageTarget(coordRoot, input) {
|
|
8
|
+
export function resolveMessageTarget(coordRoot, input, options = {}) {
|
|
7
9
|
const requestedName = normalizeName(input.name);
|
|
8
10
|
const requestedInstance = input.instanceId?.trim();
|
|
9
11
|
if (!requestedName && !requestedInstance) {
|
|
@@ -17,7 +19,7 @@ export function resolveMessageTarget(coordRoot, input) {
|
|
|
17
19
|
if (observeLiveEventLedgerRouteV3(coordRoot).state === "blocked") {
|
|
18
20
|
return unsafe("The event ledger cannot establish message recipient authority.");
|
|
19
21
|
}
|
|
20
|
-
view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(coordRoot));
|
|
22
|
+
view = requireAuthoritySafeCoordinationViewV3(options.view ?? readCoordinationViewV3(coordRoot));
|
|
21
23
|
if (!view.source_complete)
|
|
22
24
|
return unsafe("The event ledger read is incomplete.");
|
|
23
25
|
}
|
|
@@ -50,6 +52,7 @@ export function resolveMessageTarget(coordRoot, input) {
|
|
|
50
52
|
{
|
|
51
53
|
instanceId,
|
|
52
54
|
name,
|
|
55
|
+
generationId: generation.generation_id,
|
|
53
56
|
safe: !diagnostics.some((diagnostic) => diagnostic.authority_blocking),
|
|
54
57
|
fresh: generation.phase === "live" && Number.isFinite(age) && age >= 0 && age <= freshnessMs,
|
|
55
58
|
},
|
|
@@ -76,6 +79,13 @@ export function resolveMessageTarget(coordRoot, input) {
|
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
const target = selected[0];
|
|
82
|
+
if (input.generationId && input.generationId !== target.generationId) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
code: "generation_mismatch",
|
|
86
|
+
message: "The recipient generation differs from the exact requested generation.",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
79
89
|
if (input.requireLive && !target.fresh) {
|
|
80
90
|
return {
|
|
81
91
|
ok: false,
|
|
@@ -92,6 +102,71 @@ export function resolveMessageTarget(coordRoot, input) {
|
|
|
92
102
|
},
|
|
93
103
|
};
|
|
94
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve a recipient on another machine, for a name or instance this
|
|
107
|
+
* machine's ledger has never seen. Candidates come from peer presence (live
|
|
108
|
+
* or stale) and from senders this machine has received mail from. Returns
|
|
109
|
+
* null when nothing matches, so the caller can report the local miss.
|
|
110
|
+
*/
|
|
111
|
+
export function resolveRemoteMessageTarget(coordRoot, input) {
|
|
112
|
+
const requestedName = normalizeName(input.name);
|
|
113
|
+
const requestedInstance = input.instanceId?.trim();
|
|
114
|
+
if (!requestedName && !requestedInstance)
|
|
115
|
+
return null;
|
|
116
|
+
// Presence/mail names do not prove an exact V3 generation.
|
|
117
|
+
if (input.generationId)
|
|
118
|
+
return null;
|
|
119
|
+
const live = new Set(readRemoteMachines(coordRoot).flatMap(({ agents }) => agents.map((a) => a.instance_id)));
|
|
120
|
+
const known = new Map();
|
|
121
|
+
for (const { machine, agents } of readRemoteMachines(coordRoot, { includeStale: true })) {
|
|
122
|
+
for (const agent of agents) {
|
|
123
|
+
if (agent.name)
|
|
124
|
+
known.set(agent.instance_id, { instanceId: agent.instance_id, name: agent.name, machine });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const sender of knownRemoteSenders(coordRoot)) {
|
|
128
|
+
if (!known.has(sender.instance_id)) {
|
|
129
|
+
known.set(sender.instance_id, {
|
|
130
|
+
instanceId: sender.instance_id,
|
|
131
|
+
name: sender.name,
|
|
132
|
+
machine: sender.machine,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const candidates = [...known.values()]
|
|
137
|
+
.filter(({ instanceId }) => !requestedInstance || instanceId === requestedInstance)
|
|
138
|
+
.filter(({ name }) => !requestedName || normalizeName(name) === requestedName)
|
|
139
|
+
.map((candidate) => ({ ...candidate, fresh: live.has(candidate.instanceId) }));
|
|
140
|
+
if (candidates.length === 0)
|
|
141
|
+
return null;
|
|
142
|
+
const fresh = candidates.filter(({ fresh }) => fresh);
|
|
143
|
+
const selected = requestedInstance ? candidates : fresh.length > 0 ? fresh : candidates;
|
|
144
|
+
if (selected.length !== 1) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
code: "ambiguous_peer",
|
|
148
|
+
message: "Several agent instances on other machines hold that exact name; specify --instance-id.",
|
|
149
|
+
candidates: selected.map(({ instanceId, machine }) => `${instanceId} (${machine})`).sort(),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const target = selected[0];
|
|
153
|
+
if (input.requireLive && !target.fresh) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
code: "not_live",
|
|
157
|
+
message: "That agent is on another machine and outside live presence; omit --require-live to queue its message.",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
ok: true,
|
|
162
|
+
target: {
|
|
163
|
+
instance_id: target.instanceId,
|
|
164
|
+
name: target.name,
|
|
165
|
+
delivery: "remote",
|
|
166
|
+
machine: target.machine,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
95
170
|
function laterPosition(left, right) {
|
|
96
171
|
return (left.source_position.segment_ordinal > right.source_position.segment_ordinal ||
|
|
97
172
|
(left.source_position.segment_ordinal === right.source_position.segment_ordinal &&
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Git plumbing for the cross-machine presence transport (ADR 0016)
|
|
2
|
+
* Git plumbing for the cross-machine presence transport (ADR 0016) and the
|
|
3
|
+
* mail refs that ride on it (ADR 0222).
|
|
3
4
|
*
|
|
4
5
|
* Presence blobs travel as parentless commits (empty tree, JSON message)
|
|
5
6
|
* force-pushed to `refs/harnery/presence/<machine>` on the host repo's own
|
|
@@ -12,9 +13,12 @@
|
|
|
12
13
|
* detached (hook path: never block a turn) or synchronously (CLI path: the
|
|
13
14
|
* user wants to see errors).
|
|
14
15
|
*/
|
|
15
|
-
/** The ref
|
|
16
|
-
* publisher is structurally unable to touch refs/heads/* (ADR 0016).
|
|
16
|
+
/** The ref namespaces. Hardcoded — never interpolated from config — so the
|
|
17
|
+
* publisher is structurally unable to touch refs/heads/* (ADR 0016). Mail
|
|
18
|
+
* refs carry cross-machine peer messages on the same transport (ADR 0222). */
|
|
17
19
|
export declare const PRESENCE_REF_PREFIX = "refs/harnery/presence/";
|
|
20
|
+
export declare const MAIL_REF_PREFIX = "refs/harnery/mail/";
|
|
21
|
+
type HarneryRefPrefix = typeof PRESENCE_REF_PREFIX | typeof MAIL_REF_PREFIX;
|
|
18
22
|
/** Does the repo have an `origin` remote to rendezvous on? */
|
|
19
23
|
export declare function hasOrigin(root: string): boolean;
|
|
20
24
|
/** The origin remote URL, or null. Input to relay room derivation. */
|
|
@@ -34,25 +38,26 @@ export declare function sanitizeRefComponent(label: string): string;
|
|
|
34
38
|
* Returns the commit sha, or null on failure. Local-only, fast (~10ms). */
|
|
35
39
|
export declare function writePresenceCommit(root: string, message: string): string | null;
|
|
36
40
|
/**
|
|
37
|
-
* Force-push a
|
|
38
|
-
*
|
|
39
|
-
*
|
|
41
|
+
* Force-push a commit to `<prefix><machine>` (presence or mail). `sync: true`
|
|
42
|
+
* (CLI/tests) waits and reports; default is detached fire-and-forget (hook
|
|
43
|
+
* path — a slow or credential-less push must not block a turn).
|
|
40
44
|
*/
|
|
41
|
-
export declare function
|
|
45
|
+
export declare function pushMachineRef(root: string, prefix: HarneryRefPrefix, machine: string, sha: string, opts?: {
|
|
42
46
|
sync?: boolean;
|
|
43
47
|
}): {
|
|
44
48
|
ok: boolean;
|
|
45
49
|
error?: string;
|
|
46
50
|
};
|
|
47
|
-
/** Fetch every peer presence ref from origin into the local ref
|
|
48
|
-
|
|
51
|
+
/** Fetch every peer presence and mail ref from origin into the local ref
|
|
52
|
+
* namespace, in one round trip. A namespace with no refs is not an error. */
|
|
53
|
+
export declare function fetchMachineRefs(root: string, opts?: {
|
|
49
54
|
sync?: boolean;
|
|
50
55
|
}): {
|
|
51
56
|
ok: boolean;
|
|
52
57
|
error?: string;
|
|
53
58
|
};
|
|
54
|
-
/** Read every locally-known
|
|
55
|
-
export declare function
|
|
59
|
+
/** Read every locally-known ref under `prefix` → [{machine, message}]. */
|
|
60
|
+
export declare function readMachineRefs(root: string, prefix?: HarneryRefPrefix): Array<{
|
|
56
61
|
machine: string;
|
|
57
62
|
message: string;
|
|
58
63
|
}>;
|
|
@@ -61,8 +66,9 @@ export declare function readPresenceRefs(root: string): Array<{
|
|
|
61
66
|
* Records are %01-terminated (contents may contain newlines); refname and
|
|
62
67
|
* contents are %00-separated. Exported for unit tests.
|
|
63
68
|
*/
|
|
64
|
-
export declare function parseForEachRefOutput(out: string): Array<{
|
|
69
|
+
export declare function parseForEachRefOutput(out: string, prefix?: HarneryRefPrefix): Array<{
|
|
65
70
|
machine: string;
|
|
66
71
|
message: string;
|
|
67
72
|
}>;
|
|
73
|
+
export {};
|
|
68
74
|
//# sourceMappingURL=git.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../../src/core/presence/git.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../../src/core/presence/git.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH;;8EAE8E;AAC9E,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAC5D,eAAO,MAAM,eAAe,uBAAuB,CAAC;AACpD,KAAK,gBAAgB,GAAG,OAAO,mBAAmB,GAAG,OAAO,eAAe,CAAC;AAwC5E,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIrD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASzD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS1D;AAED;2EAC2E;AAC3E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOhF;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC5B;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAGjC;AAED;6EAC6E;AAC7E,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC5B;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAGjC;AAwBD,0EAA0E;AAC1E,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,gBAAsC,GAC7C,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAI7C;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,gBAAsC,GAC7C,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAe7C"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Git plumbing for the cross-machine presence transport (ADR 0016)
|
|
2
|
+
* Git plumbing for the cross-machine presence transport (ADR 0016) and the
|
|
3
|
+
* mail refs that ride on it (ADR 0222).
|
|
3
4
|
*
|
|
4
5
|
* Presence blobs travel as parentless commits (empty tree, JSON message)
|
|
5
6
|
* force-pushed to `refs/harnery/presence/<machine>` on the host repo's own
|
|
@@ -13,9 +14,11 @@
|
|
|
13
14
|
* user wants to see errors).
|
|
14
15
|
*/
|
|
15
16
|
import { spawn, spawnSync } from "node:child_process";
|
|
16
|
-
/** The ref
|
|
17
|
-
* publisher is structurally unable to touch refs/heads/* (ADR 0016).
|
|
17
|
+
/** The ref namespaces. Hardcoded — never interpolated from config — so the
|
|
18
|
+
* publisher is structurally unable to touch refs/heads/* (ADR 0016). Mail
|
|
19
|
+
* refs carry cross-machine peer messages on the same transport (ADR 0222). */
|
|
18
20
|
export const PRESENCE_REF_PREFIX = "refs/harnery/presence/";
|
|
21
|
+
export const MAIL_REF_PREFIX = "refs/harnery/mail/";
|
|
19
22
|
/** Well-known SHA of git's empty tree; `git mktree` below guarantees the
|
|
20
23
|
* object exists in the local odb before we build a commit on it. */
|
|
21
24
|
const EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
@@ -99,36 +102,31 @@ export function writePresenceCommit(root, message) {
|
|
|
99
102
|
return r.ok && r.stdout.trim() ? r.stdout.trim() : null;
|
|
100
103
|
}
|
|
101
104
|
/**
|
|
102
|
-
* Force-push a
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
+
* Force-push a commit to `<prefix><machine>` (presence or mail). `sync: true`
|
|
106
|
+
* (CLI/tests) waits and reports; default is detached fire-and-forget (hook
|
|
107
|
+
* path — a slow or credential-less push must not block a turn).
|
|
105
108
|
*/
|
|
106
|
-
export function
|
|
107
|
-
const refspec = `+${sha}:${
|
|
108
|
-
|
|
109
|
-
if (opts.sync) {
|
|
110
|
-
const r = runGit(root, args.slice(2));
|
|
111
|
-
return r.ok ? { ok: true } : { ok: false, error: r.stderr.trim().slice(0, 300) };
|
|
112
|
-
}
|
|
113
|
-
try {
|
|
114
|
-
const child = spawn("git", args, { detached: true, stdio: "ignore", env: gitEnv() });
|
|
115
|
-
child.unref();
|
|
116
|
-
return { ok: true };
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return { ok: false, error: "spawn failed" };
|
|
120
|
-
}
|
|
109
|
+
export function pushMachineRef(root, prefix, machine, sha, opts = {}) {
|
|
110
|
+
const refspec = `+${sha}:${prefix}${sanitizeRefComponent(machine)}`;
|
|
111
|
+
return runNetworkGit(root, ["push", "--quiet", "--no-verify", "origin", refspec], opts);
|
|
121
112
|
}
|
|
122
|
-
/** Fetch every peer presence ref from origin into the local ref
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
113
|
+
/** Fetch every peer presence and mail ref from origin into the local ref
|
|
114
|
+
* namespace, in one round trip. A namespace with no refs is not an error. */
|
|
115
|
+
export function fetchMachineRefs(root, opts = {}) {
|
|
116
|
+
const refspecs = [PRESENCE_REF_PREFIX, MAIL_REF_PREFIX].map((p) => `+${p}*:${p}*`);
|
|
117
|
+
return runNetworkGit(root, ["fetch", "--quiet", "--no-tags", "origin", ...refspecs], opts);
|
|
118
|
+
}
|
|
119
|
+
function runNetworkGit(root, args, opts) {
|
|
126
120
|
if (opts.sync) {
|
|
127
|
-
const r = runGit(root, args
|
|
121
|
+
const r = runGit(root, args);
|
|
128
122
|
return r.ok ? { ok: true } : { ok: false, error: r.stderr.trim().slice(0, 300) };
|
|
129
123
|
}
|
|
130
124
|
try {
|
|
131
|
-
const child = spawn("git",
|
|
125
|
+
const child = spawn("git", ["-C", root, ...args], {
|
|
126
|
+
detached: true,
|
|
127
|
+
stdio: "ignore",
|
|
128
|
+
env: gitEnv(),
|
|
129
|
+
});
|
|
132
130
|
child.unref();
|
|
133
131
|
return { ok: true };
|
|
134
132
|
}
|
|
@@ -136,23 +134,19 @@ export function fetchPresenceRefs(root, opts = {}) {
|
|
|
136
134
|
return { ok: false, error: "spawn failed" };
|
|
137
135
|
}
|
|
138
136
|
}
|
|
139
|
-
/** Read every locally-known
|
|
140
|
-
export function
|
|
141
|
-
const r = runGit(root, [
|
|
142
|
-
"for-each-ref",
|
|
143
|
-
"--format=%(refname)%00%(contents)%01",
|
|
144
|
-
`${PRESENCE_REF_PREFIX}*`,
|
|
145
|
-
]);
|
|
137
|
+
/** Read every locally-known ref under `prefix` → [{machine, message}]. */
|
|
138
|
+
export function readMachineRefs(root, prefix = PRESENCE_REF_PREFIX) {
|
|
139
|
+
const r = runGit(root, ["for-each-ref", "--format=%(refname)%00%(contents)%01", `${prefix}*`]);
|
|
146
140
|
if (!r.ok)
|
|
147
141
|
return [];
|
|
148
|
-
return parseForEachRefOutput(r.stdout);
|
|
142
|
+
return parseForEachRefOutput(r.stdout, prefix);
|
|
149
143
|
}
|
|
150
144
|
/**
|
|
151
145
|
* Parse `for-each-ref --format='%(refname)%00%(contents)%01'` output.
|
|
152
146
|
* Records are %01-terminated (contents may contain newlines); refname and
|
|
153
147
|
* contents are %00-separated. Exported for unit tests.
|
|
154
148
|
*/
|
|
155
|
-
export function parseForEachRefOutput(out) {
|
|
149
|
+
export function parseForEachRefOutput(out, prefix = PRESENCE_REF_PREFIX) {
|
|
156
150
|
const rows = [];
|
|
157
151
|
for (const rec of out.split("\u0001")) {
|
|
158
152
|
const trimmed = rec.replace(/^\n+/, "");
|
|
@@ -162,10 +156,10 @@ export function parseForEachRefOutput(out) {
|
|
|
162
156
|
if (sep === -1)
|
|
163
157
|
continue;
|
|
164
158
|
const refname = trimmed.slice(0, sep).trim();
|
|
165
|
-
if (!refname.startsWith(
|
|
159
|
+
if (!refname.startsWith(prefix))
|
|
166
160
|
continue;
|
|
167
161
|
rows.push({
|
|
168
|
-
machine: refname.slice(
|
|
162
|
+
machine: refname.slice(prefix.length),
|
|
169
163
|
message: trimmed.slice(sep + 1).trim(),
|
|
170
164
|
});
|
|
171
165
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { type PresenceAgent } from "./blob.js";
|
|
18
18
|
export type { PresenceAgent, PresenceBlob } from "./blob.js";
|
|
19
19
|
export { buildPresenceBlob } from "./blob.js";
|
|
20
|
-
export { PRESENCE_REF_PREFIX, parseForEachRefOutput, sanitizeRefComponent } from "./git.js";
|
|
20
|
+
export { MAIL_REF_PREFIX, PRESENCE_REF_PREFIX, parseForEachRefOutput, sanitizeRefComponent, } from "./git.js";
|
|
21
21
|
export { ensureRelayDaemon, relayDaemonStatus, runRelayDaemon } from "./relay-client.js";
|
|
22
22
|
export type PublishResult = {
|
|
23
23
|
status: "published";
|
|
@@ -49,7 +49,7 @@ export declare function publishPresence(coordRoot: string, opts?: {
|
|
|
49
49
|
force?: boolean;
|
|
50
50
|
sync?: boolean;
|
|
51
51
|
}): PublishResult;
|
|
52
|
-
/** Throttled fetch of peer presence refs from origin. */
|
|
52
|
+
/** Throttled fetch of peer presence and mail refs from origin. */
|
|
53
53
|
export declare function fetchPresence(coordRoot: string, opts?: {
|
|
54
54
|
force?: boolean;
|
|
55
55
|
sync?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/presence/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAgBH,OAAO,EAAqB,KAAK,aAAa,EAAqB,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/presence/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAgBH,OAAO,EAAqB,KAAK,aAAa,EAAqB,MAAM,WAAW,CAAC;AAWrF,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAYzF,MAAM,MAAM,aAAa,GACrB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACrE;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC,MAAM,MAAM,WAAW,GACnB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAwCvC;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7C,aAAa,CA6Cf;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7C,WAAW,CAsCb;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GAC3D,aAAa,EAAE,CAuDjB"}
|
|
@@ -20,9 +20,9 @@ import { coordEnv } from "../../lib/env.js";
|
|
|
20
20
|
import { resolveMachineLabel } from "../../lib/machine.js";
|
|
21
21
|
import { presenceEnabled } from "../config.js";
|
|
22
22
|
import { buildPresenceBlob } from "./blob.js";
|
|
23
|
-
import {
|
|
23
|
+
import { fetchMachineRefs, hasOrigin, PRESENCE_REF_PREFIX, pushMachineRef, readMachineRefs, sanitizeRefComponent, writePresenceCommit, } from "./git.js";
|
|
24
24
|
export { buildPresenceBlob } from "./blob.js";
|
|
25
|
-
export { PRESENCE_REF_PREFIX, parseForEachRefOutput, sanitizeRefComponent } from "./git.js";
|
|
25
|
+
export { MAIL_REF_PREFIX, PRESENCE_REF_PREFIX, parseForEachRefOutput, sanitizeRefComponent, } from "./git.js";
|
|
26
26
|
export { ensureRelayDaemon, relayDaemonStatus, runRelayDaemon } from "./relay-client.js";
|
|
27
27
|
/** Re-push at least this often while sessions are live, so peers can treat a
|
|
28
28
|
* blob older than ~3x this as "machine offline" rather than "no change". */
|
|
@@ -95,7 +95,9 @@ export function publishPresence(coordRoot, opts = {}) {
|
|
|
95
95
|
const sha = writePresenceCommit(coordRoot, json);
|
|
96
96
|
if (!sha)
|
|
97
97
|
return { status: "error", error: "commit-tree failed" };
|
|
98
|
-
const push =
|
|
98
|
+
const push = pushMachineRef(coordRoot, PRESENCE_REF_PREFIX, blob.machine, sha, {
|
|
99
|
+
sync: opts.sync,
|
|
100
|
+
});
|
|
99
101
|
if (!push.ok)
|
|
100
102
|
return { status: "error", error: push.error ?? "push failed" };
|
|
101
103
|
// Optimistic on the detached path: a failed background push self-heals at
|
|
@@ -111,7 +113,7 @@ export function publishPresence(coordRoot, opts = {}) {
|
|
|
111
113
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
|
-
/** Throttled fetch of peer presence refs from origin. */
|
|
116
|
+
/** Throttled fetch of peer presence and mail refs from origin. */
|
|
115
117
|
export function fetchPresence(coordRoot, opts = {}) {
|
|
116
118
|
try {
|
|
117
119
|
if (!presenceEnabled(coordRoot))
|
|
@@ -144,7 +146,7 @@ export function fetchPresence(coordRoot, opts = {}) {
|
|
|
144
146
|
catch {
|
|
145
147
|
/* best-effort */
|
|
146
148
|
}
|
|
147
|
-
const r =
|
|
149
|
+
const r = fetchMachineRefs(coordRoot, { sync: opts.sync });
|
|
148
150
|
if (!r.ok)
|
|
149
151
|
return { status: "error", error: r.error ?? "fetch failed" };
|
|
150
152
|
return { status: "fetched" };
|
|
@@ -178,7 +180,7 @@ export function readRemoteMachines(coordRoot, opts = {}) {
|
|
|
178
180
|
return;
|
|
179
181
|
byMachine.set(machine, blob);
|
|
180
182
|
};
|
|
181
|
-
for (const { machine, message } of
|
|
183
|
+
for (const { machine, message } of readMachineRefs(coordRoot, PRESENCE_REF_PREFIX)) {
|
|
182
184
|
try {
|
|
183
185
|
consider(machine, JSON.parse(message));
|
|
184
186
|
}
|