Skip to content

fix(artifacts): hold back partial <artifact> open tag mid-attribute#95

Merged
hqhq1025 merged 2 commits intomainfrom
wt/loop-fix-parser-partial-open-tag
Apr 19, 2026
Merged

fix(artifacts): hold back partial <artifact> open tag mid-attribute#95
hqhq1025 merged 2 commits intomainfrom
wt/loop-fix-parser-partial-open-tag

Conversation

@hqhq1025
Copy link
Copy Markdown
Collaborator

Summary

  • findSafeFlushPoint in the streaming artifact parser only guarded against tails that are a strict prefix of <artifact (e.g. <art). When a chunk split landed mid-attribute — for example <artifact identifier=\"a1\" type=\"html\" then title=\"t\">body</artifact> — the tail already starts with <artifact but has no > yet, so the function flushed the partial open tag as a text event. The next chunk began past the tag, OPEN_TAG_RE never matched, and the artifact was silently lost into the chat stream.
  • Now also hold the tail back when it starts with <artifact and the closing > hasn't arrived. Added a regression test that asserts both the correct artifact:start/artifact:end events fire and no <artifact substring leaks into a text event.

Constraint checks

  • Compatibility: no public API change, identical behavior for already-correct splits.
  • Upgradeability: pure logic patch, no schema or IPC change.
  • No bloat: +1 condition, +1 test case.
  • Elegance: single-line guard, comment explains the why.

Test plan

  • pnpm --filter @open-codesign/artifacts test — 6/6 pass with fix; new test fails on unfixed parser (verified locally)
  • CI green

`findSafeFlushPoint` only held the tail back when it was a strict prefix
of `<artifact` (e.g. `<art`). A stream split mid-attribute — say
`<artifact identifier="a1" type="html"` followed by ` title="t">…</artifact>`
— left a tail starting with `<artifact ` but with no `>` yet, so the
function flushed the partial open tag as a `text` event. The next chunk
began past the open tag and `OPEN_TAG_RE` never matched, so the artifact
was silently lost.

Also hold back when the tail starts with `<artifact` and the closing `>`
has not arrived. Added a regression test covering the mid-attribute split
and asserting no `<artifact` substring leaks into a `text` event.
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Findings

  • [Major] False-positive <artifact... prefixes can stall normal text streaming and grow buffer usage — findSafeFlushPoint now holds back any tail starting with <artifact when > is missing, including non-tag text like <artifactual ... that can never match OPEN_TAG_RE; this prevents incremental text emission and can accumulate unbounded buffered text in long streams, evidence packages/artifacts/src/parser.ts:175.
    Suggested fix:

    const tail = buffer.slice(ltIdx);
    if ('<artifact'.startsWith(tail)) return ltIdx;
    
    const isArtifactOpenStart = /^<artifact(?:\s|$)/.test(tail);
    if (isArtifactOpenStart && !tail.includes('>')) return ltIdx;
  • [Minor] Missing regression test for non-tag <artifact... prefixes — the new test validates mid-attribute split recovery, but there is no guard test for plain text like <artifactual to ensure text events still stream and no holdback regression is introduced, evidence packages/artifacts/src/parser.test.ts:51.
    Suggested fix:

    it('does not hold back plain text starting with "<artifact" but not a tag', () => {
      const events = collectEvents(['before <artifactual text', ' continues']);
      expect(events).toEqual([
        { type: 'text', delta: 'before <artifactual text' },
        { type: 'text', delta: ' continues' },
      ]);
    });

Summary

  • Review mode: initial
  • 2 issue(s) found in the latest diff. docs/VISION.md and docs/PRINCIPLES.md were Not found in repo/docs, so project-level policy cross-check relied on CLAUDE.md and changed files only.

Testing

  • Not run (automation)

open-codesign Bot

Comment thread packages/artifacts/src/parser.ts Outdated
if (ltIdx === -1) return buffer.length;
const tail = buffer.slice(ltIdx);
if ('<artifact'.startsWith(tail)) return ltIdx;
if (tail.startsWith('<artifact') && !tail.includes('>')) return ltIdx;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Major] This condition is too broad: tail.startsWith('<artifact') also matches non-tag text (for example <artifactual ...) that will never satisfy OPEN_TAG_RE. In streaming mode this can suppress text events and grow state.buffer until flush.

Suggested fix:

const isArtifactOpenStart = /^<artifact(?:\s|$)/.test(tail);
if (isArtifactOpenStart && !tail.includes('>')) return ltIdx;

});
});

it('handles open tag split mid-attribute (between attrs, before final ">")', () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Minor] Please add a regression test for non-tag prefixes such as <artifactual to ensure this holdback rule does not delay normal text streaming.

Suggested test:

it('does not hold back plain text starting with "<artifact" but not a tag', () => {
  const events = collectEvents(['before <artifactual text', ' continues']);
  expect(events).toEqual([
    { type: 'text', delta: 'before <artifactual text' },
    { type: 'text', delta: ' continues' },
  ]);
});

@hqhq1025
Copy link
Copy Markdown
Collaborator Author

Addressed Codex [Major] finding on the <artifact guard in findSafeFlushPoint (15083b6).

The previous guard if (tail.startsWith('<artifact') && !tail.includes('>')) return ltIdx; would hold the buffer indefinitely on plain text like <artifactual or <artifact-like, stalling the text stream.

Tightened to only hold back when the next char is whitespace (attributes coming) or absent (need more bytes to decide). A letter/digit/dash means a different word — flush it.

Added 2 regression tests:

  • 'the <artifactual data' + ' here' → flushes as text, no artifact:start.
  • 'something <artifact-like' + ' suffix' → flushes as text, no artifact:start.

Original mid-attribute split test still passes. pnpm --filter @open-codesign/artifacts test → 8/8 green.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Findings

  • No new issues found in the modified lines with >=80% confidence.

Summary

  • Review mode: follow-up after new commits
  • Previously reported false-positive holdback risk is addressed by the new next-character guard in packages/artifacts/src/parser.ts:175, and follow-up tests for <artifactual / <artifact-like are present in packages/artifacts/src/parser.test.ts:92 and packages/artifacts/src/parser.test.ts:102.
  • docs/VISION.md and docs/PRINCIPLES.md: Not found in repo/docs.
  • Residual risk: parser behavior for malformed <artifact> variants (for example missing required attributes) is still governed by existing parser rules and not expanded in this diff.

Testing

  • Not run (automation)

open-codesign Bot

@hqhq1025 hqhq1025 merged commit b034c17 into main Apr 19, 2026
4 of 6 checks passed
@hqhq1025 hqhq1025 deleted the wt/loop-fix-parser-partial-open-tag branch April 19, 2026 07:37
hqhq1025 added a commit that referenced this pull request Apr 19, 2026
- packages/artifacts/src/parser.test.ts: biome auto-fix line wrap (formatting
  regression that slipped in with #95)
- packages/core/src/skills/loader.ts: add biome-ignore on parseMapping (score 23)
  and loadSkillsFromDir (score 19) to unblock pre-push hook for all PR branches.
  Refactor tracked in issue #94.

Both were causing agents to bypass --no-verify or push from temp worktrees.
This restores the contract that hooks pass on main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant