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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-machine peer messages (ADR 0222). A ping whose recipient lives on
|
|
3
|
+
* another machine is queued here and published as part of this machine's
|
|
4
|
+
* mail ref, `refs/harnery/mail/<machine>`, on the same origin the presence
|
|
5
|
+
* refs use. Each machine writes only its own ref, so force-pushing it never
|
|
6
|
+
* races another writer.
|
|
7
|
+
*
|
|
8
|
+
* The ref is a parentless commit whose message is one JSON blob:
|
|
9
|
+
* outbox — messages this machine sent that the recipient machine has not
|
|
10
|
+
* acknowledged yet;
|
|
11
|
+
* acks — ids of messages this machine imported from other machines.
|
|
12
|
+
*
|
|
13
|
+
* The recipient machine imports envelopes addressed to it into the ordinary
|
|
14
|
+
* per-instance inbox (keeping the sender's message id, so a re-import is a
|
|
15
|
+
* no-op) and acknowledges them in its own ref. The sender drops an
|
|
16
|
+
* acknowledged message from its outbox; the recipient stops acknowledging
|
|
17
|
+
* once the sender's ref no longer lists it. Delivery is therefore
|
|
18
|
+
* at-least-once into a deduplicating inbox.
|
|
19
|
+
*
|
|
20
|
+
* Local state is one append-only log, `.harnery/mail/log.jsonl`, updated
|
|
21
|
+
* under the durable-history lease so concurrent hooks on one machine agree.
|
|
22
|
+
* Repo access is the trust boundary, exactly as for presence: anyone who can
|
|
23
|
+
* push to origin can publish a mail ref.
|
|
24
|
+
*/
|
|
25
|
+
import type { HarneryInboxService } from "../inbox/service.js";
|
|
26
|
+
export declare const HARNERY_MAIL_ENVELOPE_SCHEMA: "harnery.mail-envelope/v1";
|
|
27
|
+
/** An unacknowledged message stops being offered after this long. */
|
|
28
|
+
export declare const MAIL_TTL_MS: number;
|
|
29
|
+
export interface HarneryMailEnvelopeV1 {
|
|
30
|
+
schema: typeof HARNERY_MAIL_ENVELOPE_SCHEMA;
|
|
31
|
+
message_id: string;
|
|
32
|
+
from_machine: string;
|
|
33
|
+
to_machine: string;
|
|
34
|
+
sender_instance_id: string | null;
|
|
35
|
+
sender_display_name: string;
|
|
36
|
+
recipient_instance_id: string;
|
|
37
|
+
recipient_display_name: string;
|
|
38
|
+
created_at: string;
|
|
39
|
+
body: string;
|
|
40
|
+
body_bytes: number;
|
|
41
|
+
}
|
|
42
|
+
export interface HarneryMailBlobV1 {
|
|
43
|
+
v: 1;
|
|
44
|
+
machine: string;
|
|
45
|
+
published_at: string;
|
|
46
|
+
outbox: HarneryMailEnvelopeV1[];
|
|
47
|
+
acks: string[];
|
|
48
|
+
}
|
|
49
|
+
/** This machine's mail identity: the same sanitized label as its ref. */
|
|
50
|
+
export declare function mailMachine(): string;
|
|
51
|
+
export interface QueueMailInput {
|
|
52
|
+
messageId: string;
|
|
53
|
+
replayOnly?: boolean;
|
|
54
|
+
toMachine: string;
|
|
55
|
+
senderInstanceId: string | null;
|
|
56
|
+
senderName: string;
|
|
57
|
+
recipientInstanceId: string;
|
|
58
|
+
recipientName: string;
|
|
59
|
+
body: string;
|
|
60
|
+
maxBodyBytes: number;
|
|
61
|
+
now?: Date;
|
|
62
|
+
}
|
|
63
|
+
export interface HarneryMailQueueReceipt {
|
|
64
|
+
envelope: HarneryMailEnvelopeV1;
|
|
65
|
+
state: "queued" | "duplicate";
|
|
66
|
+
delivery_state: "pending" | "delivered" | "expired";
|
|
67
|
+
synced: true;
|
|
68
|
+
}
|
|
69
|
+
/** Queue one message for another machine. The caller publishes afterwards. */
|
|
70
|
+
export declare function queueMail(coordRoot: string, input: QueueMailInput): HarneryMailQueueReceipt;
|
|
71
|
+
export type MailPublishResult = {
|
|
72
|
+
status: "published";
|
|
73
|
+
machine: string;
|
|
74
|
+
outbox: number;
|
|
75
|
+
acks: number;
|
|
76
|
+
sha: string;
|
|
77
|
+
} | {
|
|
78
|
+
status: "skipped";
|
|
79
|
+
reason: string;
|
|
80
|
+
} | {
|
|
81
|
+
status: "error";
|
|
82
|
+
error: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Publish this machine's mail ref when its content changed, or as a keepalive
|
|
86
|
+
* while messages are still unacknowledged. A machine that never sent or
|
|
87
|
+
* received cross-machine mail never creates the ref.
|
|
88
|
+
*/
|
|
89
|
+
export declare function publishMail(coordRoot: string, opts?: {
|
|
90
|
+
force?: boolean;
|
|
91
|
+
sync?: boolean;
|
|
92
|
+
now?: Date;
|
|
93
|
+
}): MailPublishResult;
|
|
94
|
+
export interface MailImportResult {
|
|
95
|
+
received: number;
|
|
96
|
+
delivered: number;
|
|
97
|
+
released: number;
|
|
98
|
+
rejected: number;
|
|
99
|
+
published?: MailPublishResult["status"];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Apply every locally-fetched peer mail ref: import envelopes addressed to
|
|
103
|
+
* this machine, record acknowledgements of this machine's outbox, and stop
|
|
104
|
+
* acknowledging messages their sender has dropped. Reads only local refs;
|
|
105
|
+
* `fetchPresence` brings them in. Never throws.
|
|
106
|
+
*/
|
|
107
|
+
export declare function importMail(coordRoot: string, inbox: HarneryInboxService, opts?: {
|
|
108
|
+
now?: Date;
|
|
109
|
+
publish?: boolean;
|
|
110
|
+
throttle?: boolean;
|
|
111
|
+
}): MailImportResult;
|
|
112
|
+
export interface RemoteSender {
|
|
113
|
+
instance_id: string;
|
|
114
|
+
name: string;
|
|
115
|
+
machine: string;
|
|
116
|
+
}
|
|
117
|
+
/** Senders this machine has received mail from, so a reply can find them even
|
|
118
|
+
* after their machine's presence goes quiet. */
|
|
119
|
+
export declare function knownRemoteSenders(coordRoot: string): RemoteSender[];
|
|
120
|
+
export interface MailStatus {
|
|
121
|
+
machine: string;
|
|
122
|
+
outbox: Array<{
|
|
123
|
+
message_id: string;
|
|
124
|
+
to_machine: string;
|
|
125
|
+
recipient: string;
|
|
126
|
+
queued_at: string;
|
|
127
|
+
}>;
|
|
128
|
+
acks: number;
|
|
129
|
+
delivered: number;
|
|
130
|
+
expired: number;
|
|
131
|
+
}
|
|
132
|
+
export declare function mailStatus(coordRoot: string): MailStatus;
|
|
133
|
+
//# sourceMappingURL=mail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail.d.ts","sourceRoot":"","sources":["../../../src/core/presence/mail.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-machine peer messages (ADR 0222). A ping whose recipient lives on
|
|
3
|
+
* another machine is queued here and published as part of this machine's
|
|
4
|
+
* mail ref, `refs/harnery/mail/<machine>`, on the same origin the presence
|
|
5
|
+
* refs use. Each machine writes only its own ref, so force-pushing it never
|
|
6
|
+
* races another writer.
|
|
7
|
+
*
|
|
8
|
+
* The ref is a parentless commit whose message is one JSON blob:
|
|
9
|
+
* outbox — messages this machine sent that the recipient machine has not
|
|
10
|
+
* acknowledged yet;
|
|
11
|
+
* acks — ids of messages this machine imported from other machines.
|
|
12
|
+
*
|
|
13
|
+
* The recipient machine imports envelopes addressed to it into the ordinary
|
|
14
|
+
* per-instance inbox (keeping the sender's message id, so a re-import is a
|
|
15
|
+
* no-op) and acknowledges them in its own ref. The sender drops an
|
|
16
|
+
* acknowledged message from its outbox; the recipient stops acknowledging
|
|
17
|
+
* once the sender's ref no longer lists it. Delivery is therefore
|
|
18
|
+
* at-least-once into a deduplicating inbox.
|
|
19
|
+
*
|
|
20
|
+
* Local state is one append-only log, `.harnery/mail/log.jsonl`, updated
|
|
21
|
+
* under the durable-history lease so concurrent hooks on one machine agree.
|
|
22
|
+
* Repo access is the trust boundary, exactly as for presence: anyone who can
|
|
23
|
+
* push to origin can publish a mail ref.
|
|
24
|
+
*/
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, utimesSync, writeFileSync, } from "node:fs";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { resolveMachineLabel } from "../../lib/machine.js";
|
|
29
|
+
import { presenceEnabled } from "../config.js";
|
|
30
|
+
import { HARNERY_INBOX_RECORD_SCHEMA, HarneryInboxCapacityError, HarneryInboxMessageConflictError, HarneryInboxMessageHistoryMissingError, } from "../inbox/contract.js";
|
|
31
|
+
import { readCrashSafeJsonlFile, updateCrashSafeJsonlFile } from "../storage/durable-history.js";
|
|
32
|
+
import { hasOrigin, MAIL_REF_PREFIX, pushMachineRef, readMachineRefs, sanitizeRefComponent, writePresenceCommit, } from "./git.js";
|
|
33
|
+
export const HARNERY_MAIL_ENVELOPE_SCHEMA = "harnery.mail-envelope/v1";
|
|
34
|
+
const MAIL_LOG_SCHEMA = "harnery.mail-log/v1";
|
|
35
|
+
/** An unacknowledged message stops being offered after this long. */
|
|
36
|
+
export const MAIL_TTL_MS = 14 * 24 * 60 * 60_000;
|
|
37
|
+
/** While a message is still unacknowledged, re-push at least this often, so a
|
|
38
|
+
* detached push that failed heals without anyone noticing. */
|
|
39
|
+
const MAIL_KEEPALIVE_SECS = 600;
|
|
40
|
+
const MAX_OUTBOX = 100;
|
|
41
|
+
/** Hook-driven imports run at most this often (the presence fetch is 60s). */
|
|
42
|
+
const IMPORT_INTERVAL_SECS = 30;
|
|
43
|
+
const MAX_LOG_RECORDS = 20_000;
|
|
44
|
+
const MAX_LOG_RECORD_BYTES = 16_384;
|
|
45
|
+
/** This machine's mail identity: the same sanitized label as its ref. */
|
|
46
|
+
export function mailMachine() {
|
|
47
|
+
return sanitizeRefComponent(resolveMachineLabel());
|
|
48
|
+
}
|
|
49
|
+
function mailDir(coordRoot) {
|
|
50
|
+
return join(coordRoot, ".harnery", "mail");
|
|
51
|
+
}
|
|
52
|
+
function logPath(coordRoot) {
|
|
53
|
+
return join(mailDir(coordRoot), "log.jsonl");
|
|
54
|
+
}
|
|
55
|
+
const LOG_OPTIONS = { max_record_bytes: MAX_LOG_RECORD_BYTES, max_records: MAX_LOG_RECORDS };
|
|
56
|
+
function stamp(now) {
|
|
57
|
+
return now.toISOString();
|
|
58
|
+
}
|
|
59
|
+
function projectMail(records) {
|
|
60
|
+
const sent = new Map();
|
|
61
|
+
const outbound = new Map();
|
|
62
|
+
const acks = new Map();
|
|
63
|
+
const imported = new Set();
|
|
64
|
+
const senders = new Map();
|
|
65
|
+
let delivered = 0;
|
|
66
|
+
let expired = 0;
|
|
67
|
+
for (const record of records) {
|
|
68
|
+
if (record?.schema !== MAIL_LOG_SCHEMA)
|
|
69
|
+
continue;
|
|
70
|
+
switch (record.kind) {
|
|
71
|
+
case "outbound":
|
|
72
|
+
outbound.set(record.envelope.message_id, { at: record.at, envelope: record.envelope });
|
|
73
|
+
sent.set(record.envelope.message_id, {
|
|
74
|
+
envelope: record.envelope,
|
|
75
|
+
delivery_state: "pending",
|
|
76
|
+
});
|
|
77
|
+
break;
|
|
78
|
+
case "delivered":
|
|
79
|
+
if (outbound.delete(record.message_id))
|
|
80
|
+
delivered++;
|
|
81
|
+
if (sent.has(record.message_id))
|
|
82
|
+
sent.get(record.message_id).delivery_state = "delivered";
|
|
83
|
+
break;
|
|
84
|
+
case "expired":
|
|
85
|
+
if (outbound.delete(record.message_id))
|
|
86
|
+
expired++;
|
|
87
|
+
if (sent.has(record.message_id))
|
|
88
|
+
sent.get(record.message_id).delivery_state = "expired";
|
|
89
|
+
break;
|
|
90
|
+
case "imported":
|
|
91
|
+
imported.add(record.message_id);
|
|
92
|
+
acks.set(record.message_id, { at: record.at, from_machine: record.from_machine });
|
|
93
|
+
if (record.sender_instance_id) {
|
|
94
|
+
senders.set(record.sender_instance_id, {
|
|
95
|
+
name: record.sender_display_name,
|
|
96
|
+
machine: record.from_machine,
|
|
97
|
+
at: record.at,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
case "released":
|
|
102
|
+
acks.delete(record.message_id);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { sent, outbox: [...outbound.values()], acks, imported, senders, delivered, expired };
|
|
107
|
+
}
|
|
108
|
+
function readMailLog(coordRoot) {
|
|
109
|
+
try {
|
|
110
|
+
return readCrashSafeJsonlFile(logPath(coordRoot), LOG_OPTIONS);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function updateMailLog(coordRoot, update) {
|
|
117
|
+
mkdirSync(mailDir(coordRoot), { recursive: true });
|
|
118
|
+
return updateCrashSafeJsonlFile(logPath(coordRoot), LOG_OPTIONS, (records) => update(projectMail(records)));
|
|
119
|
+
}
|
|
120
|
+
/** Queue one message for another machine. The caller publishes afterwards. */
|
|
121
|
+
export function queueMail(coordRoot, input) {
|
|
122
|
+
if (input.replayOnly !== undefined && typeof input.replayOnly !== "boolean") {
|
|
123
|
+
throw new Error("invalid replayOnly flag");
|
|
124
|
+
}
|
|
125
|
+
if (typeof input.messageId !== "string" ||
|
|
126
|
+
!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(input.messageId)) {
|
|
127
|
+
throw new Error("invalid message id");
|
|
128
|
+
}
|
|
129
|
+
const bodyBytes = Buffer.byteLength(input.body);
|
|
130
|
+
if (bodyBytes === 0 || bodyBytes > input.maxBodyBytes) {
|
|
131
|
+
throw new HarneryInboxCapacityError("message_body_limit", bodyBytes, input.maxBodyBytes);
|
|
132
|
+
}
|
|
133
|
+
const now = input.now ?? new Date();
|
|
134
|
+
const envelope = {
|
|
135
|
+
schema: HARNERY_MAIL_ENVELOPE_SCHEMA,
|
|
136
|
+
message_id: input.messageId,
|
|
137
|
+
from_machine: mailMachine(),
|
|
138
|
+
to_machine: sanitizeRefComponent(input.toMachine),
|
|
139
|
+
sender_instance_id: input.senderInstanceId,
|
|
140
|
+
sender_display_name: input.senderName,
|
|
141
|
+
recipient_instance_id: input.recipientInstanceId,
|
|
142
|
+
recipient_display_name: input.recipientName,
|
|
143
|
+
created_at: stamp(now),
|
|
144
|
+
body: input.body,
|
|
145
|
+
body_bytes: bodyBytes,
|
|
146
|
+
};
|
|
147
|
+
return updateMailLog(coordRoot, (projection) => {
|
|
148
|
+
const existing = projection.sent.get(envelope.message_id);
|
|
149
|
+
if (existing) {
|
|
150
|
+
const fields = [
|
|
151
|
+
"from_machine",
|
|
152
|
+
"to_machine",
|
|
153
|
+
"sender_instance_id",
|
|
154
|
+
"sender_display_name",
|
|
155
|
+
"recipient_instance_id",
|
|
156
|
+
"recipient_display_name",
|
|
157
|
+
"body",
|
|
158
|
+
"body_bytes",
|
|
159
|
+
];
|
|
160
|
+
if (fields.some((field) => existing.envelope[field] !== envelope[field])) {
|
|
161
|
+
throw new HarneryInboxMessageConflictError(envelope.message_id);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
result: {
|
|
165
|
+
envelope: existing.envelope,
|
|
166
|
+
state: "duplicate",
|
|
167
|
+
delivery_state: existing.delivery_state,
|
|
168
|
+
synced: true,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (input.replayOnly) {
|
|
173
|
+
throw new HarneryInboxMessageHistoryMissingError(envelope.message_id);
|
|
174
|
+
}
|
|
175
|
+
if (projection.outbox.length >= MAX_OUTBOX) {
|
|
176
|
+
throw new HarneryInboxCapacityError("pending_count_limit", projection.outbox.length, MAX_OUTBOX);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
append: [{ schema: MAIL_LOG_SCHEMA, kind: "outbound", at: stamp(now), envelope }],
|
|
180
|
+
result: { envelope, state: "queued", delivery_state: "pending", synced: true },
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function statePath(coordRoot) {
|
|
185
|
+
return join(mailDir(coordRoot), "publish-state.json");
|
|
186
|
+
}
|
|
187
|
+
function readState(coordRoot) {
|
|
188
|
+
try {
|
|
189
|
+
return JSON.parse(readFileSync(statePath(coordRoot), "utf8"));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return {};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function writeState(coordRoot, state) {
|
|
196
|
+
try {
|
|
197
|
+
mkdirSync(mailDir(coordRoot), { recursive: true });
|
|
198
|
+
const p = statePath(coordRoot);
|
|
199
|
+
const tmp = `${p}.tmp.${process.pid}`;
|
|
200
|
+
writeFileSync(tmp, JSON.stringify(state), "utf8");
|
|
201
|
+
renameSync(tmp, p);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
/* best-effort: the keepalive re-push covers a lost state write */
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** Expire stale outbound mail, then return the blob this machine should publish. */
|
|
208
|
+
function currentBlob(coordRoot, now) {
|
|
209
|
+
const projection = updateMailLog(coordRoot, (p) => {
|
|
210
|
+
const cutoff = now.getTime() - MAIL_TTL_MS;
|
|
211
|
+
const stale = p.outbox.filter(({ at }) => Date.parse(at) < cutoff);
|
|
212
|
+
return {
|
|
213
|
+
append: stale.map(({ envelope }) => ({
|
|
214
|
+
schema: MAIL_LOG_SCHEMA,
|
|
215
|
+
kind: "expired",
|
|
216
|
+
at: stamp(now),
|
|
217
|
+
message_id: envelope.message_id,
|
|
218
|
+
})),
|
|
219
|
+
result: { ...p, outbox: p.outbox.filter(({ at }) => Date.parse(at) >= cutoff) },
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
v: 1,
|
|
224
|
+
machine: mailMachine(),
|
|
225
|
+
published_at: stamp(now),
|
|
226
|
+
outbox: projection.outbox.map(({ envelope }) => envelope),
|
|
227
|
+
acks: [...projection.acks.keys()].sort(),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Publish this machine's mail ref when its content changed, or as a keepalive
|
|
232
|
+
* while messages are still unacknowledged. A machine that never sent or
|
|
233
|
+
* received cross-machine mail never creates the ref.
|
|
234
|
+
*/
|
|
235
|
+
export function publishMail(coordRoot, opts = {}) {
|
|
236
|
+
try {
|
|
237
|
+
if (!presenceEnabled(coordRoot))
|
|
238
|
+
return { status: "skipped", reason: "disabled" };
|
|
239
|
+
// A machine that never sent or received cross-machine mail has no log;
|
|
240
|
+
// the hook path stops here without spawning git.
|
|
241
|
+
if (!existsSync(logPath(coordRoot)))
|
|
242
|
+
return { status: "skipped", reason: "no mail" };
|
|
243
|
+
if (!hasOrigin(coordRoot))
|
|
244
|
+
return { status: "skipped", reason: "no origin remote" };
|
|
245
|
+
const now = opts.now ?? new Date();
|
|
246
|
+
const blob = currentBlob(coordRoot, now);
|
|
247
|
+
const empty = blob.outbox.length === 0 && blob.acks.length === 0;
|
|
248
|
+
const hash = createHash("sha256")
|
|
249
|
+
.update(JSON.stringify([blob.outbox.map((e) => e.message_id), blob.acks]))
|
|
250
|
+
.digest("hex");
|
|
251
|
+
const state = readState(coordRoot);
|
|
252
|
+
if (!opts.force) {
|
|
253
|
+
if (empty && (state.empty ?? true))
|
|
254
|
+
return { status: "skipped", reason: "nothing to carry" };
|
|
255
|
+
const pushedAt = Date.parse(state.pushed_at ?? "");
|
|
256
|
+
const sincePush = Number.isFinite(pushedAt)
|
|
257
|
+
? (now.getTime() - pushedAt) / 1000
|
|
258
|
+
: Number.POSITIVE_INFINITY;
|
|
259
|
+
if (state.hash === hash && (blob.outbox.length === 0 || sincePush < MAIL_KEEPALIVE_SECS)) {
|
|
260
|
+
return { status: "skipped", reason: "unchanged" };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const sha = writePresenceCommit(coordRoot, JSON.stringify(blob));
|
|
264
|
+
if (!sha)
|
|
265
|
+
return { status: "error", error: "commit-tree failed" };
|
|
266
|
+
const push = pushMachineRef(coordRoot, MAIL_REF_PREFIX, blob.machine, sha, {
|
|
267
|
+
sync: opts.sync,
|
|
268
|
+
});
|
|
269
|
+
if (!push.ok)
|
|
270
|
+
return { status: "error", error: push.error ?? "push failed" };
|
|
271
|
+
writeState(coordRoot, { hash, pushed_at: stamp(now), empty });
|
|
272
|
+
return {
|
|
273
|
+
status: "published",
|
|
274
|
+
machine: blob.machine,
|
|
275
|
+
outbox: blob.outbox.length,
|
|
276
|
+
acks: blob.acks.length,
|
|
277
|
+
sha,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function parseBlob(machine, message) {
|
|
285
|
+
try {
|
|
286
|
+
const blob = JSON.parse(message);
|
|
287
|
+
if (blob?.v !== 1 || sanitizeRefComponent(String(blob.machine)) !== machine)
|
|
288
|
+
return null;
|
|
289
|
+
if (!Number.isFinite(Date.parse(blob.published_at)))
|
|
290
|
+
return null;
|
|
291
|
+
if (!Array.isArray(blob.outbox) || !Array.isArray(blob.acks))
|
|
292
|
+
return null;
|
|
293
|
+
return blob;
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function validEnvelope(envelope, from, self) {
|
|
300
|
+
return (envelope?.schema === HARNERY_MAIL_ENVELOPE_SCHEMA &&
|
|
301
|
+
envelope.from_machine === from &&
|
|
302
|
+
envelope.to_machine === self &&
|
|
303
|
+
typeof envelope.message_id === "string" &&
|
|
304
|
+
typeof envelope.recipient_instance_id === "string" &&
|
|
305
|
+
typeof envelope.body === "string" &&
|
|
306
|
+
Buffer.byteLength(envelope.body) === envelope.body_bytes);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Apply every locally-fetched peer mail ref: import envelopes addressed to
|
|
310
|
+
* this machine, record acknowledgements of this machine's outbox, and stop
|
|
311
|
+
* acknowledging messages their sender has dropped. Reads only local refs;
|
|
312
|
+
* `fetchPresence` brings them in. Never throws.
|
|
313
|
+
*/
|
|
314
|
+
export function importMail(coordRoot, inbox, opts = {}) {
|
|
315
|
+
const result = { received: 0, delivered: 0, released: 0, rejected: 0 };
|
|
316
|
+
try {
|
|
317
|
+
if (!presenceEnabled(coordRoot))
|
|
318
|
+
return result;
|
|
319
|
+
const now = opts.now ?? new Date();
|
|
320
|
+
if (opts.throttle && !importDue(coordRoot, now))
|
|
321
|
+
return result;
|
|
322
|
+
const self = mailMachine();
|
|
323
|
+
const peers = readMachineRefs(coordRoot, MAIL_REF_PREFIX)
|
|
324
|
+
.filter(({ machine }) => machine !== self)
|
|
325
|
+
.map(({ machine, message }) => ({ machine, blob: parseBlob(machine, message) }))
|
|
326
|
+
.filter((peer) => peer.blob !== null);
|
|
327
|
+
if (peers.length === 0)
|
|
328
|
+
return result;
|
|
329
|
+
updateMailLog(coordRoot, (projection) => {
|
|
330
|
+
const append = [];
|
|
331
|
+
const pending = new Map(projection.outbox.map((o) => [o.envelope.message_id, o.envelope]));
|
|
332
|
+
for (const { machine, blob } of peers) {
|
|
333
|
+
const listed = new Set();
|
|
334
|
+
for (const envelope of blob.outbox) {
|
|
335
|
+
if (!validEnvelope(envelope, machine, self))
|
|
336
|
+
continue;
|
|
337
|
+
listed.add(envelope.message_id);
|
|
338
|
+
if (projection.imported.has(envelope.message_id))
|
|
339
|
+
continue;
|
|
340
|
+
if (!receive(inbox, envelope, result))
|
|
341
|
+
continue;
|
|
342
|
+
append.push({
|
|
343
|
+
schema: MAIL_LOG_SCHEMA,
|
|
344
|
+
kind: "imported",
|
|
345
|
+
at: stamp(now),
|
|
346
|
+
message_id: envelope.message_id,
|
|
347
|
+
from_machine: machine,
|
|
348
|
+
sender_instance_id: envelope.sender_instance_id,
|
|
349
|
+
sender_display_name: envelope.sender_display_name,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
for (const id of blob.acks) {
|
|
353
|
+
if (pending.get(id)?.to_machine !== machine)
|
|
354
|
+
continue;
|
|
355
|
+
pending.delete(id);
|
|
356
|
+
append.push({
|
|
357
|
+
schema: MAIL_LOG_SCHEMA,
|
|
358
|
+
kind: "delivered",
|
|
359
|
+
at: stamp(now),
|
|
360
|
+
message_id: id,
|
|
361
|
+
});
|
|
362
|
+
result.delivered++;
|
|
363
|
+
}
|
|
364
|
+
const publishedAt = Date.parse(blob.published_at);
|
|
365
|
+
for (const [id, ack] of projection.acks) {
|
|
366
|
+
if (ack.from_machine !== machine || listed.has(id))
|
|
367
|
+
continue;
|
|
368
|
+
if (publishedAt <= Date.parse(ack.at))
|
|
369
|
+
continue;
|
|
370
|
+
append.push({
|
|
371
|
+
schema: MAIL_LOG_SCHEMA,
|
|
372
|
+
kind: "released",
|
|
373
|
+
at: stamp(now),
|
|
374
|
+
message_id: id,
|
|
375
|
+
});
|
|
376
|
+
result.released++;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { append, result: undefined };
|
|
380
|
+
});
|
|
381
|
+
if (opts.publish !== false && result.received + result.delivered + result.released > 0) {
|
|
382
|
+
result.published = publishMail(coordRoot, { now }).status;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
/* mail must never break a hook */
|
|
387
|
+
}
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
/** Hooks import at most once per interval: refs only change when the
|
|
391
|
+
* throttled presence fetch lands, so checking every prompt spawns git for
|
|
392
|
+
* nothing. The stamp is touched first so concurrent hooks do not stampede. */
|
|
393
|
+
function importDue(coordRoot, now) {
|
|
394
|
+
const path = join(mailDir(coordRoot), "import-stamp");
|
|
395
|
+
try {
|
|
396
|
+
if ((now.getTime() - statSync(path).mtimeMs) / 1000 < IMPORT_INTERVAL_SECS)
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
/* no stamp yet → import */
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
mkdirSync(mailDir(coordRoot), { recursive: true });
|
|
404
|
+
writeFileSync(path, "", "utf8");
|
|
405
|
+
utimesSync(path, now, now);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
/* best-effort */
|
|
409
|
+
}
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
/** Inbox append for one envelope. A full inbox leaves the message unacknowledged
|
|
413
|
+
* so the sender keeps offering it; a malformed one is acknowledged and dropped. */
|
|
414
|
+
function receive(inbox, envelope, result) {
|
|
415
|
+
const record = {
|
|
416
|
+
schema: HARNERY_INBOX_RECORD_SCHEMA,
|
|
417
|
+
kind: "message",
|
|
418
|
+
message_id: envelope.message_id,
|
|
419
|
+
sender_instance_id: envelope.sender_instance_id,
|
|
420
|
+
sender_display_name: envelope.sender_display_name,
|
|
421
|
+
sender_machine: envelope.from_machine,
|
|
422
|
+
recipient_instance_id: envelope.recipient_instance_id,
|
|
423
|
+
recipient_display_name: envelope.recipient_display_name,
|
|
424
|
+
created_at: envelope.created_at,
|
|
425
|
+
body: envelope.body,
|
|
426
|
+
body_bytes: envelope.body_bytes,
|
|
427
|
+
};
|
|
428
|
+
try {
|
|
429
|
+
if (inbox.receive(record) === "received")
|
|
430
|
+
result.received++;
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
if (error instanceof HarneryInboxCapacityError && error.reason_code !== "message_body_limit") {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
result.rejected++;
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/** Senders this machine has received mail from, so a reply can find them even
|
|
442
|
+
* after their machine's presence goes quiet. */
|
|
443
|
+
export function knownRemoteSenders(coordRoot) {
|
|
444
|
+
return [...projectMail(readMailLog(coordRoot)).senders].map(([instance_id, s]) => ({
|
|
445
|
+
instance_id,
|
|
446
|
+
name: s.name,
|
|
447
|
+
machine: s.machine,
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
export function mailStatus(coordRoot) {
|
|
451
|
+
const projection = projectMail(readMailLog(coordRoot));
|
|
452
|
+
return {
|
|
453
|
+
machine: mailMachine(),
|
|
454
|
+
outbox: projection.outbox.map(({ at, envelope }) => ({
|
|
455
|
+
message_id: envelope.message_id,
|
|
456
|
+
to_machine: envelope.to_machine,
|
|
457
|
+
recipient: envelope.recipient_display_name,
|
|
458
|
+
queued_at: at,
|
|
459
|
+
})),
|
|
460
|
+
acks: projection.acks.size,
|
|
461
|
+
delivered: projection.delivered,
|
|
462
|
+
expired: projection.expired,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
@@ -265,7 +265,7 @@ export declare const SemanticMeaningV2Schema: import("@sinclair/typebox").TObjec
|
|
|
265
265
|
evidence_event_ids: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
|
|
266
266
|
}>>;
|
|
267
267
|
tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
268
|
-
value: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"recovery" | "research" | "waiting" | "planning" | "implementation" | "verification" | "coordination" | "
|
|
268
|
+
value: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"recovery" | "research" | "delivery" | "waiting" | "planning" | "implementation" | "verification" | "coordination" | "documentation" | "infrastructure">[]>>;
|
|
269
269
|
basis: import("@sinclair/typebox").TLiteral<"model-synthesis" | "prediction">;
|
|
270
270
|
confidence: import("@sinclair/typebox").TLiteral<"low"> | import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"high">, import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"low">]> | import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"low">]>;
|
|
271
271
|
evidence_event_ids: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
|
|
@@ -325,7 +325,7 @@ export declare const SemanticModelReplyV2Schema: import("@sinclair/typebox").TOb
|
|
|
325
325
|
evidence_event_ids: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
|
|
326
326
|
}>>;
|
|
327
327
|
tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
328
|
-
value: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"recovery" | "research" | "waiting" | "planning" | "implementation" | "verification" | "coordination" | "
|
|
328
|
+
value: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"recovery" | "research" | "delivery" | "waiting" | "planning" | "implementation" | "verification" | "coordination" | "documentation" | "infrastructure">[]>>;
|
|
329
329
|
basis: import("@sinclair/typebox").TLiteral<"model-synthesis" | "prediction">;
|
|
330
330
|
confidence: import("@sinclair/typebox").TLiteral<"low"> | import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"high">, import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"low">]> | import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"low">]>;
|
|
331
331
|
evidence_event_ids: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
|
|
@@ -391,7 +391,7 @@ export declare function semanticModelReplyV2SchemaFor(expected: {
|
|
|
391
391
|
evidence_event_ids: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<string>[]>>;
|
|
392
392
|
}>>;
|
|
393
393
|
tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
394
|
-
value: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"recovery" | "research" | "waiting" | "planning" | "implementation" | "verification" | "coordination" | "
|
|
394
|
+
value: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"recovery" | "research" | "delivery" | "waiting" | "planning" | "implementation" | "verification" | "coordination" | "documentation" | "infrastructure">[]>>;
|
|
395
395
|
basis: import("@sinclair/typebox").TLiteral<"model-synthesis" | "prediction">;
|
|
396
396
|
confidence: import("@sinclair/typebox").TLiteral<"low"> | import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"high">, import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"low">]> | import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"low">]>;
|
|
397
397
|
evidence_event_ids: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<string>[]>>;
|
|
@@ -516,7 +516,7 @@ export declare const SemanticAcceptedReadModelV2Schema: import("@sinclair/typebo
|
|
|
516
516
|
evidence_event_ids: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
|
|
517
517
|
}>>;
|
|
518
518
|
tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
519
|
-
value: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"recovery" | "research" | "waiting" | "planning" | "implementation" | "verification" | "coordination" | "
|
|
519
|
+
value: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"recovery" | "research" | "delivery" | "waiting" | "planning" | "implementation" | "verification" | "coordination" | "documentation" | "infrastructure">[]>>;
|
|
520
520
|
basis: import("@sinclair/typebox").TLiteral<"model-synthesis" | "prediction">;
|
|
521
521
|
confidence: import("@sinclair/typebox").TLiteral<"low"> | import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"high">, import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"low">]> | import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"low">]>;
|
|
522
522
|
evidence_event_ids: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
|
|
@@ -844,7 +844,7 @@ export declare const SemanticAgentReadModelV2Schema: import("@sinclair/typebox")
|
|
|
844
844
|
evidence_event_ids: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
|
|
845
845
|
}>>;
|
|
846
846
|
tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
847
|
-
value: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"recovery" | "research" | "waiting" | "planning" | "implementation" | "verification" | "coordination" | "
|
|
847
|
+
value: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"recovery" | "research" | "delivery" | "waiting" | "planning" | "implementation" | "verification" | "coordination" | "documentation" | "infrastructure">[]>>;
|
|
848
848
|
basis: import("@sinclair/typebox").TLiteral<"model-synthesis" | "prediction">;
|
|
849
849
|
confidence: import("@sinclair/typebox").TLiteral<"low"> | import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"high">, import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"low">]> | import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"medium">, import("@sinclair/typebox").TLiteral<"low">]>;
|
|
850
850
|
evidence_event_ids: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/** The sharing mode for this process, resolved once. Tests reset it with `resetStorageSharing`. */
|
|
2
2
|
export declare function stateSharing(): "private" | "group";
|
|
3
|
+
/** Directory mode for personal state in the user's home, which never follows project sharing. */
|
|
4
|
+
export declare const PERSONAL_DIR_MODE = 448;
|
|
5
|
+
/** File mode for personal state in the user's home, such as a per-install secret key. */
|
|
6
|
+
export declare const PERSONAL_FILE_MODE = 384;
|
|
3
7
|
export declare function resetStorageSharing(): void;
|
|
4
8
|
/**
|
|
5
9
|
* Mode for a directory of project state. Group mode includes setgid: Harnery chmods directories
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modes.d.ts","sourceRoot":"","sources":["../../../src/core/storage/modes.ts"],"names":[],"mappings":"AAgBA,mGAAmG;AACnG,wBAAgB,YAAY,IAAI,SAAS,GAAG,OAAO,CASlD;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wCAAwC;AACxC,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD"}
|
|
1
|
+
{"version":3,"file":"modes.d.ts","sourceRoot":"","sources":["../../../src/core/storage/modes.ts"],"names":[],"mappings":"AAgBA,mGAAmG;AACnG,wBAAgB,YAAY,IAAI,SAAS,GAAG,OAAO,CASlD;AAED,iGAAiG;AACjG,eAAO,MAAM,iBAAiB,MAAQ,CAAC;AAEvC,yFAAyF;AACzF,eAAO,MAAM,kBAAkB,MAAQ,CAAC;AAExC,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wCAAwC;AACxC,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD"}
|