Skip to content

Commit 10627ff

Browse files
committed
feat(learning): add session execution history ledger and workbench view
1 parent f0c2cf5 commit 10627ff

11 files changed

Lines changed: 336 additions & 3 deletions

src/frontend/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,6 +1465,13 @@ <h4>Session Execution</h4>
14651465
<pre id="learning-session-execution" class="workbench-feedback">No session execution yet.</pre>
14661466
</div>
14671467

1468+
<div class="workbench-section">
1469+
<h4>Session History</h4>
1470+
<ul id="learning-session-history" class="workbench-list">
1471+
<li class="muted">No session history yet.</li>
1472+
</ul>
1473+
</div>
1474+
14681475
<div id="learning-runtime-summary" class="workbench-runtime">
14691476
Runtime: unavailable
14701477
</div>

src/frontend/path.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ <h4>Session Execution</h4>
124124
<pre id="learning-session-execution" class="workbench-feedback">No session execution yet.</pre>
125125
</div>
126126

127+
<div class="workbench-section">
128+
<h4>Session History</h4>
129+
<ul id="learning-session-history" class="workbench-list">
130+
<li class="muted">No session history yet.</li>
131+
</ul>
132+
</div>
133+
127134
<div id="learning-runtime-summary" class="workbench-runtime">
128135
Runtime: unavailable
129136
</div>

src/frontend/path_app.js

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ window.pathApp = {
5050
runtimeState: null,
5151
tutorFeedback: null,
5252
sessionExecution: null,
53+
sessionHistory: null,
5354
},
5455

5556
// Animation State
@@ -1876,7 +1877,7 @@ window.pathApp = {
18761877
const includeDivergence = true;
18771878
const includeRetrain = true;
18781879
try {
1879-
const [sessionPlan, qualitySnapshot, misconceptions, runtimeState] = await Promise.all([
1880+
const [sessionPlan, qualitySnapshot, misconceptions, runtimeState, sessionHistory] = await Promise.all([
18801881
this._requestLearningApi('/api/knowledge/session/plan', {
18811882
userId,
18821883
focusAtomIds,
@@ -1895,13 +1896,18 @@ window.pathApp = {
18951896
fetch('/api/knowledge/state', { method: 'GET' })
18961897
.then((response) => response.json())
18971898
.catch(() => null),
1899+
this._requestLearningApi('/api/knowledge/session/history', {
1900+
userId,
1901+
limit: 8,
1902+
}),
18981903
]);
18991904
this.learningWorkbench.sessionPlan = sessionPlan || null;
19001905
this.learningWorkbench.qualitySnapshot = qualitySnapshot || null;
19011906
this.learningWorkbench.misconceptions = misconceptions || null;
19021907
this.learningWorkbench.runtimeState = runtimeState && runtimeState.success === true
19031908
? runtimeState
19041909
: null;
1910+
this.learningWorkbench.sessionHistory = sessionHistory || null;
19051911
this.learningWorkbench.lastUpdatedAt = new Date().toISOString();
19061912

19071913
const actionCount = Number(sessionPlan?.summary?.totalActions || sessionPlan?.actions?.length || 0);
@@ -1979,6 +1985,49 @@ window.pathApp = {
19791985
return answerMap;
19801986
},
19811987

1988+
_appendLearningWorkbenchSessionRecord: function(record) {
1989+
if (!record || typeof record !== 'object') {
1990+
return;
1991+
}
1992+
const recordId = String(record.id || '').trim();
1993+
if (!recordId) {
1994+
return;
1995+
}
1996+
const base = this.learningWorkbench.sessionHistory && typeof this.learningWorkbench.sessionHistory === 'object'
1997+
? this.learningWorkbench.sessionHistory
1998+
: {
1999+
userId: this.learningWorkbench.userId,
2000+
generatedAt: new Date().toISOString(),
2001+
records: [],
2002+
summary: {
2003+
totalRecords: 0,
2004+
totalExecutedActions: 0,
2005+
totalUpdatedMasteryCount: 0,
2006+
averageMasteryDelta: 0,
2007+
averageTutorConfidence: 0,
2008+
},
2009+
};
2010+
const existingRecords = Array.isArray(base.records) ? base.records.slice() : [];
2011+
const deduped = [record, ...existingRecords.filter((item) => String(item?.id || '').trim() !== recordId)].slice(0, 8);
2012+
this.learningWorkbench.sessionHistory = {
2013+
...base,
2014+
generatedAt: new Date().toISOString(),
2015+
records: deduped,
2016+
summary: {
2017+
...(base.summary || {}),
2018+
totalRecords: deduped.length,
2019+
totalExecutedActions: deduped.reduce((sum, item) => sum + Number(item?.executedCount || 0), 0),
2020+
totalUpdatedMasteryCount: deduped.reduce((sum, item) => sum + Number(item?.updatedMasteryCount || 0), 0),
2021+
averageMasteryDelta: deduped.length > 0
2022+
? deduped.reduce((sum, item) => sum + Number(item?.averageMasteryDelta || 0), 0) / deduped.length
2023+
: 0,
2024+
averageTutorConfidence: deduped.length > 0
2025+
? deduped.reduce((sum, item) => sum + Number(item?.averageTutorConfidence || 0), 0) / deduped.length
2026+
: 0,
2027+
},
2028+
};
2029+
},
2030+
19822031
executeLearningWorkbenchAction: async function(params = {}) {
19832032
const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
19842033
const atomId = String(params.atomId || '').trim();
@@ -2075,6 +2124,7 @@ window.pathApp = {
20752124
try {
20762125
const result = await this._requestLearningApi('/api/knowledge/session/execute', {
20772126
userId,
2127+
executionKind: 'session',
20782128
sessionPlan,
20792129
actionLimit,
20802130
answersByActionId,
@@ -2087,6 +2137,7 @@ window.pathApp = {
20872137
...result,
20882138
receivedAt: new Date().toISOString(),
20892139
};
2140+
this._appendLearningWorkbenchSessionRecord(result?.record || null);
20902141
const firstExecuted = Array.isArray(result?.items)
20912142
? result.items.find((item) => item && item.status === 'executed' && item.result)
20922143
: null;
@@ -2159,6 +2210,7 @@ window.pathApp = {
21592210
try {
21602211
const result = await this._requestLearningApi('/api/knowledge/session/execute', {
21612212
userId,
2213+
executionKind: 'retest',
21622214
sessionPlan: retestSessionPlan,
21632215
actionLimit: retestActions.length,
21642216
answersByActionId,
@@ -2172,6 +2224,7 @@ window.pathApp = {
21722224
...result,
21732225
receivedAt: new Date().toISOString(),
21742226
};
2227+
this._appendLearningWorkbenchSessionRecord(result?.record || null);
21752228
const summary = result?.summary || {};
21762229
this._setLearningWorkbenchStatus(
21772230
`Retest execution finished: executed ${Number(summary.executedCount || 0)}/${Number(summary.attemptedActions || 0)}, mastery delta ${Number(summary.averageMasteryDelta || 0).toFixed(3)}.`
@@ -2199,6 +2252,7 @@ window.pathApp = {
21992252
const updatedEl = document.getElementById('learning-workbench-updated-at');
22002253
const tutorFeedbackEl = document.getElementById('learning-tutor-feedback');
22012254
const sessionExecutionEl = document.getElementById('learning-session-execution');
2255+
const sessionHistoryEl = document.getElementById('learning-session-history');
22022256

22032257
if (updatedEl) {
22042258
updatedEl.textContent = this.learningWorkbench.lastUpdatedAt
@@ -2351,15 +2405,33 @@ window.pathApp = {
23512405
}
23522406
}
23532407

2408+
if (sessionHistoryEl) {
2409+
const records = this.learningWorkbench.sessionHistory?.records || [];
2410+
if (!Array.isArray(records) || records.length === 0) {
2411+
sessionHistoryEl.innerHTML = '<li class="muted">No session history yet.</li>';
2412+
} else {
2413+
sessionHistoryEl.innerHTML = records.slice(0, 8).map((record) => {
2414+
const executedAt = new Date(record.executedAt || Date.now()).toLocaleString();
2415+
const kind = this._escapeHtml(String(record.executionKind || 'session'));
2416+
const executedCount = Number(record.executedCount || 0);
2417+
const attempted = Number(record.attemptedActions || 0);
2418+
const delta = Number(record.averageMasteryDelta || 0);
2419+
const signedDelta = `${delta >= 0 ? '+' : ''}${delta.toFixed(3)}`;
2420+
return `<li><span class="chip">${kind}</span> ${executedAt} · ${executedCount}/${attempted} · mastery Δ ${signedDelta}</li>`;
2421+
}).join('');
2422+
}
2423+
}
2424+
23542425
if (runtimeEl) {
23552426
const runtimeState = this.learningWorkbench.runtimeState?.state || null;
23562427
if (!runtimeState) {
23572428
runtimeEl.textContent = 'Runtime: unavailable';
23582429
} else {
23592430
const sessionTelemetry = runtimeState.sessionActionTelemetry || null;
2431+
const historyCount = Number(runtimeState.sessionExecutionHistoryRecords || 0);
23602432
const sessionSummary = sessionTelemetry
2361-
? `, sessionActions=${Number(sessionTelemetry.executionCount || 0)}, inferred=${Number(sessionTelemetry.inferredMasteryUpdateCount || 0)}, explicit=${Number(sessionTelemetry.explicitMasteryUpdateCount || 0)}`
2362-
: '';
2433+
? `, sessionActions=${Number(sessionTelemetry.executionCount || 0)}, inferred=${Number(sessionTelemetry.inferredMasteryUpdateCount || 0)}, explicit=${Number(sessionTelemetry.explicitMasteryUpdateCount || 0)}, history=${historyCount}`
2434+
: `, history=${historyCount}`;
23632435
runtimeEl.textContent = `Runtime: docs=${runtimeState.documents}, atoms=${runtimeState.activeAtoms}, relations=${runtimeState.activeRelationEdges}, ingestP95=${Number(runtimeState.ingestTelemetry?.ingestP95Ms || 0).toFixed(2)}ms${sessionSummary}`;
23642436
}
23652437
}

src/knowledge.api.contract.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe('Knowledge mastery API contract wiring', () => {
1919
'/api/knowledge/session/plan',
2020
'/api/knowledge/session/action',
2121
'/api/knowledge/session/execute',
22+
'/api/knowledge/session/history',
2223
'/api/knowledge/quality/snapshot',
2324
'/api/knowledge/quality/evaluate',
2425
'/api/knowledge/ingest/guardrails/evaluate',
@@ -47,6 +48,7 @@ describe('Knowledge mastery API contract wiring', () => {
4748
'interface MasteryMisconceptionAPI',
4849
'interface LearningPathAPI',
4950
'interface StudySessionAPI',
51+
'interface StudySessionHistoryAPI',
5052
'interface StudySessionActionAPI',
5153
'interface StudySessionPlanExecutionAPI',
5254
'interface TutorActionAPI',

src/learning/KnowledgeLearningPlatform.persistence.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,21 @@ describe('KnowledgeLearningPlatform persistence', () => {
8282
persistMemory: true,
8383
memoryLayer: 'session',
8484
});
85+
const sessionPlan = await platformA.buildStudySession({
86+
userId: 'user_persist',
87+
focusAtomIds: [atomId],
88+
maxActions: 2,
89+
includeDivergence: false,
90+
includeRetrain: false,
91+
});
92+
await platformA.executeStudySessionPlan({
93+
userId: 'user_persist',
94+
executionKind: 'session',
95+
sessionPlan,
96+
actionLimit: 1,
97+
persistMemory: true,
98+
memoryLayer: 'session',
99+
});
85100

86101
expect(fs.existsSync(snapshotPath)).toBe(true);
87102

@@ -101,6 +116,7 @@ describe('KnowledgeLearningPlatform persistence', () => {
101116
expect(state.retrievalTelemetry.queryCount).toBeGreaterThan(0);
102117
expect(state.sessionActionTelemetry.executionCount).toBeGreaterThan(0);
103118
expect(state.sessionActionTelemetry.analyzedAnswerCount).toBeGreaterThan(0);
119+
expect(state.sessionExecutionHistoryRecords).toBeGreaterThan(0);
104120

105121
const queryResult = await platformB.queryKnowledge({
106122
query: 'persistence snapshots restarts',
@@ -113,6 +129,13 @@ describe('KnowledgeLearningPlatform persistence', () => {
113129
expect(storeDiagnostics.exists).toBe(true);
114130
expect(storeDiagnostics.loaded).toBe(true);
115131

132+
const history = await platformB.queryStudySessionHistory({
133+
userId: 'user_persist',
134+
limit: 5,
135+
});
136+
expect(history.records.length).toBeGreaterThan(0);
137+
expect(history.records[0]?.executionKind).toBe('session');
138+
116139
const guardrail = await platformB.evaluateIngestGuardrails({});
117140
expect(guardrail.latestSummary).not.toBeNull();
118141
expect(guardrail.latestSummary?.ingestedDocuments).toBe(1);

src/learning/KnowledgeLearningPlatform.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,7 @@ describe('KnowledgeLearningPlatform', () => {
535535
const execution = await platform.executeStudySessionPlan({
536536
userId: 'user_session_execute',
537537
sessionPlan,
538+
executionKind: 'session',
538539
actionLimit: 3,
539540
includeRetestPlan: false,
540541
persistMemory: true,
@@ -554,9 +555,19 @@ describe('KnowledgeLearningPlatform', () => {
554555
expect(execution.masteryDelta.comparedAtoms).toBeGreaterThan(0);
555556
expect(execution.masteryDelta.items.length).toBe(execution.masteryDelta.comparedAtoms);
556557
expect(execution.retestPlan.summary.totalActions).toBe(0);
558+
expect(execution.record.userId).toBe('user_session_execute');
559+
expect(execution.record.executionKind).toBe('session');
560+
const history = await platform.queryStudySessionHistory({
561+
userId: 'user_session_execute',
562+
limit: 5,
563+
});
564+
expect(history.records.length).toBeGreaterThanOrEqual(1);
565+
expect(history.records[0]?.id).toBe(execution.record.id);
566+
expect(history.summary.totalExecutedActions).toBeGreaterThan(0);
557567

558568
const stateAfterExecution = platform.getKnowledgeState();
559569
expect(stateAfterExecution.sessionActionTelemetry.executionCount).toBeGreaterThanOrEqual(3);
570+
expect(stateAfterExecution.sessionExecutionHistoryRecords).toBeGreaterThanOrEqual(1);
560571
});
561572

562573
test('session plan execution consumes answersByActionId for auto analysis and inferred mastery updates', async () => {
@@ -589,6 +600,7 @@ describe('KnowledgeLearningPlatform', () => {
589600
...sessionPlan,
590601
actions: [firstAction],
591602
},
603+
executionKind: 'retest',
592604
actionLimit: 1,
593605
answersByActionId: {
594606
[firstAction.id]: 'xylophone quasar nebula',
@@ -611,6 +623,7 @@ describe('KnowledgeLearningPlatform', () => {
611623
expect(execution.masteryDelta.items[0]?.updatedByExecution).toBe(true);
612624
expect(execution.retestPlan.summary.totalActions).toBeGreaterThanOrEqual(1);
613625
expect(execution.retestPlan.actions[0]?.source).toBe('retrain_plan');
626+
expect(execution.record.executionKind).toBe('retest');
614627
});
615628

616629
test('learning quality evaluation enforces mastery and evidence thresholds', async () => {

0 commit comments

Comments
 (0)