Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .tickets/tex-nak8.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@ Episode 000003 adversarial projection found one further current-v2 gap: use_focu
**2026-08-01T23:52:53Z**

Episode 000003 now projects its exact 10,133,930-byte two-source prefix set as 212 source events: 210 readable resident turns plus two structural roots, with zero unsupported events, zero nonconforming lines, and zero unsupported_* diagnostics. Focused test 26/26; full 212 pass/1 scale skip; lint and production build pass. Lync upstream ownership is tracked as lyn-cnew.

**2026-08-02T08:21:50Z**

Removed Textile's now-redundant Behold v2 presenter overlay. Textile now consumes
the checksum-pinned Lync package from canonical commit `6e0734d` directly. Exact
episode 000014 projects all 692 source events with no unsupported or unclaimed
event; Textile retains only its reader, grouping, and storage responsibilities.
30 changes: 30 additions & 0 deletions .tickets/tex-q3ad.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
id: tex-q3ad
status: closed
deps: []
links: []
created: 2026-08-02T10:15:37Z
type: task
priority: 0
assignee: deepfates
external-ref: beh-wo5b
tags: [behold, lync, reader]
---
# Read Behold null cognition as a private-life event

Consume Lync's additive behold.entity-cognition-turn.v1 presenter so Textile shows admitted perception plus the resident's explicit no-bodily-action choice without fabricating an action or outcome.

## Acceptance Criteria

The checksum-pinned Lync candidate is updated, raw and indexed resident reading use the shared presenter, a focused Textile regression retains source identity and absent consequence fields, and full verify passes.


## Notes

**2026-08-02T10:15:42Z**

Vendored exact Lync 860aa549... archive dad3cff0..., updated provenance/docs, and focused raw reader regression passes 28/28. Pending full Textile verify.

**2026-08-02T10:16:18Z**

Full Textile verify passes: lint, 228 tests plus one opt-in scale skip, and both production builds. The installed physical package is the exact checksum-pinned Lync candidate.
6 changes: 6 additions & 0 deletions .tickets/tex-wrif.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,9 @@ Principal independent review on 2026-08-01: code and ownership design are accept
**2026-08-02T03:03:00Z**

2026-08-02 Behold episode 000003 ordinary browser check against current main: exact manifest is 2 sources / 20,556,307 bytes / digest 05307f02... The Stories drawer pointer Import Archive input accepts only one file; choosing a ZIP of the exact manifest + two Lync sources routes to the Twitter archive parser and fails for missing data/manifest.js. The intended hidden multiple input could not be opened through the keyboard/gamepad Import Archive action in the in-app browser, even though current source has the action. Therefore this exact retained source set is not yet claimed readable through the ordinary UI. Single-file historical Textile reading remains real. Candidate diagnosis is front-door multi-file activation/archive dispatch, distinct from the indexed-union scale work; reproduce before changing it.

2026-08-02 exact browser correction on current main: the ordinary visible multi-file picker does admit the Behold manifest plus both canonical resident files, but the first episode-000012 attempt failed because WebKit supplied a 1,114,112-byte native Blob chunk to Lync's explicitly 1,048,576-byte indexed admission boundary. The browser File adapter now splits arbitrary native chunks into at-most-1-MiB pieces without changing, copying, or weakening the authenticated prefix. A focused regression supplies the observed oversized shape. The repaired visible local-only reader then opened exact episode 000014: 56,638,047 authenticated bytes, 692 source events, 690 readable turns, two structural roots, all presented, zero nonconforming events. Navigating the real Lark life exposed the 1h23m wall-clock gap after turn 228 rather than presenting stop/resume as one scene. Full verify is 227 pass / one opt-in scale skip plus lint and production builds. This crosses the browser front-door defect and current 56 MB checkpoint; the ticket remains open for its stated representative retained six-hour-scale browser exercise rather than promoting the existing synthetic projector gate.

**2026-08-02T11:17:02Z**

2026-08-02 pinned Lync candidate 0a511b6 adds truthful place_held_against_focus presentation. The exact episode-000019 ordered-prefix front door imported 68,444,160 authenticated bytes: 871 source events, 869 readable, two structural, zero unsupported diagnostics, 12,278 explicit source-only/withheld diagnostics. Full Textile verify passes (228 pass, one opt-in scale skip, lint, production builds). This repairs presentation completeness but does not close the ticket's retained six-hour browser exercise.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
> **Status:** private application. Version 0.1.0 is the first coherent source
> release candidate for the raw-Lync corpus workflow and UI; it is not an npm
> package. Textile supports `@deepfates/lync >=0.3.0 <0.5.0`; this checkout pins
> the exact packed Lync 0.4.3 indexed-union candidate documented in
> the exact packed Lync 0.4.3 indexed-union and presentation candidate documented in
> `vendor/LYNC-PROVENANCE.md` pending an owner-authorized registry release.

Textile is a tactile multiverse reader for branching stories, conversations,
Expand Down Expand Up @@ -120,17 +120,22 @@ O(events + edges + public presentation text), not O(source bytes). The ordinary
multi-file picker reports resident/source/readable/structural/diagnostic counts
and rejects changed, truncated, conflicting, dangling, or cyclic input. Existing
single-file raw imports keep their eager, lossless archive/export behavior.
Source event times remain attached to their readable turns; when adjacent turns
are at least five minutes apart, the story surface marks the elapsed interval so
a stopped or otherwise discontinuous life is not presented as one continuous
scene. The marker describes time only and does not invent an episode boundary.

Readable events above one MiB of text use an explicit bounded reader with
FIRST/PREV/NEXT/LAST 65,536-character windows. This is presentation
virtualization, not truncation: the exact complete source string remains in the
story model, full-tree JSON, and kept-context export paths.

Textile reads raw corpus events through Lync's non-mutating, kind/profile-aware
presentation contract. The same contract is consumed by Splice and Curare; its
checksum-pinned local candidate keeps this private app installable before Lync
0.4 publication. For Behold `org.behold.inhabitant.v1`, it presents the safe
resident perception/action/outcome allowlist, preserves the original source
presentation contract. The same contract is consumed by Splice and Curare; the
checksum-pinned Lync package keeps this private app installable before Lync 0.4
publication. For Behold `org.behold.inhabitant.v1` and `.v2`, it presents the safe
resident perception/action/outcome allowlist plus explicit non-action cognition,
preserves the original source
events for full-tree export, and fails closed for unknown nested shapes. See
[the Behold resident Lync presentation boundary](docs/behold-resident-lync-presentation.md).

Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions client/interface/components/StoryText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ interface StoryTextProps {
}

const STORY_PATH_WINDOW_SIZE = 48;
const STORY_TIME_SEAM_MS = 5 * 60 * 1000;

function elapsedTimeLabel(elapsedMs: number): string {
const totalMinutes = Math.floor(elapsedMs / 60_000);
const days = Math.floor(totalMinutes / (24 * 60));
const hours = Math.floor((totalMinutes % (24 * 60)) / 60);
const minutes = totalMinutes % 60;
if (days > 0) return `${days}d${hours > 0 ? ` ${hours}h` : ""} later`;
if (hours > 0) return `${hours}h${minutes > 0 ? ` ${minutes}m` : ""} later`;
return `${minutes}m later`;
}

interface StoryNodeProseProps {
text: string;
Expand Down Expand Up @@ -215,6 +226,14 @@ export function StoryText({
{visiblePath.map((segment, visibleIndex) => {
const index = windowStart + visibleIndex;
const previous = currentPath[index - 1];
const elapsedMs =
previous?.createdAt !== undefined && segment.createdAt !== undefined
? segment.createdAt - previous.createdAt
: null;
const timeSeam =
showTurnBoundaries && elapsedMs !== null && elapsedMs >= STORY_TIME_SEAM_MS
? elapsedTimeLabel(elapsedMs)
Comment on lines +229 to +235

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit elapsed markers to timestamps with matching semantics

Only calculate this marker when both adjacent values are known source-event times. On a reopened kept-context artifact, projectRawLyncFile assigns a carried local turn's createdAt from local.keepEvent.at, while its source parent retains the source event time; this code therefore labels the interval between the source event and a later curation action as if it were the elapsed time before the local turn, potentially showing a large but fabricated narrative discontinuity.

Useful? React with 👍 / 👎.

: null;
const seam =
!showTurnBoundaries && previous
? storySeam(previous.text, segment.text)
Expand Down Expand Up @@ -253,6 +272,14 @@ export function StoryText({
data-node-id={segment.id}
data-source-text-length={segment.text.length}
>
{timeSeam ? (
<span
className="story-time-seam"
title={`${new Date(previous!.createdAt!).toISOString()} → ${new Date(segment.createdAt!).toISOString()}`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate timestamps before formatting the time seam

Reject or suppress out-of-range timestamps before calling toISOString(). A kept-conversation Markdown manifest can currently supply any numeric turn.createdAt because parseKeptConversationMarkdown validates only the top-level arrays; with two portable turns such as timestamps 0 and 9e99, the elapsed-time condition passes and this expression throws RangeError: Invalid time value, crashing the reader instead of rejecting the malformed import.

Useful? React with 👍 / 👎.

>
{timeSeam}
</span>
) : null}
{seam}
<StoryNodeProse
text={body}
Expand Down
28 changes: 28 additions & 0 deletions client/interface/components/__tests__/StoryText.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,34 @@ describe("StoryText prose surface", () => {
expect(render([human, model], 1)).not.toContain("story-turn-boundary");
});

it("makes a material source-time discontinuity visible without inventing an episode", () => {
const before: StoryNode = {
...unknown,
id: "resident-228",
text: "Stop worrying.",
sourceId: "source-228",
createdAt: Date.parse("2026-08-02T06:05:27.978Z"),
};
const after: StoryNode = {
...unknown,
id: "resident-229",
text: "Read the first private-life turn.",
sourceId: "source-229",
createdAt: Date.parse("2026-08-02T07:29:17.641Z"),
};
const html = render([before, after], 1);
expect(html).toContain("1h 23m later");
expect(html).toContain(
'title="2026-08-02T06:05:27.978Z → 2026-08-02T07:29:17.641Z"',
);

const continuous = render(
[before, { ...after, createdAt: before.createdAt! + 4 * 60_000 }],
1,
);
expect(continuous).not.toContain("story-time-seam");
});

it("presents a multi-megabyte turn as an explicit bounded reader window", () => {
const large: StoryNode = {
...unknown,
Expand Down
31 changes: 31 additions & 0 deletions client/interface/lync/__tests__/orderedLyncSourceSet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,37 @@ describe("Behold ordered Lync source sets", () => {
.rejects.toThrow(/read-only/);
});

it("normalizes browser-native Blob chunks to Lync's indexed admission bound", async () => {
const bytes = new Uint8Array(1024 * 1024 + 65_536).fill(7);
const file = {
name: "wide-browser-chunk.lync",
size: bytes.byteLength,
slice: () => ({
stream: () =>
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(bytes);
controller.close();
},
}),
arrayBuffer: async () => bytes.slice().buffer,
}),
} as unknown as File;
const binding = source(
0,
"OxfordWide",
"/canonical/OxfordWide/wide-browser-chunk.lync",
bytes,
"org.behold.inhabitant.v2",
);
const chunks: Uint8Array[] = [];
for await (const chunk of orderedLyncFileSource({ binding, file }).stream()) {
chunks.push(chunk);
}
expect(chunks.map((chunk) => chunk.byteLength)).toEqual([1024 * 1024, 65_536]);
expect(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)))).toEqual(Buffer.from(bytes));
});

it("fails closed on union conflicts and source changes between index and projection", async () => {
const lines = aster.toString("utf8").trimEnd().split("\n");
const conflicting = JSON.parse(lines[1]!);
Expand Down
39 changes: 39 additions & 0 deletions client/interface/lync/__tests__/rawLync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,45 @@ describe("raw .lync projection", () => {
.toEqual(raw.trim().split("\n").map((line) => JSON.parse(line)));
});

it("reads resident null cognition without fabricating an action outcome", () => {
const lines = readFileSync(oxfordResidentV2FixtureUrl, "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line));
const event = lines[1];
const cognition = event.payload.payload;
cognition.protocol = "behold.entity-cognition-turn.v1";
cognition.id = "OxfordCedar:cognition:7";
cognition.utterance = {
assistant: { role: "assistant", content: '{"action":null,"arguments":{}}' },
};
delete cognition.action;
delete cognition.outcome;
delete cognition.nextObservation;

const projection = projectRawLyncFile(
`${lines.map(JSON.stringify).join("\n")}\n`,
"resident-cognition.lync",
);
expect(projection.readableEventCount).toBe(1);
expect(projection.unsupportedEventCount).toBe(0);
const beat = projection.snapshot.turns.find(
(turn) => turn.meta.sourceKind === "lync/turn",
);
expect(beat?.payload.text).toContain("OxfordCedar · cognition 7");
expect(beat?.payload.text).toContain("OxfordCedar chose no bodily action.");
expect(beat?.payload.text).not.toContain("unknown outcome");
expect(beat?.meta.sourcePresentationSections?.map((section) => section.role)).toEqual([
"perception",
"action",
]);
const storedTurn = (beat?.meta.sourceEvent?.payload as {
payload?: Record<string, unknown>;
})?.payload;
expect(storedTurn?.action).toBeUndefined();
expect(storedTurn?.outcome).toBeUndefined();
});

it("distinguishes resident chat input dispatch from independently observed delivery", () => {
for (const actionName of ["chat", "whisper"] as const) {
const events = readV2FixtureEvents();
Expand Down
15 changes: 13 additions & 2 deletions client/interface/lync/indexedRawLync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export interface IndexedRawLyncProjection extends RawLyncProjection {
ownership: OrderedLyncReviewOwnership;
}

// Match Lync's default indexed-union admission bound. Browser Blob streams do
// not promise a particular chunk size and WebKit can hand us a chunk slightly
// larger than one MiB, so the File adapter—not the corpus—owns normalizing it.
const INDEXED_LYNC_STREAM_CHUNK_BYTES = 1024 * 1024;

/** Browser File adapter that exposes only the manifest-authenticated prefix. */
export function orderedLyncFileSource(source: OrderedLyncSourceFile): ReReadableLyncSource {
const { binding, file } = source;
Expand All @@ -46,8 +51,14 @@ export function orderedLyncFileSource(source: OrderedLyncSourceFile): ReReadable
break;
}
const chunk = result.value.subarray(0, remaining);
supplied += chunk.byteLength;
yield chunk;
for (let start = 0; start < chunk.byteLength; start += INDEXED_LYNC_STREAM_CHUNK_BYTES) {
const bounded = chunk.subarray(
start,
Math.min(chunk.byteLength, start + INDEXED_LYNC_STREAM_CHUNK_BYTES),
);
supplied += bounded.byteLength;
yield bounded;
}
if (supplied === binding.endOffset) {
await reader.cancel();
break;
Expand Down
11 changes: 6 additions & 5 deletions client/interface/lync/rawLyncPresentation.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { LyncEventBody } from "@deepfates/lync/events";
import {
BEHOLD_INHABITANT_PROFILE,
presentLyncEvent,
resolveLyncPresentationProfiles,
type LyncPresentation,
type LyncPresentationContext,
} from "../../../vendor/lync-presentation/index.js";
} from "@deepfates/lync/presentation";

export { BEHOLD_INHABITANT_PROFILE, resolveLyncPresentationProfiles };
export {
BEHOLD_INHABITANT_PROFILE,
resolveLyncPresentationProfiles,
} from "@deepfates/lync/presentation";

export type {
LyncPresentation as RawLyncPresentation,
Expand All @@ -16,7 +17,7 @@ export type {
LyncPresentationRole as RawLyncPresentationRole,
LyncPresentationSection as RawLyncPresentationSection,
LyncPresentationSource as RawLyncPresentationSource,
} from "../../../vendor/lync-presentation/index.js";
} from "@deepfates/lync/presentation";

export type RawLyncPresentationContext = LyncPresentationContext;

Expand Down
6 changes: 3 additions & 3 deletions client/interface/lync/rawLyncPresentationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type {
LyncPresentationRole as RawLyncPresentationRole,
LyncPresentationSection as RawLyncPresentationSection,
LyncPresentationSource as RawLyncPresentationSource,
} from "../../../vendor/lync-presentation/index.js";
} from "@deepfates/lync/presentation";

/**
* Compact on-Loom form for a presentation section. When a section is exactly
Expand All @@ -14,9 +14,9 @@ export type {
* expands this marker back to the public Lync presentation shape.
*/
export type StoredRawLyncPresentationSection =
| import("../../../vendor/lync-presentation/index.js").LyncPresentationSection
| import("@deepfates/lync/presentation").LyncPresentationSection
| {
role: import("../../../vendor/lync-presentation/index.js").LyncPresentationRole;
role: import("@deepfates/lync/presentation").LyncPresentationRole;
sourcePaths: string[];
sameAsTurnText: true;
};
10 changes: 7 additions & 3 deletions client/interface/lync/storyLoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -602,9 +602,13 @@ function turnToStoryNode(
return {
id: turn.id,
text,
// Archive/portable artifacts need their source time for an honest reopen.
// Keep legacy story projections byte-for-byte shaped as before.
...(meta?.archiveSource || meta?.portableTurnId ? { createdAt: turn.createdAt } : {}),
// Imported artifacts need their source time for honest reading. In
// particular, an indexed resident life may cross a stop/resume boundary
// while remaining one causal thread; dropping its event times makes that
// discontinuity look like one uninterrupted scene.
...(meta?.archiveSource || meta?.portableTurnId || meta?.sourceId
? { createdAt: turn.createdAt }
: {}),
continuations: [],
origin: originFromMeta(meta),
actor: meta?.author,
Expand Down
8 changes: 8 additions & 0 deletions client/styles/terminal.css
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,14 @@ html, body {
margin-top: 0.65rem;
}

.story-text .story-time-seam {
display: block;
margin: 0.9rem 0 0.45rem;
color: var(--theme-muted);
font-size: 0.82em;
opacity: 0.82;
}

.story-text .story-path-window-notice {
display: block;
margin: 0.35rem 0 0.65rem;
Expand Down
Loading